Flutter 應用熱更新

xiangzhihong發表於2020-04-24

Flutter 熱更新簡介

所謂熱更新,指的是當應用程式碼出現缺陷問題時,不需要重新打包提交App Store即可完成缺陷的修復。眾所周知,使用原生技術開發的應用體驗雖然好,但開發、上線週期長也常常被詬病,特別是當應用出現線上問題時,不得不重新打包釋出,大大的影響了使用者體驗,而熱更新技術就是為有效解決線上缺陷而提出的。

不過,熱更新雖然具有很大的優點,但是濫用熱修復也會給應用帶來不好的體驗,並且蘋果對於熱更新和修復是明令禁止的,所以熱更新主要針對的是國內Android市場。目前,Flutter對外開放的SDK是不支援熱更新的,但是在Flutter的原始碼裡有一部分預埋的熱更新相關的程式碼,可以通過一些必要的手段在Android端實現動態更新功能。

眾所周知,不論是新建立的Flutter專案,還是原生工程以Moudle或者aar的方式整合Flutter,最終Flutter在原生Android端應用中都是以混合的形式存在的。所以,當我們拆開一個Flutter在release模式下編譯生成的aar包時,其目錄結構下圖所示。

在這裡插入圖片描述
實際開發中,只需要關注assets、jni、libs這三個目錄即可,其他都是原生的殼工程產物。

  • jni:該檔案目錄下存放的是libflutter.so檔案,該檔案是Flutter引擎層的C++實現,提供skia繪製引擎、Dart和Text紋理繪製等支援。
  • libs:該檔案目錄下存放的是flutter.jar檔案,該檔案為Flutter嵌入層的 Java實現,主要為Flutter的原生層提供平臺功能支援,比如建立執行緒。
  • assets:該檔案目錄主要用於存放Flutter應用層的資源,包括images、font等。

而眾觀目前所有的Flutter熱更新方案中,其基本原理實現都是一樣的,即通過修改libapp.so的載入路徑,把它替換成開發者自己的libapp_hot.so來實現熱更新。我們可以開啟io.flutter.embedding.engine.loader包中的FlutterLoader類來檢視libapp.so包的載入邏輯。

在原生Android開發中,我們可以使用Tinker、AndFix、Sophix和Robust等熱修復框架來實現應用的熱更新操作。綜合比較,Tinker是一款不錯的熱修復框架,並且它支援修復so檔案,既然Flutter熱更新的基本原理是替換libapp.so,那麼我們就可以利用Tinker熱更新功能將需要修復的libapp_hot.so送達客戶端,然後再載入libapp_hot.so檔案即可實現程式碼的熱更新。

接入Bugly

在使用Tinker之前,需要我們在原生Android平臺接入Tinker。不過,在Flutter開發中,我們可以通過整合Bugly來整合Tinker,因為Bugly預設整合了Tinker,並且還提供缺陷上報和應用升級等功能,可謂一舉兩得。 接入Bugly之前,需要先開通Bugly賬號,並在官網註冊自己的產品,如果還沒有賬號,可以開啟Bugly官網註冊一個。使用Android Studio開啟Flutter專案的Android工程,然後在根目錄的build.gradle檔案中新增如下指令碼,如下所示。

buildscript {

dependencies {
// tinkersupport外掛, 其中lastest.release表示拉取最新版本
        classpath "com.tencent.bugly:tinker-support:1.2.0"
    }
}

複製程式碼

接著,開啟Android工程app目錄下的build.gradle檔案,並在android節點和dependencies節點新增如下配置指令碼。

android {
     …  //省略其他配置
     defaultConfig {
       ndk {
         //設定支援的so庫架構
         abiFilters 'armeabi', 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
        }
      }
    }

dependencies {
     implementation 'com.android.support:multidex:1.0.3'
     implementation 'com.tencent.bugly:crashreport_upgrade:1.4.2'
     //指定tinker依賴版本
     implementation 'com.tencent.tinker:tinker-android-lib:1.9.14' 
  }

複製程式碼

當需要更新升級SDK的時候,只需要更新配置指令碼中的版本號即可。接下來,在build.gradle的同級目錄下建立一個名為tinker-support.gradle的配置檔案,並新增如下指令碼。

apply plugin: 'com.tencent.bugly.tinker-support'

def bakPath = file("${buildDir}/bakApk/")
//填寫每次構建生成的基準包目錄
def baseApkDir = "app-0208-15-10-00"

tinkerSupport {
    //開啟tinker-support外掛,預設值true
enable = true

    //指定歸檔目錄,預設值當前module的子目錄tinker
autoBackupApkDir = "${bakPath}"

    //是否啟用覆蓋tinkerPatch配置功能,預設值false
overrideTinkerPatchConfiguration = true

    // @{link tinkerPatch.oldApk }
    baseApk = "${bakPath}/${baseApkDir}/app-release.apk"
    baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"

    //構建基準包和補丁包都要指定不同的tinkerId,並且必須保證唯一性
    tinkerId = "base-1.0.1"
    enableProxyApplication = false
    supportHotplugComponent = true
}

tinkerPatch {
    ignoreWarning = false
    useSign = true
    dex {
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
        pattern = ["lib/*/*.so"]
    }
    res {
       pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
       ignoreChange = []
       largeModSize = 100
    }
    packageConfig {
    }
    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
    }
    buildConfig {
        keepDexApply = false
    }
}

複製程式碼

