Flutter 動態下發更新(Android 端)

本人之前基於 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()
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章