在本次 文章中甲锡,簡單分析了一下 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ù)。