美文网首页
[Flutter] 如何替换so文件来动态替换Flutter代码

[Flutter] 如何替换so文件来动态替换Flutter代码

作者: _Jun | 来源:发表于2023-01-17 09:51 被阅读0次

    一、Flutter代码的启动起点

    我们在多数的业务场景下,使用的都是FlutterActivityFlutterFragment。在在背后,我们知道有着FlutterEnigineDartExecutor等等多个部件在支持它们的工作。我们所要探究的,就是,它们是如何启动的,Dart代码是从何而来的,以实现动态替换libapp.so

    以官方的计数器Demo为例,默认的Activity宿主,是实现了FlutterActivity的子类,对于一个Activity,我们最应该关心的就是它的onCreate方法:

    • FlutterActivity# onCreate
    protected void onCreate(@Nullable Bundle savedInstanceState) {
      switchLaunchThemeForNormalTheme();
    
      super.onCreate(savedInstanceState);
    
      delegate = new FlutterActivityAndFragmentDelegate(this);
      delegate.onAttach(this);
      delegate.onRestoreInstanceState(savedInstanceState);
    
      lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    
      configureWindowForTransparency();
      setContentView(createFlutterView());
      configureStatusBarForFullscreenFlutterExperience();
    }
    

    其实过程很简单,FlutterActivity在这里做了一些主题的设置,因为毕竟FlutterActivity也是一个常规的Activity,它就必须按照Android的Activity的一些规范来进行设置。

    第三行代码开始,就创建了一个我们所说的FlutterActivityAndFragmentDelegate对象,FlutterActivity将绝大多数的Flutter初始化相关逻辑委托给了它,而自身则专注于设置主题、窗口、StatusBar等等。

    我们对delegate.onAttach(this);这一行代码的跟踪,最终能走到如下的一个创建流程:

    FlutterActivity->
        FlutterActivityAndFragmentDelegate->
            onAttach()->
                setupFlutterEngine->
                    1.尝试去Cache中获取Engine
                    2.尝试从Host中获取Engine
                    3.都没有的话创建一个新的Engine->
                        Engine #Constructor->
                        1. 会对Assets、DartExecutor、各种Channel、FlutterJNI做处理
                        2. 还会对FlutterLoader做处理->
                            startInitialization方法做初始化
                                -> 1. 必须在主线程初始化Flutter
                                -> 2. 先检查settings变量;
                                -> 3. 获取全局的ApplicationContext防止内存泄漏
                                -> 4. VsyncWaiter对象的初始化
                                -> 5. 最后会生成一个initTask交给线程池去执行
    

    1.1 initTask对象

    initTask是一个Callable对象,和Runnable类似的,我们可以将它理解成一个任务,也就是一段代码,他最终会被交给线程池去执行:

    initResultFuture = Executors.newSingleThreadExecutor().submit(initTask);
    

    initTask的代码如下

     // Use a background thread for initialization tasks that require disk access.
    Callable<InitResult> initTask =
        new Callable<InitResult>() {
          @Override
          public InitResult call() {
             ResourceExtractor resourceExtractor = initResources(appContext); 
    
             flutterJNI.loadLibrary(); 
    
               // Prefetch the default font manager as soon as possible on a background thread.  
              // It helps to reduce time cost of engine setup that blocks the platform thread.  
             Executors.newSingleThreadExecutor() 
                 .execute(
                     new Runnable () { 
                         @Override
                         public void run () { 
                             flutterJNI.prefetchDefaultFontManager(); 
                         } 
                     }
             ); 
    
             if (resourceExtractor != null) { 
                 resourceExtractor.waitForCompletion(); 
             } 
    
             return new InitResult( 
                 PathUtils.getFilesDir(appContext), 
                 PathUtils.getCacheDirectory(appContext), 
                 PathUtils.getDataDirectory(appContext)
             ); 
          }
        };
    

    我们可以抓一下其中的关键字:

    • ResourceExtractor

    • FlutterJNI.loadLibrary

    • FlutterJNI.prefetchDefaultFontManager

    • PathUtils

    不难发现,主要是在做一些资源的预取。

    ResourceExtractor主要是针对在DEBUG或者是JIT模式下,针对安装包内资源的提取逻辑。

    在DEBUG或者JIT模式下,需要提取Assets目录下的资源文件到存储中,Assets本质上还是Zip压缩包的一部分,没有自己的物理路径,所以需要提取,并返回真真实的物理路径。在DEBUG和JIT模式下,FlutterSDK和业务代码将被构建成Kernel格式的二进制文件,Engine将通过文件内存映射的方式进行加载。

    详见:「三、libflutter.so和libapp.so」

    1.2 ResourceExtractor

    libflutter.so和libapp.so

    在DEBUG | JIT模式下,我们是没有libapp.so的,而在release模式下,是有libapp.so文件的,我们分别解包两个不同的Apk文件,可以很清楚地看到这一点:

    我们知道,libflutter.so是存放flutter的一些基础类库的so文件,而libapp.so则是存放我们业务代码的so文件,那如果在DEBUG|JIT模式下,没有libapp.so,那么我们的业务代码存储在哪里呢?

    此时,我们就要看看ResourceExtractor的initResources方法,究竟干了些什么:

     /** Extract assets out of the APK that need to be cached as uncompressed files on disk. */
    private ResourceExtractor initResources(@NonNull Context applicationContext) {
      ResourceExtractor resourceExtractor = null;
      if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
        final String dataDirPath = PathUtils.getDataDirectory(applicationContext);
        final String packageName = applicationContext.getPackageName();
        final PackageManager packageManager = applicationContext.getPackageManager();
        final AssetManager assetManager = applicationContext.getResources().getAssets();
        resourceExtractor =
            new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager);
    
        // In debug/JIT mode these assets will be written to disk and then
        // mapped into memory so they can be provided to the Dart VM.
         resourceExtractor
            .addResource(fullAssetPathFrom(flutterApplicationInfo.vmSnapshotData))
            .addResource(fullAssetPathFrom(flutterApplicationInfo.isolateSnapshotData))
            .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB));
    
        resourceExtractor.start();
      }
      return resourceExtractor;
    }
    

    其中的addResource方法,分别提供了VM的快照数据、iSolate的快照数据DEFAULT_KERNEL_BLOB的数据。因为Flutter本身支持热重载的特性,保存状态和快照(Snapshot)之间必然是不可分割的。

    而DEFAULT_KERNEL_BLOB是一个字符串常量: "kernel_blob.bin",结合前面的内容:

    FlutterSDK和业务代码将被构建成Kernel格式的二进制文件

    我们有理由猜测, "kernel_blob.bin" ,就是我们的业务代码,Flutter是支持逻辑代码热重载的,所以这个字面量加载的资源同样可能会被重新加载。

    这也是为什么,如果我们在State中,新增了某个变量作为Widget的某个状态,在initState中调用了,然后使用热重载之后,会导致State中找不到这个变量,因为initState在初次启动时就被调用过了,后续的热重载只会将之前的Snapshot恢复回来,而不会走initState的逻辑。

    我们可以在app-debug.apk的assets中,找到"kernel_blob.bin"文件,同样也可以找到isolate_snapshot_data、vm_snapshot_data文件,所以ResourceExtractor加载的,基本上都是这个文件夹中的文件。

    image.png

    但是,在非DEBUG|JIT模式下,就不需要通过ResourceExtractor来进行加载了。

    回到initTask方法,只在resourceExtractor != null时,会去等待它的完成。

    ResourceExtractor resourceExtractor = initResources(appContext);
    
    flutterJNI.loadLibrary();
    
    // Prefetch the default font manager as soon as possible on a background thread.
    // It helps to reduce time cost of engine setup that blocks the platform thread.
    Executors.newSingleThreadExecutor()
        .execute(
            new Runnable() {
              @Override
              public void run() {
                flutterJNI.prefetchDefaultFontManager();
              }
            });
    
    if (resourceExtractor != null) {
      resourceExtractor.waitForCompletion();
    }
    

    1.3 FlutterJNI#loadLibrary

    public void loadLibrary() {
      if (FlutterJNI.loadLibraryCalled) {
        Log.w(TAG, "FlutterJNI.loadLibrary called more than once" );
      }
    
      System.loadLibrary( "flutter" );
      FlutterJNI.loadLibraryCalled = true;
    }
    

    代码比较简单,无非就是调用System.loadLibrary去加载Library文件。需要注意的是,表面上找到是flutter,但是在Native(C++)层中,会为它拼接上前缀和后缀:lib和.so,所以,实际上load行为查找的是位于apk包下的lib目录下的对应架构文件夹下的libflutter.so

    initTask任务提交给线程池之后,就相当于startInitialization走完了。

    你会发现有个问题,在Debug模式下,我们加载业务代码是从二进制文件:"kernel_blob.bin"中加载的,而Release模式下,实在libapp.so中加载的,上面已经出现了加载"kernel_blob.bin"和libflutter.so ,那么在release模式下,另一个Library文件:libapp.so是什么时候加载的呢?

    所以,就要进入我们的第二个关键方法:ensureInitializationComplete

    二、ensureInitializationComplete

    实际上,ensureInitializationComplete和startInitialization在FlutterEngine的初始化代码中

    flutterLoader.startInitialization(context.getApplicationContext());
    flutterLoader.ensureInitializationComplete(context, dartVmArgs);
    

    代码一百多行,但是大多都是一些配置性的代码:

    public void ensureInitializationComplete(
        @NonNull Context applicationContext, @Nullable String[] args) {
      if (initialized) {
        return;
      }
      if (Looper.myLooper() != Looper.getMainLooper()) {
        throw new IllegalStateException(
            "ensureInitializationComplete must be called on the main thread" );
      }
      if (settings == null) {
        throw new IllegalStateException(
            "ensureInitializationComplete must be called after startInitialization" );
      }
      try {
        InitResult result = initResultFuture.get();
    
        List<String> shellArgs = new ArrayList<>();
        shellArgs.add( "--icu-symbol-prefix=_binary_icudtl_dat" );
    
        shellArgs.add(
            "--icu-native-lib-path="
    + flutterApplicationInfo.nativeLibraryDir
                + File.separator
                + DEFAULT_LIBRARY);
        if (args != null) {
          Collections.addAll(shellArgs, args);
        }
    
        String kernelPath = null;
        if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
          String snapshotAssetPath =
              result.dataDirPath + File.separator + flutterApplicationInfo.flutterAssetsDir;
          kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB;
          shellArgs.add( "--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
          shellArgs.add( "--" + VM_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.vmSnapshotData);
          shellArgs.add(
              "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.isolateSnapshotData);
        } else {
          shellArgs.add(
              "--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName);
    
          // Most devices can load the AOT shared library based on the library name
    // with no directory path.  Provide a fully qualified path to the library
    // as a workaround for devices where that fails.
    shellArgs.add(
              "--"
    + AOT_SHARED_LIBRARY_NAME
                  + "="
    + flutterApplicationInfo.nativeLibraryDir
                  + File.separator
                  + flutterApplicationInfo.aotSharedLibraryName);
        }
    
        shellArgs.add( "--cache-dir-path=" + result.engineCachesPath);
        if (!flutterApplicationInfo.clearTextPermitted) {
          shellArgs.add( "--disallow-insecure-connections" );
        }
        if (flutterApplicationInfo.domainNetworkPolicy != null) {
          shellArgs.add( "--domain-network-policy=" + flutterApplicationInfo.domainNetworkPolicy);
        }
        if (settings.getLogTag() != null) {
          shellArgs.add( "--log-tag=" + settings.getLogTag());
        }
    
        ApplicationInfo applicationInfo =
            applicationContext
                .getPackageManager()
                .getApplicationInfo(
                    applicationContext.getPackageName(), PackageManager.GET_META_DATA);
        Bundle metaData = applicationInfo.metaData;
        int oldGenHeapSizeMegaBytes =
            metaData != null ? metaData.getInt(OLD_GEN_HEAP_SIZE_META_DATA_KEY) : 0;
        if (oldGenHeapSizeMegaBytes == 0) {
          // default to half of total memory.
    ActivityManager activityManager =
              (ActivityManager) applicationContext.getSystemService(Context.ACTIVITY_SERVICE);
          ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
          activityManager.getMemoryInfo(memInfo);
          oldGenHeapSizeMegaBytes = (int) (memInfo.totalMem / 1e6 / 2);
        }
    
        shellArgs.add( "--old-gen-heap-size=" + oldGenHeapSizeMegaBytes);
    
        if (metaData != null && metaData.getBoolean(ENABLE_SKPARAGRAPH_META_DATA_KEY)) {
          shellArgs.add( "--enable-skparagraph" );
        }
    
        long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
    
        flutterJNI.init(
            applicationContext,
            shellArgs.toArray(new String[0]),
            kernelPath,
            result.appStoragePath,
            result.engineCachesPath,
            initTimeMillis);
    
        initialized = true;
      } catch (Exception e) {
        Log.e(TAG, "Flutter initialization failed." , e);
        throw new RuntimeException(e);
      }
    }
    

    显然,ensureInitializationComplete也必须在主线程中进行调用,并且必须在startInitialization之后进行调用。此外,我们要注意另外一个东西:shellArgs。

    2.1 ShellArgs

    Shell是什么大家并不陌生,在计算机中,Shell通常作为系统调用用户操作之间的那么个东西,它存在的形式在Linux/Mac中一般就是一个Shell软件,通常运行在终端当中(你可以粗略地就将Shell 和终端划等号 )。

    所以,Flutter的Shell自然而然地旨在设置Flutter运行的一个「基底」,ShellArgs,则是我们使用这么个「基底」的参数。

    和之前提到的ResourceExtractor在JIT|DEBUG模式下主动去加载VM和Isoalte快照数据类似地,ShellArgs会在DEBUG和JIT模式下,去设置VM快照数据、Isolate快照数据和Kernel的地址。

    别忘了,Kernel即上述的“kernel_blob.bin”二进制文件,是在Debug阶段我们的业务代码,和libapp.so是相对的。

    而在除上述之外的条件下,Flutter设置了一个AOT_SHARED_LIBRARY_NAME的路径:

    shellArgs.add(
        "--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName);
    
    shellArgs.add(
        "--"
    + AOT_SHARED_LIBRARY_NAME
    + "="
    + flutterApplicationInfo.nativeLibraryDir
    + File.separator
    + flutterApplicationInfo.aotSharedLibraryName);
    

    在运行时,这个向shareArgs这个List中添加内容的两个字符串的内容,大致上就是指定了装载在系统的Apk安装包中的so文件的路径。

    --aot-shared-library-name=libapp.so
    --aot-shared-library-name=/data/app/~~RjRJYnLhVBHYW8pHHPeX2g==/com.example.untitled1-wcMTYW1VkfGA2LxW62gUFA==/lib/arm64/libapp.so
    

    因为Tinker本身是支持二进制SO库的动态化的,之前尝试过去动态修改aotSharedLibraryName的值和路径,希望FlutterLoader从该地址去加载libapp.so,以实现Android侧借助Tinker热修复Flutter代码,但是并没有细看源码,打了N个Debug包去测试,结果现在发现这逻辑压根没走。

    除了上述的两个libapp.so的名称和路径之外,在DEBUG | JIT模式下的ShellArgs的全家福大致如下:

    其实你仔细看看,上述的Kernel的Path并没有在这里面,因为它作为参数,传递给了flutterJNI.init函数。

    三、实践:自定义libapp.so的加载

    至此,我们今天最开始的一个话题:Embdder和代码Dart代码从何而来, 便有了结果 。结合上述的内容,我们可以做一个小小的实践,我们通过传入ShellArgs,来加载指定的 libapp.so 文件。

    回到我们最初的流程:

    FlutterActivity->
        FlutterActivityAndFragmentDelegate->
            onAttach()->
                setupFlutterEngine->
                    ……
                    startInitialization
                    ensureInitializationComplete // alpha
    

    我们需要在上述的过程的alpha之前,完成对AOT_SHARED_LIBRARY_NAME 对应的路径(一模一样,也是 AOT_SHARED_LIBRARY_NAME )这两个字符串的内容替换,比如:

    --aot-shared-library-name=libapp.so
    --aot-shared-library-name= /data/app/~~RjRJYnLhVBHYW8pHHPeX2g==/com.example.untitled1-wcMTYW1VkfGA2LxW62gUFA==/lib/arm64/libapp.so
    

    我们希望替换成:

    --aot-shared-library-name=libfixedapp.so
    --aot-shared-library-name= /temp/lib/arm64/libfixedapp.so
    

    3.1 flutterApplicationInfo和FlutterActivity#getShellArgs()

    这是FlutterLoader的一个实例对象,它在startInitialization阶段被赋值:

    public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
      // ……
      try {
     final Context appContext = applicationContext.getApplicationContext();
        // ……
        flutterApplicationInfo = ApplicationInfoLoader.load(appContext);
    
        ……
    

    所以,我们只需要在合适的时机去修改这个值即可。

    但是并没有合适的时机,因为Flutter并没有为我们提供可以侵入去反射设置它的时机,如果在startInitialization,我们唯一可以侵入的时机是attach()函数,但是会让我们反射设置的值被覆盖掉。

    但是,我们关注一下,在setupFlutterEngine时,我们new FlutterEngine的参数:

    flutterEngine =
        new FlutterEngine(
            host.getContext(),
            host.getFlutterShellArgs().toArray(),
            /*automaticallyRegisterPlugins=*/ false,
            /*willProvideRestorationData=*/ host.shouldRestoreAndSaveState());
    

    此处的host,就是我们的FlutterActivity,因为FlutterActivity本身就是FlutterActivityAndFragmentDelegate.Host接口的实现类,而这个host.getFlutterShellArgs().toArray(),最终会作为我们在FlutterActivity预设的参数,在所其他系统预设参数被加入之前被加入到我们的shellArgs数组中。

    所以,我们在FlutterActivity的子类,也就是MainActivity下,重写getFlutterShellArgs()方法:

    class MainActivity: FlutterActivity() {
        override fun getFlutterShellArgs(): FlutterShellArgs {
            return super.getFlutterShellArgs().apply {
    this.add( "--aot-shared-library-name=libfixedapp.so" )
                this.add( "--aot-shared-library-name=/data/data/com.example.untitled1/libfixedapp.so" )
            }
    }
    }
    

    我们可以在debug模式下debug,看看有没有效果:

    显然,是有效果的。

    因为只能从几个特定的目录中去加载so库文件,我们必须将补丁SO文件放在/data/data/com.example.untitled1对应的目录之下。

    接下来,我们先写一个有bug的Flutter代码,我们把标题改成:This is Counter Title with bug 并且新增一个 _decrementCounter() 并把计数器的加法按钮对应的增加按钮,改成减少调用。

    然后在Flutter项目根目录使用安装Release包:

    flutter build apk --release
    adb install build/app/outputs/flutter-apk/app-release.apk
    

    然后我们修复Bug,将代码恢复到最开始的默认状态,然后:

    flutter build apk --release
    open build/app/outputs/flutter-apk/
    

    解压apk,然后把对应的so文件移出来,放到对应的文件夹下: /data/data/com.example.untitled1/libfixedapp.so 。完成之后,重新启动程序,即可从新的、我们指定的路径加载新的 libapp.so 了:

    作者:开中断
    链接:https://juejin.cn/post/7189533148022046778

    相关文章

      网友评论

          本文标题:[Flutter] 如何替换so文件来动态替换Flutter代码

          本文链接:https://www.haomeiwen.com/subject/cxvthdtx.html