在上面的配置指令碼中,我們需要重點關注baseApkDir和tinkerId兩個屬性。其中,baseApkDir表示每次構建生成的基準包目錄,而tinkerId表示構建基準包和補丁包需要指定的唯一標識。然後,再次開啟build.gradle檔案,在build.gradle檔案的頭部引入tinker-support.gradle配置指令碼檔案,如下所示。

//依賴Tinker外掛指令碼
apply from: 'tinker-support.gradle'

複製程式碼

Bugly提供了兩種初始化SDK的方式,區別是是否需要反射Application。首先,自定義一個繼承自TinkerApplication的Application類,如下所示。

public class SampleApplication extends TinkerApplication {
    public SampleApplication() {
        super(ShareConstants.TINKER_ENABLE_ALL, "com.xzh.hotreload.SampleApplicationLike",
                "com.tencent.tinker.loader.TinkerLoader", true);
    }
}

複製程式碼

可以發現,SampleApplication需要傳入四個引數,需要變更的就是第二個引數,表示自定義的ApplicationLike,如下所示。

public class SampleApplicationLike extends DefaultApplicationLike {

    public static final String TAG = "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags,
            boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
            long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }


    @Override
    public void onCreate() {
        super.onCreate();
        // SDK初始化,appId替換成你的在Bugly平臺申請的appId, 除錯時,將第三個引數改為true
        Bugly.init(getApplication(), "900029763", false);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        MultiDex.install(base);

        // 安裝tinker
        // TinkerManager.installTinker(this); 替換成下面Bugly提供的方法
        Beta.installTinker(this);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
        getApplication().registerActivityLifecycleCallbacks(callbacks);
    }
}

複製程式碼

SampleApplicationLike類中Bugly初始化時需要用到appId,如果沒有可以到Bugly官網註冊應用,然後系統會生成一個對應的appId,如下圖所示。

在這裡插入圖片描述
由於Tinker需要開啟MultiDex,所以整合Bugly時還需要在build.gradle配置檔案中新增multidex外掛才可以使用MultiDex.install()方法。完成上述操作後,還需要在AndroidManifest.xml配置中新增如下配置。

<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

//相容Android N或者以上裝置,必須配置FileProvider來訪問共享路徑的檔案
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>

複製程式碼

需要說明的是,製作Android正式環境的基準包時,還需要在build.gradle檔案中配置簽名資訊,如下所示。

android {
     …  //省略其他配置
     signingConfigs {
        release {
            try {
                storeFile file("./keystore/release.keystore")
                storePassword "testres"
                keyAlias "testres"
                keyPassword "testres"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }
    }

複製程式碼

當製作簽名正式包時,為了避免因為混淆而造成程式碼功能異常,還需要在Proguard混淆檔案中增加配置如下來避免混淆SDK。

-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# tinker混淆規則
-dontwarn com.tencent.tinker.**
-keep class com.tencent.tinker.** { *; }
-keep class android.support.**{*;}

複製程式碼

到此,原生Android接入Bugly就大體完成了。如果接入過程有任何問題,可以參考Bugly官網接入文件

Flutter應用熱更新

接下來,我們通過 執行Android的熱更新操作之前,需要先製作應用的基準包。執行flutter build apk –release打包命令,或者開啟Android Studio右邊的Gradle皮膚執行打包操作,如下圖所示。

在這裡插入圖片描述
執行assemble操作後,系統會在Flutter專案的build/app/bakApk資料夾下生成對應的基準包,並且每個正式簽名的的基準包目錄都會包含基準包、混淆配置檔案和資源Id檔案,如下圖所示。
在這裡插入圖片描述
然後,將生成的app-release.apk正式基準包上傳到Bugly的後臺。當線上版本出現缺陷問題時,就可以使用tinker-support外掛生成對應的補丁包。 修復程式碼裡面的缺陷問題後,還需要修改tinker-support.gradle檔案裡面對應的baseApkDir和tinkerId的屬性值,然後才能執行補丁包生成操作,如下圖所示。
在這裡插入圖片描述
執行完補丁包生成操作之後,就會在Flutter工程的根目錄的build/outputs資料夾下生成對應的補丁包檔案,如下圖所示。
在這裡插入圖片描述
然後,將生成的帶簽名的補丁包檔案上傳到Bugly後臺,選擇補丁的下發範圍,點選立即下發讓補丁生效,然後重新啟動Flutter應用就可以看到應用更新後的效果。

注: 本文部分摘自《Flutter應用跨平臺開發實戰》(即將出版) 參考資料:

1,移動跨平臺方案對比:WEEX、React Native、Flutter和PWA
2,Flutter入門與環境搭建
3,Flutter開發之Dart語言基礎
4,Flutter基礎知識
5,Flutter開發之基礎Widgets
6,Flutter 應用程式除錯
7,Flutter For Web入門實戰
8,Flutter開發之非同步程式設計
9,Flutter開發之網路請求
10,Flutter開發之JSON解析
11,Flutter開發之路由與導航
12,Flutter 必備開源專案
13,Flutter 國際化適配實戰
14,Flutter應用整合極光推送
15,Flutter混合開發
16,構建屬於自己的Flutter混合開發框架
17,Flutter 應用效能檢測與優化
18,Flutter異常監測與上報
19,Flutter的Hot Reload是如何做到的
20,Apple為什麼不封殺 Flutter,以後會封殺嗎
21,《Flutter in action》開源

相關文章