Flutter动态化-Android(一)

Flutter动态化-Android(一)

上篇我们讲了flutter engine编译环境搭建,这篇我们正式来看下如何修改flutter engine源码,实现动态化。

本篇文章将分为两部分来讲,本篇讲述有哪些核心代码需要修改的,如何编译engine源码,以及在flutter应用中该如何使用;下一篇会讲解如何打包编译成aar,以方便混合开发时使用。

一、flutter engine核心代码修改

思路:分析libapp.so和flutter_assets的加载过程,在加载之前修改源码替换需要成自己的路径

1.1、FlutterLoaderensureInitializationComplete

从前面的文章我们可以知道,这个方法主要是构建shellArgs列表,在C层代码中会调用此配置初始化参数

先看下面这段代码:

if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
        String snapshotAssetPath =
                PathUtils.getDataDirectory(applicationContext) + File.separator + flutterAssetsDir;
    kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB;
    shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
    shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData);
    shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData);
}

在debug和JIT模式时会配置flutter_assetsvm_snapshot_dataisolate_snapshot_data的位置,我们知道在libapp.so文件的本质是相当于vm_snapshot_dataisolate_snapshot_data的打包合集,从这里我们可以找到想到将libapp.soflutter_assets文件放到debug或者JIT模式时的文件路径下,也就是PathUtils.getDataDirectory(applicationContext)目录下。

我们再来看如何修改这个代码对应的else部分的代码:

// 查看/user/0/package/app_flutter目录是否存在libapp.so文件,如果存在就传递这个新的路径,否则就还使用默认路径(也就是lib/arm/或者lib/arm64/)的libapp.so文件
File appFile = new File(PathUtils.getDataDirectory(applicationContext) + File.separator + aotSharedLibraryName);
String aotSharedLibraryPath = applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName;
if(appFile.exists()){
  aotSharedLibraryPath = appFile.getPath();
}
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryPath);

如果不需要动态替换flutter_assets文件,其实上面就修改就足够动态替换libapp.so了。还有一种不用修改engine源码的方法就是在继承FlutterActivity时重写getFlutterShellArgs方法,把AOT_SHARED_LIBRARY_NAME传递成自定义的路径,亲测有效,小伙伴们自行尝试,有问题可以在留言区交流。

附上AOT_SHARED_LIBRARY_NAME在C层使用的代码

// 代码位置shell-->common-->switches.cc
if (aot_shared_library_name.size() > 0) {
  // 循环会导致最后一个shellArgs中AOT_SHARED_LIBRARY_NAME生效,不用担心设置了多个AOT_SHARED_LIBRARY_NAME
  for (std::string_view name : aot_shared_library_name) {
    settings.application_library_path.emplace_back(name);
  }
}

1.2、FlutterJNInativeAttach

这是一个调用libflutter.so的jni方法,通过这个方法,我们可以传递一个路径到C层,在C层的初始化AndroidShellHolder之前将自定义的路径配置进去

修改后的nativeAttach方法体以及相关代码,如下

private native long nativeAttach(@NonNull FlutterJNI flutterJNI, String dynamicPath, boolean isBackgroundView);

 public void attachToNative(String dynamicPath, boolean isBackgroundView) {
    ensureRunningOnMainThread();
    ensureNotAttachedToNative();
        // 因为当前类中无法获得Contenxt,所以需要FlutterNativeView和FlutterEngine调用attachToNative方法时传入路径   
    nativePlatformViewId = nativeAttach(this, dynamicPath, isBackgroundView);
 }

1.3、platform_view_android_jni.cc修改

