Flutter Android 端熱修復(熱更新)實踐

上一篇 文章中,簡單分析了一下 Flutter 在 Android 端的啓動流程,雖然沒有更深刻的分析,可是咱們能夠了解到,對於 Flutter 端的 Dart VM 的啓動等,是經過 Android 傳遞的資源(或者說路徑)過去,Dart VM 加載這些資源完成初始化的,那麼咱們能夠經過動態替換資源就能夠達到熱更新的目的。git

注意:github

  • 不一樣版本的 Flutter 代碼與邏輯可能有所不一樣,但總體流程大同小異。
  • 一樣的,不一樣版本 Flutter 編譯以後的產物不一樣,
  • Release 模式 和 Debug 模式下的編譯產物不一樣,這裏以 Release 爲例,代碼也是 Release 版本的代碼。

本次測試的開發環境:shell

  • Android Studio 3.5
  • Flutter 1.10.3-pre.39 chanel master
  • Dart 2.6.0

1、資源複製

經過以前文章的分析,能夠知道,FlutterMain 這個類中,會傳遞指定資源路徑,提供給 Dart VM 進行初始化。編程

這裏面有兩個重要的資源,一個是 libflutter.so ,一個是 libapp.so。 經過名字就能夠看出來,libflutter.so 是框架相關的庫,而 libapp.so 就是咱們寫的代碼編譯成的 so 庫,咱們就是要經過動態替換這個文件,達到熱更新的目的。

爲了可以讓 Dart VM 加載咱們修改以後的 so 庫,咱們確定須要將修改後的 so 庫放到 app 的私有目錄下。這裏直接從手機根目錄下獲取,固然從網絡下載等都是一樣的道理。 先定義一個輔助類,將文件複製到手機私有目錄下。bash

public class FlutterFileUtils {
    ///將文件拷貝到私有目錄
    public static String copyLibAndWrite(Context context, String fileName){
        try {
            File dir = context.getDir("libs", Activity.MODE_PRIVATE);
            File destFile = new File(dir.getAbsolutePath() + File.separator + fileName);
            if (destFile.exists() ) {
                destFile.delete();
            }

            if (!destFile.exists()){
                boolean res = destFile.createNewFile();
                if (res){

                    String path = Environment.getExternalStorageDirectory().toString();
                    FileInputStream is = new FileInputStream(new File(path + "/" + fileName));

                    FileOutputStream fos = new FileOutputStream(destFile);
                    byte[] buffer = new byte[is.available()];
                    int byteCount;
                    while ((byteCount = is.read(buffer)) != -1){
                        fos.write(buffer,0,byteCount);
                    }
                    fos.flush();
                    is.close();
                    fos.close();
                    return destFile.getAbsolutePath();
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
        return "";
    }

}

複製代碼

在程序啓動的時候,咱們調用這個方法,將文件複製過去,也就是在 MainActivity 的 onCreate 方法中。微信

@Override
  protected void onCreate(Bundle savedInstanceState) {

    String path = FlutterFileUtils.copyLibAndWrite(MainActivity.this,"libapp_fix.so");
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }
複製代碼

複製文件等操做都須要讀寫權限,不要忘了。網絡

2、自定義 FlutterActivity 和 FlutterActivityDelegat

在以前分析啓動流程的文章中,提到過,MainActivity 繼承自 FlutterActivity,而 FlutterActivity 只是一個代理類,真正的操做都是在 FlutterActivityDelegate 這個類中進行的,而在 FlutterActivityDelegate 中會調用 FlutterMain 中的方法進行 Dart VM 等的初始化。 所以咱們要作的就是,修改 FlutterActivity 和 FlutterActivityDelegate 這兩個類,以達到修改 FlutterMain 的目的。這裏爲了方便,只是簡單的複製了一份代碼,將 FlutterActivity 改成 HotFixFlutterActivity,FlutterActivityDelegate 改成 HotFixFlutterActivityDelegate ,而後修改裏面的代碼,固然還有其餘的方法,這裏不在演示。app

一、修改 MainActivity 爲繼承自咱們本身的 HotFixFlutterActivity
public class MainActivity extends HotFixFlutterActivity implements EasyPermissions.PermissionCallbacks
複製代碼
二、HotFixFlutterActivity 中將 FlutterActivityDelegate 替換爲咱們本身的 HotFixFlutterActivityDelegate
public class HotFixFlutterActivity extends Activity implements FlutterView.Provider, PluginRegistry, HotFixFlutterActivityDelegate.ViewFactory {

    private final HotFixFlutterActivityDelegate delegate = new HotFixFlutterActivityDelegate(this, this);
    private final FlutterActivityEvents eventDelegate;
    private final FlutterView.Provider viewProvider;
    private final PluginRegistry pluginRegistry;

    public HotFixFlutterActivity() {
        this.eventDelegate = this.delegate;
        this.viewProvider = this.delegate;
        this.pluginRegistry = this.delegate;
    }
    ...
    }
複製代碼
三、修改 HotFixFlutterActivityDelegate

代碼修改到這裏,當程序運行後,MainActivity 的 onCreate 方法裏面會執行到 HotFixFlutterActivityDelegate 的 onCreate 方法中,而在這裏,會調用 FlutterMain 裏面的方法進行初始化操做,所以咱們還須要修改 onCreate 這個方法。框架

onCreate 中默認調用的代碼以下:ide

FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);
複製代碼

咱們確定須要本身定義一個相似的文件,修改裏面的方法,來提供咱們調用達到替換資源的目的。好比咱們定義的相似的類叫 MyFlutterMain,那麼 這裏的代碼修改成以下:

public void onCreate(Bundle savedInstanceState) {
        if (Build.VERSION.SDK_INT >= 21) {
            Window window = this.activity.getWindow();
            window.addFlags(-2147483648);
            window.setStatusBarColor(1073741824);
            window.getDecorView().setSystemUiVisibility(1280);
        }

        String[] args = getArgsFromIntent(this.activity.getIntent());
        MyFlutterMain.startInitialization(this.activity.getApplicationContext());
        MyFlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);
        this.flutterView = this.viewFactory.createFlutterView(this.activity);
        if (this.flutterView == null) {
            FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
            this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
            this.flutterView.setLayoutParams(matchParent);
            this.activity.setContentView(this.flutterView);
            this.launchView = this.createLaunchView();
            if (this.launchView != null) {
                this.addLaunchView();
            }
        }

        if (!this.loadIntent(this.activity.getIntent())) {
            String appBundlePath = MyFlutterMain.findAppBundlePath();
            if (appBundlePath != null) {
                this.runBundle(appBundlePath);
            }

        }
    }
複製代碼

注意,這裏多了一行:

MyFlutterMain.startInitialization(this.activity.getApplicationContext());
複製代碼

主要是在ensureInitializationComplete這裏,會進行一個判斷:

if (Looper.myLooper() != Looper.getMainLooper()) {
                throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
            } else if (sSettings == null) {
                throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
            } 
複製代碼

而只有在 startInitialization 以後,sSettings 纔會被初始化,正常狀況下,FlutterMain.startInitialization 這個方法是在 Application 的 onCreate 中調用的:

public class FlutterApplication extends Application {
    private Activity mCurrentActivity = null;

