安卓熱更新技術方案
方案集成之前的預設問題:
??如果考慮付費,推薦選擇阿里的Sophix,Sophix是綜合優化的產物,功能完善、開發簡單透明、提供分發及監控管理,
??如果不考慮付費,只需支持方法級別的Bug修復,不支持資源及so,推薦使用Robust,
??如果不考慮付費,但要考慮需要同時支持資源及so,推薦使用Tinker,
正常開發流程:
新版本上線,發現問題或用戶反饋bug,緊急修復,上線版本,用戶重新安裝,如下圖:

熱修復流程:
新版本上線,發現問題或用戶反饋,緊急修復,上線補丁,自動修復,如下圖:

方案對比分析:
| 方案 | Tinker | Robust | Sophix |
|---|---|---|---|
| 類替換 | yes | no | yes |
| so替換 | yes | no | yes |
| 資源替換 | yes | no | yes |
| 全平臺支持 | yes | yes | yes |
| 即時生效 | no | yes | yes |
| 補丁包大小 | 較小 | 一般 | 較小 |
| 復雜度 | 一般 | 復雜 | 傻瓜式接入 |
| 成功率 | 較高 | 非常高 | 非常高 |
| 收費 | no | no | yes |
Sophix集成實作:

1.添加工程依賴
gradle遠程倉庫依賴, 打開專案找到app的build.gradle檔案,添加如下配置:添加maven倉庫地址:
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/repositories/releases"
}
}
添加gradle坐標版本依賴:
android {
......
defaultConfig {
applicationId "com.xxx.xxx" //包名
......
ndk {
//選擇要添加的對應cpu型別的.so庫,
//熱修復支持五種
abiFilters 'arm64-v8a', 'armeabi', 'armeabi-v7a', 'x86', 'x86_64'
}
......
}
......
}
dependencies {
......
compile 'com.aliyun.ams:alicloud-android-hotfix:3.2.18'
......
}
2.添加應用權限:
Sophix SDK使用到以下權限,使用maven依賴或者aar依賴可以不用配置,具體配置在AndroidManifest.xml中,
<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_EXTERNAL_STORAGE"/>
3.配置AndroidManifest檔案
在AndroidManifest.xml中間的application節點下添加如下配置:
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="App ID" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="App Secret" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="RSA密鑰" />
4. 混淆配置
#基線包使用,生成mapping.txt
-printmapping mapping.txt
#生成的mapping.txt在app/build/outputs/mapping/release路徑下,移動到/app路徑下
#修復后的專案使用,保證混淆結果一致
#-applymapping mapping.txt
#hotfix
-keep class com.taobao.sophix.**{*;}
-keep class com.ta.utdid2.device.**{*;}
#防止inline
-dontoptimize
5.初始化
初始化的呼叫應該盡可能的早,必須在Application.attachBaseContext()的最開始(在super.attachBaseContext之后,如果有Multidex,也需要在Multidex.install之后)進行SDK初始化操作,初始化之前不能用到其他自定義類,否則極有可能導致崩潰,而查詢服務器是否有可用補丁的操作可以在后面的任意地方,不建議在Application.onCreate()中初始化,因為如果帶有ContentProvider,就會使得Sophix初始化時機太遲從而引發問題,
Sophix最新版本引入了新的初始化方式,
原來的初始化方式仍然可以使用,只是新方式可以提供更全面的功能修復支持,將會帶來以下優點:
初始化與應用原先業務代碼完全隔離,使得原先真正的Application可以修復,并且減少了補丁預加載時間等等,
新方式能夠更完美地兼容Android 8.0以后版本,
具體而言,是需要用戶自行加入以下這個類:
package com.my.pkg;
import android.app.Application;
import android.content.Context;
import android.support.annotation.Keep;
import android.util.Log;
import com.taobao.sophix.PatchStatus;
import com.taobao.sophix.SophixApplication;
import com.taobao.sophix.SophixEntry;
import com.taobao.sophix.SophixManager;
import com.taobao.sophix.listener.PatchLoadStatusListener;
import com.my.pkg.MyRealApplication;
/**
* Sophix入口類,專門用于初始化Sophix,不應包含任何業務邏輯,
* 此類必須繼承自SophixApplication,onCreate方法不需要實作,
* 此類不應與專案中的其他類有任何互相呼叫的邏輯,必須完全做到隔離,
* AndroidManifest中設定application為此類,而SophixEntry中設為原先Application類,
* 注意原先Application里不需要再重復初始化Sophix,并且需要避免混淆原先Application類,
* 如有其它自定義改造,請咨詢官方后妥善處理,
*/
public class SophixStubApplication extends SophixApplication {
private final String TAG = "SophixStubApplication";
// 此處SophixEntry應指定真正的Application,并且保證RealApplicationStub類名不被混淆,
@Keep
@SophixEntry(MyRealApplication.class)
static class RealApplicationStub {}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 如果需要使用MultiDex,需要在此處呼叫,
// MultiDex.install(this);
initSophix();
}
private void initSophix() {
String appVersion = "0.0.0";
try {
appVersion = this.getPackageManager()
.getPackageInfo(this.getPackageName(), 0)
.versionName;
} catch (Exception e) {
}
final SophixManager instance = SophixManager.getInstance();
instance.setContext(this)
.setAppVersion(appVersion)
.setSecretMetaData(null, null, null)
.setEnableDebug(true)
.setEnableFullLog()
.setPatchLoadStatusStub(new PatchLoadStatusListener() {
@Override
public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
if (code == PatchStatus.CODE_LOAD_SUCCESS) {
Log.i(TAG, "sophix load patch success!");
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 如果需要在后臺重啟,建議此處用SharePreference保存狀態,
Log.i(TAG, "sophix preload patch success. restart app to make effect.");
}
}
}).initialize();
}
}
// queryAndLoadNewPatch不可放在attachBaseContext 中,否則無網路權限,建議放在后面任意時刻,如onCreate中
SophixManager.getInstance().queryAndLoadNewPatch();
這其中,關鍵一點是:
@Keep
@SophixEntry(MyRealApplication.class)
static class RealApplicationStub {}
SophixEntry應指定專案中原先真正的Application(原專案里application的android::name指定的),這里用MyRealApplication指代,并且保證RealApplicationStub類名不被混淆,而SophixStubApplication的類名和包名可以自行取名,
這里的Keep是android.support包中的類,目的是為了防止這個內部靜態類的類名被混淆,因為sophix內部會反射獲取這個類的SophixEntry,如果專案中沒有依賴android.support的話,就需要在progurad里面手動指定RealApplicationStub不被混淆,
在proguard檔案里面需要加上下面內容:
-keepclassmembers class com.my.pkg.MyRealApplication {
public <init>();
}
-keep class com.my.pkg.SophixStubApplication$RealApplicationStub
目的是防止真正Application的構造方法被proguard混淆,
最后,需要把AndroidManifest里面的application改為這個新增的SophixStubApplication類:
<application
android:name="com.my.pkg.SophixStubApplication"
... .../>
... ...
... ...
... ...
Bugly熱更新集成實作
1.添加插件依賴
工程根目錄下“build.gradle”檔案中添加:
buildscript {
repositories {
jcenter()
}
dependencies {
// tinkersupport插件, 其中lastest.release指拉取最新版本,也可以指定明確版本號,例如1.0.4
classpath "com.tencent.bugly:tinker-support:1.1.5"
}
}
注意:自tinkersupport 1.0.3版本起無需再配tinker插件的classpath,
版本對應關系:
tinker-support 1.1.3 對應 tinker 1.9.8
tinker-support 1.1.2 對應 tinker 1.9.6
tinker-support 1.1.1 對應 tinker 1.9.1
tinker-support 1.0.9 對應 tinker 1.9.0
tinker-support 1.0.8 對應 tinker 1.7.11
tinker-support 1.0.7 對應 tinker 1.7.9
tinker-support 1.0.4 對應 tinker 1.7.7
tinker-support 1.0.3 對應 tinker 1.7.6
tinker-support 1.0.2 對應 tinker 1.7.5(需配置tinker插件的classpath)
2.集成SDK
gradle配置
在app module的“build.gradle”檔案中添加(示例配置):
android {
defaultConfig {
ndk {
//設定支持的SO庫架構
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
}
dependencies {
compile "com.android.support:multidex:1.0.1" // 多dex配置
//注釋掉原有bugly的倉庫
//compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本號,也可以指定明確的版本號,例如1.3.4
compile 'com.tencent.bugly:crashreport_upgrade:1.3.6'
// 指定tinker依賴版本(注:應用升級1.3.5版本起,不再內置tinker)
compile 'com.tencent.tinker:tinker-android-lib:1.9.9'
compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本號,也可以指定明確的版本號,例如2.2.0
}
后續更新升級SDK時,只需變更配置腳本中的版本號即可,
注意: 升級SDK已經集成crash上報功能,已經集成Bugly的用戶需要注釋掉原來Bugly的jcenter庫; 已經配置過符號表的Bugly用戶保留原有符號表配置; Bugly SDK(2.1.5及以上版本)已經將Java Crash和Native Crash捕獲功能分開,如果想使用NDK庫,需要配置: compile ‘com.tencent.bugly:nativecrashreport:latest.release’
在app module的“build.gradle”檔案中添加:
// 依賴插件腳本
apply from: 'tinker-support.gradle'
tinker-support.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
// 開啟后tinkerPatch配置不生效,即無需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 編譯補丁包時,必需指定基線版本的apk,默認值為空
// 如果為空,則表示不是進行補丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-release.apk"
// 對應tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"
// 對應tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"
// 構建基準包和補丁包都要指定不同的tinkerId,并且必須保證唯一性
tinkerId = "base-1.0.1"
// 構建多渠道補丁時使用
// buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 是否啟用加固模式,默認為false.(tinker-spport 1.0.7起支持)
// isProtectedApp = true
// 是否開啟反射Application模式
enableProxyApplication = false
// 是否支持新增非export的Activity(注意:設定為true才能修改AndroidManifest檔案)
supportHotplugComponent = true
}
/**
* 一般來說,我們無需對下面的引數做任何的修改
* 對于各引數的詳細介紹請參考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
tinkerPatch {
//oldApk ="${bakPath}/${appName}/app-release.apk"
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"
// path = "/usr/local/bin/7za"
}
buildConfig {
keepDexApply = false
//tinkerId = "1.0.1-base"
//applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可選,設定mapping檔案,建議保持舊apk的proguard混淆方式
//applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" // 可選,設定R.txt檔案,通過舊apk檔案保持ResId的分配
}
}
3.初始化SDK
enableProxyApplication = false 的情況
這是Tinker推薦的接入方式,一定程度上會增加接入成本,但具有更好的兼容性,
集成Bugly升級SDK之后,我們需要按照以下方式自定義ApplicationLike來實作Application的代碼(以下是示例):
自定義Application
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL, "xxx.xxx.SampleApplicationLike",
"com.tencent.tinker.loader.TinkerLoader", false);
}
}
注意:這個類集成TinkerApplication類,這里面不做任何操作,所有Application的代碼都會放到ApplicationLike繼承類當中
引數決議
引數1:tinkerFlags 表示Tinker支持的型別 dex only、library only or all suuport,default: TINKER_ENABLE_ALL
引數2:delegateClassName Application代理類 這里填寫你自定義的ApplicationLike
引數3:loaderClassName Tinker的加載器,使用默認即可
引數4:tinkerLoadVerifyFlag 加載dex或者lib是否驗證md5,默認為false
將以前的Applicaton配置為繼承TinkerApplication的類:

自定義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);
// you must install multiDex whatever tinker is installed!
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);
}
}
注意:tinker需要你開啟MultiDex,你需要在dependencies中進行配置compile "com.android.support:multidex:1.0.1"才可以使用MultiDex.install方法; SampleApplicationLike這個類是Application的代理類,以前所有在Application的實作必須要全部拷貝到這里,在onCreate方法呼叫SDK的初始化方法,在onBaseContextAttached中呼叫Beta.installTinker(this);,
enableProxyApplication = true 的情況
注:無須你改造Application,主要是為了降低接入成本,我們插件會動態替換AndroidMinifest檔案中的Application為我們定義好用于反射真實Application的類(需要您接入SDK 1.2.2版本 和 插件版本 1.0.3以上),
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 這里實作SDK初始化,appId替換成你的在Bugly平臺申請的appId
// 除錯時,將第三個引數改為true
Bugly.init(this, "900029763", false);
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// you must install multiDex whatever tinker is installed!
MultiDex.install(base);
// 安裝tinker
Beta.installTinker();
}
}
4. AndroidManifest.xml配置
在AndroidMainfest.xml中進行以下配置:
1. 權限配置
<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" />
2. Activity配置
<activity
android:name="com.tencent.bugly.beta.ui.BetaActivity"
android:configChanges="keyboardHidden|orientation|screenSize|locale"
android:theme="@android:style/Theme.Translucent" />
3. 配置FileProvider
注意:如果您想兼容Android N或者以上的設備,必須要在AndroidManifest.xml檔案中配置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>
如果使用的第三方庫也配置了同樣的FileProvider, 可以通過繼承FileProvider類來解決合并沖突的問題,示例如下:
<provider
android:name=".utils.BuglyFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="name,authorities,exported,grantUriPermissions">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"
tools:replace="name,resource"/>
</provider>
FileProvider類是在support-v4包中的,檢查你的工程是否引入該類別庫,
在res目錄新建xml檔案夾,創建provider_paths.xml檔案如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- /storage/emulated/0/Download/${applicationId}/.beta/apk-->
<external-path name="beta_external_path" path="Download/"/>
<!--/storage/emulated/0/Android/data/${applicationId}/files/apk/-->
<external-path name="beta_external_files_path" path="Android/data/"/>
</paths>
注意:這里配置的兩個外部存盤路徑是升級SDK下載的檔案可能存在的路徑,一定要按照上面格式配置,不然可能會出現錯誤,
1.3.1及以上版本,可以不用進行以上配置,aar已經在AndroidManifest配置了,并且包含了對應的資源檔案,
5.混淆配置
為了避免混淆SDK,在Proguard混淆檔案中增加以下配置:
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# tinker混淆規則
-dontwarn com.tencent.tinker.**
-keep class com.tencent.tinker.** { *; }
如果使用了support-v4包,還需要配置以下混淆規則:
-keep class android.support.**{*;}
robust集成
1.添加插件依賴
1.在App的build.gradle,加入如下依賴:
apply plugin: 'com.android.application'
//制作補丁時將這個打開,auto-patch-plugin緊跟著com.android.application
//apply plugin: 'auto-patch-plugin'
apply plugin: 'robust'
compile 'com.meituan.robust:robust:0.4.99'
2.在整個專案的build.gradle加入classpath:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.meituan.robust:gradle-plugin:0.4.99'
classpath 'com.meituan.robust:auto-patch-plugin:0.4.99'
}
}
3.在工程專案的src同級目錄下配置robust.xml檔案
注意 gradle 3.6及以上版本默認啟用R8,會將插入的ChangeQuickRedirect變數優化掉,需要在混淆檔案proguard-rules.pro中加入以下代碼:
-keepclassmembers class **{ public static com.meituan.robust.ChangeQuickRedirect *; }
Robust補丁自動化,為Robust自動生成補丁
使用者只需要提交修改完bug后的代碼,運行和線上apk打包同樣的gradle命令即可,會在專案的app/build/outputs/robust目錄下生成補丁,
自動化補丁工具
gradle命令:./gradlew clean assembleRelease --stacktrace --no-daemon
2.修復使用
1.使用插件時,需要把auto-patch-plugin放置在com.android.application插件之后,其余插件之前,
apply plugin: 'com.android.application'
apply plugin: 'auto-patch-plugin'
2.將保存下來的mapping檔案和methodsMap.robust檔案放在app/robust/檔案夾下,
3.修改代碼,在改動的方法上面添加@Modify注解,對于Lambda運算式請在修改的方法里面呼叫RobustModify.modify()方法
@Modify
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
//或者是被修改的方法里面呼叫RobustModify.modify()方法
protected void onCreate(Bundle savedInstanceState) {
RobustModify.modify()
super.onCreate(savedInstanceState);
}
新增的方法和欄位使用@Add注解
//增加方法
@Add
public String getString() {
return "Robust";
}
//增加類
@Add
public class NewAddCLass {
public static String get() {
return "robust";
}
}
4.運行和生成線上apk同樣的命令,即可生成補丁,補丁目錄app/build/outputs/robust/patch.jar
5.補丁制作成功后會停止構建apk,出現類似于如下的提示,表示補丁生成成功

3.樣例使用
-
生成樣例apk,執行gradle命令:
./gradlew clean assembleRelease --stacktrace --no-daemon
-
安裝樣例apk,保存mapping.txt檔案以及app/build/outputs/robust/methodsMap.robust檔案
-
修改代碼之后,加上**@Modify**注解或者呼叫RobustModify.modify()方法
-
把保存的mapping.txt和methodsMap.robust放到app/robust目錄下
-
執行與生成樣式apk相同的gradle命令:
./gradlew clean assembleRelease --stacktrace --no-daemon
-
補丁制作成功后會停止構建apk,出現類似于如下的提示,表示補丁生成成功

-
將補丁檔案copy到手機目錄/sdcard/robust下
adb push ~/Desktop/code/robust/app/build/outputs/robust/patch.jar /sdcard/robust/patch.jar
補丁的路徑/sdcard/robust是PatchManipulateImp中指定的
4. 注意事項:
1.內部類的構造方法是private(private會生成一個匿名的建構式)時,需要在制作補丁程序中手動修改構造方法的訪問域為public
2.對于方法的回傳值是this的情況現在支持不好,比如builder模式,但在制作補丁代碼時,可以通過如下方式來解決,增加一個類來包裝一下(如下面的B類),
method a(){ return this; }
改為
method a(){ return new B().setThis(this).getThis(); }
3. 欄位增加能力內測中,不過暫時可以通過增加新類,把欄位放到新類中的方式來實作欄位增加能力
4.新增的類支持包括靜態內部類和非內部類
5.對于只有欄位訪問的函式無法直接修復,可通過呼叫處間接修復
總結:
應用層面來說,阿里系的Sophix方案最優,但是該方案收費,美團的Robust不支持資源修改,所以有了很大限制,而且該技術官方已經很久不維護,并且集成對公司專案改動比較大,所以綜合來說性價比最高的方案是騰訊系的Tinker,針對Tinker熱更新,騰訊有Bugly針對tinker的集成,補丁檔案有一站式的管理,而且還可以獨立出Bugly,由公司自己的服務器去維護補丁檔案的管理,更加靈活,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/280728.html
標籤:其他
