在上一篇 文章中,簡單分析了一下 Flutter 在 Android 端的啓動流程,雖然沒有更深刻的分析,可是咱們能夠了解到,對於 Flutter 端的 Dart VM 的啓動等,是經過 Android 傳遞的資源(或者說路徑)過去,Dart VM 加載這些資源完成初始化的,那麼咱們能夠經過動態替換資源就能夠達到熱更新的目的。git
注意:github
本次測試的開發環境:shell
經過以前文章的分析,能夠知道,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);
}
複製代碼
複製文件等操做都須要讀寫權限,不要忘了。網絡
在以前分析啓動流程的文章中,提到過,MainActivity 繼承自 FlutterActivity,而 FlutterActivity 只是一個代理類,真正的操做都是在 FlutterActivityDelegate 這個類中進行的,而在 FlutterActivityDelegate 中會調用 FlutterMain 中的方法進行 Dart VM 等的初始化。 所以咱們要作的就是,修改 FlutterActivity 和 FlutterActivityDelegate 這兩個類,以達到修改 FlutterMain 的目的。這裏爲了方便,只是簡單的複製了一份代碼,將 FlutterActivity 改成 HotFixFlutterActivity,FlutterActivityDelegate 改成 HotFixFlutterActivityDelegate ,而後修改裏面的代碼,固然還有其餘的方法,這裏不在演示。app
public class MainActivity extends HotFixFlutterActivity implements EasyPermissions.PermissionCallbacks
複製代碼
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;
}
...
}
複製代碼
代碼修改到這裏,當程序運行後,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 而後修改這裏的代碼。
這裏主要是修改 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);
複製代碼
至此,咱們修改了代碼,讓程序初始化的時候,加載咱們修改過的資源文件了。
修復步驟:
因爲上面的代碼已經修改成加載私有目錄下的 libapp_fix.so ,若是 app 直接運行確定是不行的,所以咱們須要先打一個 release 包,解壓拿到裏面的 libapp.so ,並修改成 libapp_fix.so,而後放到手機根目錄下,這樣程序啓動後,會把這個文件複製到私有目錄。
這裏注意一下,打 release 包須要配置一下簽名文件 。
代碼就是初始化項目的代碼,修改成點擊按鈕,數字加2 :
效果以下:
修改代碼以下 :
一樣,解壓 apk,重命名 libapp.so 爲 libapp_fix.so,放到手機根目錄下。
先殺掉進程,重啓應用,查看效果:
能夠看到,已經完成了修復。
歡迎關注「Flutter 編程開發」微信公衆號 。