亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

替換so文件來動態(tài)替換Flutter代碼實現(xiàn)詳解

 更新時間:2023年01月18日 09:25:04   作者:開中斷  
這篇文章主要為大家介紹了替換so文件來動態(tài)替換Flutter代碼實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

一、Flutter代碼的啟動起點

我們在多數(shù)的業(yè)務場景下,使用的都是FlutterActivity、FlutterFragment。在在背后,我們知道有著FlutterEnigine、DartExecutor等等多個部件在支持它們的工作。我們所要探究的,就是,它們是如何啟動的,Dart代碼是從何而來的,以實現(xiàn)動態(tài)替換libapp.so。

以官方的計數(shù)器Demo為例,默認的Activity宿主,是實現(xiàn)了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();
}

其實過程很簡單,F(xiàn)lutterActivity在這里做了一些主題的設置,因為畢竟FlutterActivity也是一個常規(guī)的Activity,它就必須按照Android的Activity的一些規(guī)范來進行設置。

第三行代碼開始,就創(chuàng)建了一個我們所說的**FlutterActivityAndFragmentDelegate**對象,F(xiàn)lutterActivity將絕大多數(shù)的Flutter初始化相關邏輯委托給了它,而自身則專注于設置主題、窗口、StatusBar等等。

我們對delegate.onAttach(this);這一行代碼的跟蹤,最終能走到如下的一個創(chuàng)建流程:

FlutterActivity->
    FlutterActivityAndFragmentDelegate->
        onAttach()->
            setupFlutterEngine->
                1.嘗試去Cache中獲取Engine
                2.嘗試從Host中獲取Engine
                3.都沒有的話創(chuàng)建一個新的Engine->
                    Engine #Constructor->
                    1. 會對Assets、DartExecutor、各種Channel、FlutterJNI做處理
                    2. 還會對FlutterLoader做處理->
                        startInitialization方法做初始化
                            -> 1. 必須在主線程初始化Flutter
                            -> 2. 先檢查settings變量;
                            -> 3. 獲取全局的ApplicationContext防止內存泄漏
                            -> 4. VsyncWaiter對象的初始化
                            -> 5. 最后會生成一個initTask交給線程池去執(zhí)行

1.1 initTask對象

initTask是一個Callable對象,和Runnable類似的,我們可以將它理解成一個任務,也就是一段代碼,他最終會被交給線程池去執(zhí)行:

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

不難發(fā)現(xiàn),主要是在做一些資源的預取。

ResourceExtractor主要是針對在DEBUG或者是JIT模式下,針對安裝包內資源的提取邏輯。

在DEBUG或者JIT模式下,需要提取Assets目錄下的資源文件到存儲中,Assets本質上還是Zip壓縮包的一部分,沒有自己的物理路徑,所以需要提取,并返回真真實的物理路徑。在DEBUG和JIT模式下,F(xiàn)lutterSDK和業(yè)務代碼將被構建成Kernel格式的二進制文件,Engine將通過文件內存映射的方式進行加載。

詳見:「三、libflutter.so和libapp.so」

1.2 ResourceExtractor

libflutter.so和libapp.so

在DEBUG | JIT模式下,我們是沒有l(wèi)ibapp.so的,而在release模式下,是有l(wèi)ibapp.so文件的,我們分別解包兩個不同的Apk文件,可以很清楚地看到這一點:

我們知道,libflutter.so是存放flutter的一些基礎類庫的so文件,而libapp.so則是存放我們業(yè)務代碼的so文件,那如果在DEBUG|JIT模式下,沒有l(wèi)ibapp.so,那么我們的業(yè)務代碼存儲在哪里呢?

此時,我們就要看看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的快照數(shù)據(jù)、iSolate的快照數(shù)據(jù)DEFAULT_KERNEL_BLOB的數(shù)據(jù)。因為Flutter本身支持熱重載的特性,保存狀態(tài)和快照(Snapshot)之間必然是不可分割的。

而DEFAULT_KERNEL_BLOB是一個字符串常量: "kernel_blob.bin",結合前面的內容:

FlutterSDK和業(yè)務代碼將被構建成Kernel格式的二進制文件