// 1. jni映射方法新增参数类型
bool RegisterApi(JNIEnv* env) {
  static const JNINativeMethod flutter_jni_methods[] = {
      // Start of methods from FlutterJNI
      {
          .name = "nativeAttach",
            // 新增一个String参数类型
          .signature = "(Lio/flutter/embedding/engine/FlutterJNI;Ljava/lang/String;Z)J",
          .fnPtr = reinterpret_cast<void*>(&AttachJNI),
      },
    。。。

// 2. AttachJNI方法修改
static jlong AttachJNI(JNIEnv* env,
                       jclass clazz,
                       jobject flutterJNI,
                       jstring dynamicPath,
                       jboolean is_background_view) {
  fml::jni::JavaObjectWeakGlobalRef java_object(env, flutterJNI);
  const auto dynamic_path = fml::jni::JavaStringToString(env, dynamicPath);
  // 获取配置
  Settings settings = FlutterMain::Get().GetSettings();
  if(dynamic_path.size() > 0) {
      settings.application_library_path.clear();
        // 再AndroidShellHolder初始化前设置新路径
      settings.application_library_path.emplace_back(dynamic_path + "/libapp.so");
      settings.assets_path = dynamic_path + "/flutter_assets";
  }

  FML_LOG(INFO) << "settings.assets_path:" << settings.assets_path;
    
  // 将修改后的settings传递进去
  auto shell_holder = std::make_unique<AndroidShellHolder>(
      settings, java_object, is_background_view);
  if (shell_holder->IsValid()) {
    return reinterpret_cast<jlong>(shell_holder.release());
  } else {
    return 0;
  }
}

1.4、FlutterNativeView中相关修改的代码

private void attach(FlutterNativeView view, boolean isBackgroundView) {
    mFlutterJNI.attachToNative(PathUtils.getDynamicPath(mContext), isBackgroundView);
    dartExecutor.onAttachedToJNI();
}

1.5、FlutterEngine中相关修改的代码

// 1. 构造函数中
attachToJni(context);

// 2. attachToJni方法修改
private void attachToJni(Context context) {
        Log.v(TAG, "Attaching to JNI.");
    // TODO(mattcarroll): update native call to not take in "isBackgroundView"
    flutterJNI.attachToNative(PathUtils.getDynamicPath(context), false);

    if (!isAttachedToJni()) {
      throw new RuntimeException("FlutterEngine failed to attach to its native Object reference.");
    }
}

1.6、PathUtils新增getDynamicPath方法

// 获取动态化资源文件路径
public static String getDynamicPath(Context applicationContext){
    String packagePath = getDataDirectory(applicationContext);
    String aotLibFile = packagePath + File.separator + FlutterLoader.DEFAULT_AOT_SHARED_LIBRARY_NAME;
    String flutterAssetsPath = packagePath + File.separator + FlutterLoader.DEFAULT_FLUTTER_ASSETS_DIR;
    File aotFile = new File(aotLibFile);
    File flutterAssetsFile = new File(flutterAssetsPath);
    if (!aotFile.exists() && !flutterAssetsFile.exists()) {
      packagePath = "";
    }
    return packagePath;
}

到此,动态化所需要的方法基本都修改完了,具体代码请看https://github.com/panmin/engine/tree/feature_my_engine,欢迎starwatch,代码会不定期优化更新。

二、编译本地engine

修改完engine的代码,这一小节我们就来看看如何将修改后的engine编译成flutter.jarlibflutter.so

2.1、编译相关基础知识

  • CPU架构

    编译结果包括armarm64x86这几种架构,arm对应Android的armeabi-v7a,arm64对应Android的arm64-v8a,x86还是x86一般是模拟器上用的。

  • 是否优化

    未优化的engine包是可以添加打印出C层的代码的,engine的C++里用FML_LOG(INFO)打印log;优化后的包体积也更小。

  • 运行模式

    根据flutter的模式是分为debugprofilerelease这三种模式的。

2.2、常用的编译参数

  • --android-cpu: CPU架构,对应armarm64x86,如:gn --android-cpu arm
  • --runtime-mode: 运行模式,对应debugprofilerelease,如:gn --runtime-mode debug
  • --unoptiimized: 是否优化,带上这个参数就说明是不优化的情况

2.3、编译开始

# 1、定位到`engine/src`目录
cd engine/src
# 2、编译Android对应平台已优化的release代码,这里大家根据自己的实际使用情况,合理的使用2.2中提到的编译参数
./flutter/tools/gn --android --runtime-mode release --android-cpu arm
# 通过2中的命令会在src目录下生成一个out/android_release的目录
# 3、编译2中生成的代码成为flutter.jar和libflutter.so,这一步就最耗时的,有快有慢,看电脑性能了
ninja -C out/android_release
# 如果2中使用的CPU架构是arm64时,3中的这一步就要用android_release_arm64文件夹了
# 4、编译Android打包时需要的代码
./flutter/tools/gn --runtime-mode release --android-cpu arm
# 5、同样编译一下
ninja -C out/host_android
# 如果4中使用的是arm64,这里就需要用host_android_arm64文件夹了

通过上的编译,我们可以看到out/android_release文件夹中已经生成了flutter.jar和里面已经包含了libflutter.so

三、使用本地engine

这一小节只讲解纯flutter项目时如何使用;至于混合开发的项目中如何使用,因为牵扯到一些gradle脚本的修改,我会单独抽出一篇文章来讲。

3.1、使用本地engine打包apk

# 打包arm平台的apk
flutter build apk --target-plarform android-arm --split-per-abi --local-engine-src engine/src --local-engine=android-release
# 打包arm64平台的apk
flutter build apk --target-plarform android-arm --split-per-abi --local-engine-src engine/src --local-engine=android-release_arm64

3.2、修改代码后如何查看新的libapp.soflutter_assets文件

对于已经安装完使用本地engine打包的apk的手机来说,想动态更新新的代码,需要找到修改后代码打包生成的libapp.soflutter_assets文件,这个文件怎么生成和找到的呢?

  1. 修改dart代码
  2. 使用3.1中的命令打包apk
  3. 在跟lib同级别的目录build/app/intermediates/flutter/release下找到对应CPU架构的app.so文件,将其改名成libapp.so,然后在app启动时复制到PathUtils.getDataDirectory(applicationContext)对应的目录下,也就是user/0/package/app_flutter目录下;把build/app/intermediates/flutter/release目录下的flutter_assets也复制到这个目录下。
  4. 待下次重启app时即可生效

四、总结

本篇文章讲解了flutter engine代码实现动态化的代码修改,以及编译和使用本地的engine,下一篇我会详细讲解在混合开发时使用本地engine,以及如何修改bulid aar时的脚本,打包成aar供业务方使用,也是伸手党们的福利,欢迎大家关注和点赞。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容