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

Flutter程式設計開發發表於2019-10-31

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

注意:

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

本次測試的開發環境:

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

一、資源複製

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

Flutter Android 端熱修復(熱更新)實踐
這裡面有兩個重要的資源,一個是 libflutter.so ,一個是 libapp.so。 通過名字就可以看出來,libflutter.so 是框架相關的庫,而 libapp.so 就是我們寫的程式碼編譯成的 so 庫,我們就是要通過動態替換這個檔案,達到熱更新的目的。

為了能夠讓 Dart VM 載入我們修改之後的 so 庫,我們肯定需要將修改後的 so 庫放到 app 的私有目錄下。這裡直接從手機根目錄下獲取,當然從網路下載等都是同樣的道理。 先定義一個輔助類,將檔案複製到手機私有目錄下。

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);
  }
複製程式碼

複製檔案等操作都需要讀寫許可權,不要忘了。

二、自定義 FlutterActivity 和 FlutterActivityDelegat

在之前分析啟動流程的文章中,提到過,MainActivity 繼承自 FlutterActivity,而 FlutterActivity 只是一個代理類,真正的操作都是在 FlutterActivityDelegate 這個類中進行的,而在 FlutterActivityDelegate 中會呼叫 FlutterMain 中的方法進行 Dart VM 等的初始化。 因此我們要做的就是,修改 FlutterActivity 和 FlutterActivityDelegate 這兩個類,以達到修改 FlutterMain 的目的。這裡為了方便,只是簡單的複製了一份程式碼,將 FlutterActivity 改為 HotFixFlutterActivity,FlutterActivityDelegate 改為 HotFixFlutterActivityDelegate ,然後修改裡面的程式碼,當然還有其他的方法,這裡不在演示。

1、修改 MainActivity 為繼承自我們自己的 HotFixFlutterActivity
public class MainActivity extends HotFixFlutterActivity implements EasyPermissions.PermissionCallbacks
複製程式碼
2、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;
    }
    ...
    }
複製程式碼
3、修改 HotFixFlutterActivityDelegate

程式碼修改到這裡,當程式執行後,MainActivity 的 onCreate 方法裡面會執行到 HotFixFlutterActivityDelegate 的 onCreate 方法中,而在這裡,會呼叫 FlutterMain 裡面的方法進行初始化操作,因此我們還需要修改 onCreate 這個方法。

onCreate 中預設呼叫的程式碼如下:

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 然後修改這裡的程式碼。

三、載入自己的 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);
複製程式碼

至此,我們修改了程式碼,讓程式初始化的時候,載入我們修改過的資原始檔了。

四、測試

修復步驟:

1、打 release 包,拿到 libapp.so,重新命名為 libapp_fix.so

由於上面的程式碼已經修改為載入私有目錄下的 libapp_fix.so ,如果 app 直接執行肯定是不行的,因此我們需要先打一個 release 包,解壓拿到裡面的 libapp.so ,並修改為 libapp_fix.so,然後放到手機根目錄下,這樣程式啟動後,會把這個檔案複製到私有目錄。

這裡注意一下,打 release 包需要配置一下簽名檔案 。

程式碼就是初始化專案的程式碼,修改為點選按鈕,數字加2 :

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

2、安裝並執行 app

效果如下:

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

3、修改程式碼,重新打包

修改程式碼如下 :

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

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

4、重啟應用,完成修復

先殺掉程式,重啟應用,檢視效果:

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

可以看到,已經完成了修復。

github

最後

歡迎關注「Flutter 程式設計開發」微信公眾號 。

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

相關文章