    public FlutterApplication() {
    }

    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }

    public Activity getCurrentActivity() {
        return this.mCurrentActivity;
    }

    public void setCurrentActivity(Activity mCurrentActivity) {
        this.mCurrentActivity = mCurrentActivity;
    }
}

複製代碼

由於咱們沒有修改這裏的代碼,因此咱們要本身初始化一下,固然也能夠本身在定義一個 Application 而後修改這裏的代碼。

3、加載本身的 so

這裏主要是修改 MyFlutterMain 中的 ensureInitializationComplete 方法,加載咱們本身複製到手機私用目錄下的那個 so 就好了。

public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
   if (!isRunningInRobolectricTest) {
            if (Looper.myLooper() != Looper.getMainLooper()) {
                throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
            } else if (sSettings == null) {
                throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
            } else if (!sInitialized) {
                try {
                    if (sResourceExtractor != null) {
                        sResourceExtractor.waitForCompletion();
                    }
                    List<String> shellArgs = new ArrayList();
                    shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");
                    ApplicationInfo applicationInfo = getApplicationInfo(applicationContext);
                    shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + "libflutter.so");
                    if (args != null) {
                        Collections.addAll(shellArgs, args);
                    }

                    String kernelPath = null;
                    shellArgs.add("--aot-shared-library-name=" + sAotSharedLibraryName);
                    
                    File dir = applicationContext.getDir("libs", Activity.MODE_PRIVATE);
                    String libPath =  dir.getAbsolutePath() + File.separator + "libapp_fix.so";
                
                    shellArgs.add("--aot-shared-library-name=" + libPath);
                    shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext));
                    if (sSettings.getLogTag() != null) {
                        shellArgs.add("--log-tag=" + sSettings.getLogTag());
                    }

                    String appStoragePath = PathUtils.getFilesDir(applicationContext);
                    String engineCachesPath = PathUtils.getCacheDirectory(applicationContext);
                    FlutterJNI.nativeInit(applicationContext, (String[])shellArgs.toArray(new String[0]), (String)kernelPath, appStoragePath, engineCachesPath);
                    sInitialized = true;
                } catch (Exception var7) {
                    throw new RuntimeException(var7);
                }
            }
        }
    }
複製代碼

這裏的路徑和名稱須要對應上,我已將修復後的 so 重命名爲 libapp_fix.so ,並經過

shellArgs.add("--aot-shared-library-name=" + sAotSharedLibraryName);
複製代碼

這行代碼傳遞給底層。 同時,so 庫路徑經過以下代碼傳遞:

File dir = applicationContext.getDir("libs", Activity.MODE_PRIVATE);
                    String libPath =  dir.getAbsolutePath() + File.separator + "libapp_fix.so";

                    shellArgs.add("--aot-shared-library-name=" + libPath);
複製代碼

至此,咱們修改了代碼,讓程序初始化的時候,加載咱們修改過的資源文件了。

4、測試

修復步驟:

一、打 release 包,拿到 libapp.so,重命名爲 libapp_fix.so

因爲上面的代碼已經修改成加載私有目錄下的 libapp_fix.so ,若是 app 直接運行確定是不行的,所以咱們須要先打一個 release 包,解壓拿到裏面的 libapp.so ,並修改成 libapp_fix.so,而後放到手機根目錄下,這樣程序啓動後,會把這個文件複製到私有目錄。

這裏注意一下,打 release 包須要配置一下簽名文件 。

代碼就是初始化項目的代碼,修改成點擊按鈕,數字加2 :

二、安裝並運行 app

效果以下:

三、修改代碼,從新打包

修改代碼以下 :

一樣,解壓 apk,重命名 libapp.so 爲 libapp_fix.so,放到手機根目錄下。

四、重啓應用,完成修復

先殺掉進程,重啓應用,查看效果:

能夠看到,已經完成了修復。

github

最後

歡迎關注「Flutter 編程開發」微信公衆號 。

相關文章
相關標籤/搜索