本人之前基於 Flutter SDK 1.2.0版本,做過一次動態更新。
Flutter混合開發,熱修復(Android端)
隨着 Google 對 Flutter 的維護,Flutter 變得越來越好了。因爲官方對Flutter 不做動態更新的計劃了,Flutter Release 版本的加載方式有了變化。
這裏針對 Flutter SDK 1.12.13+hotfix.8 版本又做了修改;
主要做了如下修改:
1,覆蓋修改 flutter.jar 裏面的 FlutterLoader 類
2,爲了兼容 FlutterView 的顯示,覆蓋修改 FlutterActivityAndFragmentDelegate,FlutterView 兩個類;
3,因爲 SO 加載涉及到64位問題,所以建議 SO 只放 armeabi-v7a,
並且爲了防止客戶端沒有其他so,工程內放了一個空的32位SO(libnull.so)
原理介紹:
1,在這個版本,Flutter Release 產物,只有 libflutter.so,libapp.so,flutterAssets。
libflutter.so:是 Flutter 底層 JNI 的庫;
libapp.so:是 dart 代碼的產物;
flutterAssets:是圖片字體等資源文件;
2,主要就是動態指定這3個產物的路徑;
先看下 Flutter Engine 本身的源碼:
public class FlutterLoader {
public void startInitialization(Context applicationContext, Settings settings) {
......
// 加載 libflutter.so
System.loadLibrary("flutter");
......
}
}
public class FlutterLoader {
public void ensureInitializationComplete(Context applicationContext, String[] args) {
......
// 指定 libapp.so 路徑
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
......
}
}
// FlutterAssets 的指定,先看 dart 層怎麼獲取資源的。
// package:flutter/src/painting/image_resolution.dart
// AssetImage -> AssetBundleImageProvider -> PlatformAssetBundle
class PlatformAssetBundle extends CachingAssetBundle {
@override
Future<ByteData> load(String key) async {
// 通過通信 “flutter/assets”,獲取資源的
final ByteData asset = await defaultBinaryMessenger.send('flutter/assets', encoded.buffer.asByteData());
......
return asset;
}
}
// 在 c 層找到這個消息處理
// flutter/shell/common/engine.cc
static constexpr char kAssetChannel[] = "flutter/assets";
void Engine::HandlePlatformMessage(fml::RefPtr<PlatformMessage> message) {
if (message->channel() == kAssetChannel) {
HandleAssetPlatformMessage(std::move(message));
}
}
void Engine::HandleAssetPlatformMessage(fml::RefPtr<PlatformMessage> message) {
......
// asset_manager_ 實際是 flutter::AssetManager
if (asset_manager_) {
std::unique_ptr<fml::Mapping> asset_mapping = asset_manager_->GetAsMapping(asset_name);
}
......
}
// engine/shell/platform/android/platform_view_android_jni.cc
static void RunBundleAndSnapshotFromLibrary(JNIEnv*env, jobject jcaller, jlong shell_holder, jstring jBundlePath, jstring jEntrypoint, jstring jLibraryUrl, jobject jAssetManager) {
// 創建 flutter::AssetManager
auto asset_manager = std::make_shared < flutter::AssetManager > ();
// jAssetManager 就是 android.content.res.AssetManager
asset_manager -> PushBack(std::make_unique < flutter::APKAssetProvider > ( env, jAssetManager, fml::jni::JavaStringToString (env, jBundlePath)) );
// 賦值到 RunConfiguration
RunConfiguration config(std::move(isolate_configuration), std::move(asset_manager));
......
}
// flutter/shell/common/run_configuration.cc
RunConfiguration::RunConfiguration(std::unique_ptr<IsolateConfiguration> configuration, std::shared_ptr<AssetManager> asset_manager) : isolate_configuration_(std::move(configuration)), asset_manager_(std::move(asset_manager)) {
// 全局變量保存了 asset_manager_
PersistentCache::SetAssetManager(asset_manager_);
}
// c 層的 RunBundleAndSnapshotFromLibrary 在 java 層怎麼調用的
// io.flutter.embedding.engine.dart.DartExecutor
public class DartExecutor implements BinaryMessenger {
public void executeDartEntrypoint(@NonNull DartEntrypoint dartEntrypoint) {
......
// 這裏的 assetManager 是通過 FlutterEngine 類裏面 context.getAssets()得到的
flutterJNI.runBundleAndSnapshotFromLibrary(dartEntrypoint.pathToBundle,
dartEntrypoint.dartEntrypointFunctionName,
null, assetManager);
......
}
}
從上面的源碼,可以分析得出:
1,libflutter.so 可以通過 System.load(dataFlutterSoPath); 指定自定義路徑;
2,libapp.so 可以通過 shellArgs 添加自定義路徑:
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + FlutterLoaderHelper.getAppSoDataPath(applicationContext));
3,FlutterAssets 實際是從 AssetManager 獲取的。那麼可以通過反射 addAssetPath 注入資源;這塊瞭解插件化或者動態換膚的同學,應該很清楚怎麼做:
public static void addFlutterAssets(Context context) {
AssetManager assetManager = null;
try {
String apkPath = FlutterPathUtils.getDartCodeDir() + File.separator + FlutterPathUtils.FLUTTER_ASSETS_APK;
if (!FlutterUpdateMgr.getInstance().hasInnerSo() && FileUtils.pathFileExist(apkPath)) {
assetManager = context.getAssets();
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, apkPath);
}
} catch (Exception e) {
e.printStackTrace();
}
}
注意:Android 7.0 之後 ContextThemeWrapper.getAssets() 有變化,所以創建 FlutterEngine ,建議用 ApplicationContext。
最後,怎麼覆蓋 flutter.jar 裏面的類:
1,自己編譯 Flutter Engine 的源碼,修改後打 jar;這種很麻煩,也不方便別人使用。
2,通過 gralde 的 Transform 處理:
------ 主工程 app/build.gradle,android 裏面添加:
registerTransform(new FlutterExcludeClassTransform([
'io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.class',
'io/flutter/embedding/android/FlutterActivityAndFragmentDelegate$1.class',
'io/flutter/embedding/android/FlutterActivityAndFragmentDelegate$2.class',
'io/flutter/embedding/android/FlutterActivityAndFragmentDelegate$Host.class',
'io/flutter/embedding/android/FlutterView.class',
'io/flutter/embedding/android/FlutterView$1.class',
'io/flutter/embedding/android/FlutterView$2.class',
'io/flutter/embedding/android/FlutterView$3.class',
'io/flutter/embedding/android/FlutterView$4.class',
'io/flutter/embedding/android/FlutterView$FlutterEngineAttachmentListener.class',
'io/flutter/embedding/android/FlutterView$TransparencyMode.class',
'io/flutter/embedding/android/FlutterView$RenderMode.class',
'io/flutter/embedding/engine/loader/FlutterLoader.class',
'io/flutter/embedding/engine/loader/FlutterLoader$Settings.class',
'io/flutter/embedding/engine/loader/FlutterLoader$1$1.class',
'io/flutter/embedding/engine/loader/FlutterLoader$1.class'
]))
------ FlutterExcludeClassTransform 類,放到 app/build.gradle 最後:
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import java.security.MessageDigest
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
class FlutterExcludeClassTransform extends Transform {
List<String> mExcludeList
FlutterExcludeClassTransform(List<String> list) {
mExcludeList = list
}
@Override
String getName() {
return "FlutterExcludeClass"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
super.transform(invocation)
invocation.inputs.each { input ->
input.directoryInputs.each { dirInput ->
println("dir = " + dirInput.file.getAbsolutePath())
def dest = invocation.outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(dirInput.file, dest)
}
input.jarInputs.each { jarInput ->
def jarName = jarInput.name
def jarPath = jarInput.file.getAbsolutePath()
def isFlutterJar = jarPath.contains("io.flutter")
println("jar = " + jarPath)
def md5Name = MessageDigest.getInstance("MD5").digest(jarInput.file.readBytes()).encodeHex().toString()
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = invocation.outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
JarFile original = new JarFile(jarInput.file.getPath())
JarOutputStream jos = new JarOutputStream(new FileOutputStream(dest))
def entries = original.entries()
JarEntry entry
while (entries.hasMoreElements()) {
entry = entries.nextElement()
if (entry.name in mExcludeList && isFlutterJar) {
println("exclude jar entry = " + entry.name)
continue
}
jos.putNextEntry(entry)
if (entry.size > 0) {
def inputSteam = original.getInputStream(entry)
def bytes = new byte[8192]
def readLen
while ((readLen = inputSteam.read(bytes)) > 0) {
jos.write(bytes, 0, readLen)
}
}
jos.closeEntry()
}
jos.close()
}
}
}
}