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

在本次 文章中甲锡,簡單分析了一下 Flutter 在 Android 端的啟動流程,雖然沒有更深入的分析,但是我們可以了解到冗恨,對于 Flutter 端的 Dart VM 的啟動等尽棕,是通過 Android 傳遞的資源(或者說路徑)過去喳挑,Dart VM 加載這些資源完成初始化的,那么我們可以通過動態(tài)替換資源就可以達到熱更新的目的滔悉。

注意:

  • 不同版本的 Flutter 代碼與邏輯可能有所不同伊诵,但整體流程大同小異。
  • 同樣的回官,不同版本 Flutter 編譯之后的產(chǎn)物不同曹宴,
  • Release 模式 和 Debug 模式下的編譯產(chǎn)物不同,這里以 Release 為例歉提,代碼也是 Release 版本的代碼笛坦。

本次測試的開發(fā)環(huán)境:

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

一、資源復(fù)制

通過之前文章的分析苔巨,可以知道版扩,F(xiàn)lutterMain 這個類中,會傳遞指定資源路徑恋拷,提供給 Dart VM 進行初始化资厉。

這里面有兩個重要的資源,一個是 libflutter.so 蔬顾,一個是 libapp.so宴偿。 通過名字就可以看出來,libflutter.so 是框架相關(guān)的庫诀豁,而 libapp.so 就是我們寫的代碼編譯成的 so 庫窄刘,我們就是要通過動態(tài)替換這個文件,達到熱更新的目的舷胜。

為了能夠讓 Dart VM 加載我們修改之后的 so 庫娩践,我們肯定需要將修改后的 so 庫放到 app 的私有目錄下。這里直接從手機根目錄下獲取烹骨,當(dāng)然從網(wǎng)絡(luò)下載等都是同樣的道理翻伺。 先定義一個輔助類,將文件復(fù)制到手機私有目錄下沮焕。

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 "";
    }

}

在程序啟動的時候吨岭,我們調(diào)用這個方法,將文件復(fù)制過去峦树,也就是在 MainActivity 的 onCreate 方法中辣辫。


  @Override
  protected void onCreate(Bundle savedInstanceState) {

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

復(fù)制文件等操作都需要讀寫權(quán)限旦事,不要忘了。

二急灭、自定義 FlutterActivity 和 FlutterActivityDelegat

MainActivity 繼承自 FlutterActivity姐浮,而 FlutterActivity 只是一個代理類,真正的操作都是在 FlutterActivityDelegate 這個類中進行的葬馋,而在 FlutterActivityDelegate 中會調(diào)用 FlutterMain 中的方法進行 Dart VM 等的初始化卖鲤。 因此我們要做的就是,修改 FlutterActivity 和 FlutterActivityDelegate 這兩個類畴嘶,以達到修改 FlutterMain 的目的扫尖。這里為了方便,只是簡單的復(fù)制了一份代碼掠廓,將 FlutterActivity 改為 HotFixFlutterActivity,F(xiàn)lutterActivityDelegate 改為 HotFixFlutterActivityDelegate 甩恼,然后修改里面的代碼蟀瞧,當(dāng)然還有其他的方法,這里不在演示条摸。

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

代碼修改到這里切端,當(dāng)程序運行后,MainActivity 的 onCreate 方法里面會執(zhí)行到 HotFixFlutterActivityDelegate 的 onCreate 方法中顷啼,而在這里踏枣,會調(diào)用 FlutterMain 里面的方法進行初始化操作,因此我們還需要修改 onCreate 這個方法钙蒙。

onCreate 中默認調(diào)用的代碼如下:

FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);

我們肯定需要自己定義一個類似的文件茵瀑,修改里面的方法,來提供我們調(diào)用達到替換資源的目的躬厌。比如我們定義的類似的類叫 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 才會被初始化,正常情況下疙渣,F(xiàn)lutterMain.startInitialization 這個方法是在 Application 的 onCreate 中調(diào)用的:

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;
    }
}


因為我們沒有修改這里的代碼匙奴,所以我們要自己初始化一下,當(dāng)然也可以自己在定義一個 Application 然后修改這里的代碼昌阿。

三饥脑、加載自己的 so

這里主要是修改 MyFlutterMain 中的 ensureInitializationComplete 方法恳邀,加載我們自己復(fù)制到手機私用目錄下的那個 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);
                }
            }
        }
    }

這里的路徑和名稱需要對應(yīng)上灶轰,我已將修復(fù)后的 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);

至此乳附,我們修改了代碼,讓程序初始化的時候伴澄,加載我們修改過的資源文件了赋除。

四、測試

修復(fù)步驟:

1非凌、打 release 包举农,拿到 libapp.so,重命名為 libapp_fix.so

由于上面的代碼已經(jīng)修改為加載私有目錄下的 libapp_fix.so ,如果 app 直接運行肯定是不行的敞嗡,因此我們需要先打一個 release 包颁糟,解壓拿到里面的 libapp.so ,并修改為 libapp_fix.so喉悴,然后放到手機根目錄下棱貌,這樣程序啟動后,會把這個文件復(fù)制到私有目錄箕肃。

這里注意一下婚脱,打 release 包需要配置一下簽名文件 。

代碼就是初始化項目的代碼勺像,修改為點擊按鈕障贸,數(shù)字加2 :

2、安裝并運行 app

效果如下:

3吟宦、修改代碼惹想,重新打包

修改代碼如下 :

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

4辰狡、重啟應(yīng)用锋叨,完成修復(fù)

先殺掉進程,重啟應(yīng)用宛篇,查看效果:

可以看到娃磺,已經(jīng)完成了修復(fù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末叫倍,一起剝皮案震驚了整個濱河市偷卧,隨后出現(xiàn)的幾起案子豺瘤,更是在濱河造成了極大的恐慌,老刑警劉巖听诸,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坐求,死亡現(xiàn)場離奇詭異,居然都是意外死亡晌梨,警方通過查閱死者的電腦和手機桥嗤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仔蝌,“玉大人泛领,你說我怎么就攤上這事耍群“屯耄” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵基跑,是天一觀的道長瞧挤。 經(jīng)常有香客問我篓像,道長,這世上最難降的妖魔是什么皿伺? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮盒粮,結(jié)果婚禮上鸵鸥,老公的妹妹穿的比我還像新娘。我一直安慰自己丹皱,他們只是感情好妒穴,可當(dāng)我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著摊崭,像睡著了一般讼油。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呢簸,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天矮台,我揣著相機與錄音,去河邊找鬼根时。 笑死瘦赫,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛤迎。 我是一名探鬼主播确虱,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼替裆!你這毒婦竟也來了校辩?” 一聲冷哼從身側(cè)響起窘问,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎宜咒,沒想到半個月后惠赫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡荧呐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年汉形,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倍阐。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡概疆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出峰搪,到底是詐尸還是另有隱情岔冀,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布概耻,位于F島的核電站使套,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鞠柄。R本人自食惡果不足惜侦高,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厌杜。 院中可真熱鬧奉呛,春花似錦、人聲如沸夯尽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匙握。三九已至咆槽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間圈纺,已是汗流浹背秦忿。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蛾娶,地道東北人小渊。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像茫叭,于是被迫代替她去往敵國和親酬屉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,107評論 2 356