我們有理由猜測, "kernel_blob.bin" ,就是我們的業(yè)務代碼,F(xiàn)lutter是支持邏輯代碼熱重載的,所以這個字面量加載的資源同樣可能會被重新加載。

這也是為什么,如果我們在State中,新增了某個變量作為Widget的某個狀態(tài),在initState中調用了,然后使用熱重載之后,會導致State中找不到這個變量,因為initState在初次啟動時就被調用過了,后續(xù)的熱重載只會將之前的Snapshot恢復回來,而不會走initState的邏輯。

我們可以在app-debug.apk的assets中,找到"kernel_blob.bin"文件,同樣也可以找到isolate_snapshot_data、vm_snapshot_data文件,所以ResourceExtractor加載的,基本上都是這個文件夾中的文件。

但是,在非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走完了。

你會發(fā)現(xiàn)有個問題,在Debug模式下,我們加載業(yè)務代碼是從二進制文件:"kernel_blob.bin"中加載的,而Release模式下,實在libapp.so中加載的,上面已經出現(xiàn)了加載"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通常作為系統(tǒng)調用用戶操作之間的那么個東西,它存在的形式在Linux/Mac中一般就是一個Shell軟件,通常運行在終端當中(你可以粗略地就將Shell 和終端劃等號 )。

所以,F(xiàn)lutter的Shell自然而然地旨在設置Flutter運行的一個「基底」,ShellArgs,則是我們使用這么個「基底」的參數(shù)。

和之前提到的ResourceExtractor在JIT|DEBUG模式下主動去加載VM和Isoalte快照數(shù)據(jù)類似地,ShellArgs會在DEBUG和JIT模式下,去設置VM快照數(shù)據(jù)、Isolate快照數(shù)據(jù)和Kernel的地址。

別忘了,Kernel即上述的“kernel_blob.bin”二進制文件,是在Debug階段我們的業(yè)務代碼,和libapp.so是相對的。

而在除上述之外的條件下,F(xiàn)lutter設置了一個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中添加內容的兩個字符串的內容,大致上就是指定了裝載在系統(tǒng)的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庫的動態(tài)化的,之前嘗試過去動態(tài)修改aotSharedLibraryName的值和路徑,希望FlutterLoader從該地址去加載libapp.so,以實現(xiàn)Android側借助Tinker熱修復Flutter代碼,但是并沒有細看源碼,打了N個Debug包去測試,結果現(xiàn)在發(fā)現(xiàn)這邏輯壓根沒走。

除了上述的兩個libapp.so的名稱和路徑之外,在DEBUG | JIT模式下的ShellArgs的全家福大致如下:

其實你仔細看看,上述的Kernel的Path并沒有在這里面,因為它作為參數(shù),傳遞給了flutterJNI.init函數(shù)。

三、實踐:自定義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()函數(shù),但是會讓我們反射設置的值被覆蓋掉。

但是,我們關注一下,在setupFlutterEngine時,我們new FlutterEngine的參數(shù):

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

此處的host,就是我們的FlutterActivity,因為FlutterActivity本身就是FlutterActivityAndFragmentDelegate.Host接口的實現(xiàn)類,而這個host.getFlutterShellArgs().toArray(),最終會作為我們在FlutterActivity預設的參數(shù),在所其他系統(tǒng)預設參數(shù)被加入之前被加入到我們的shellArgs數(shù)組中。

所以,我們在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() , 并把計數(shù)器的加法按鈕對應的增加按鈕,改成減少調用。

然后在Flutter項目根目錄使用安裝Release包:

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

然后我們修復Bug,將代碼恢復到最開始的默認狀態(tài),然后:

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

解壓apk,然后把對應的so文件移出來,放到對應的文件夾下: /data/data/com.example.untitled1/libfixedapp.so 。完成之后,重新啟動程序,即可從新的、我們指定的路徑加載新的 libapp.so 了:

以上就是替換so文件來動態(tài)替換Flutter代碼實現(xiàn)詳解的詳細內容,更多關于so文件動態(tài)替換Flutter代碼的資料請關注腳本之家其它相關文章!

相關文章

最新評論