Tinker熱修復——Bugly讓熱修復變得如此簡單

本文轉載自 https://www.jianshu.com/p/546d15891d9b ,有刪改

前言

不知你是否遇到這樣的情況?千辛萬苦上開發了一個版本,好不容易上線了,突然發現了一個嚴重bug需要進行緊急修復,怎麼辦?難道又要重新打包App、測試,發佈新個版本?就爲了修改一兩行的代碼?
莫慌,這種問題其實可以分分鐘解決。如果你學會了這項黑科技——熱修復。
在用戶使用App的時候,不知不覺,這個Bug就被修復了。

介紹

當下熱修復框架的種類繁多,其中有名的包括阿里的AndFix,微信的Tinker、QQ空間的超級補丁,美團的Robust、餓了麼的Amigo、美麗說蘑菇街的Aceso等等,雖然熱修復框架很多,但熱修復框架的核心技術主要有三類,分別是代碼修復、資源修復和動態鏈接庫修復,部分熱修復框架的對比如下表所示:
在這裏插入圖片描述
上表的信息很難做到完全準確,因爲部分的熱修復框架還在不斷更新迭代。

對比總結:(總結對比摘自Tinker官方Wiki
1,AndFix作爲native解決方案,首先面臨的是穩定性與兼容性問題,更重要的是它無法實現類替換,它是需要大量額外的開發成本的;
2,Robust兼容性與成功率較高,但是它與AndFix一樣,無法新增變量與類只能用做的bugFix方案;
3,Qzone方案可以做到發佈產品功能,但是它主要問題是插樁帶來Dalvik的性能問題,以及爲了解決Art下內存地址問題而導致補丁包急速增大的。

特別是在Android N之後,由於混合編譯的inline策略修改,對於市面上的各種方案都不太容易解決。而Tinker熱補丁方案不僅支持類、So以及資源的替換,它還是2.X-7.X的全平臺支持。利用Tinker我們不僅可以用做bugfix,甚至可以替代功能的發佈。Tinker已運行在微信的數億Android設備上,那麼爲什麼你不使用Tinker呢?

Tinker的已知問題

由於原理與系統限制,Tinker有以下已知問題(摘自Tinker官網說明):

1,Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大組件(1.9.0支持新增非export的Activity);
2,由於Google Play的開發者條款限制,不建議在GP渠道動態更新代碼;
3,在Android N上,補丁對應用啓動時間有輕微的影響;
4,不支持部分三星android-21機型,加載補丁時會主動拋出"TinkerRuntimeException:checkDexInstall failed";
5,對於資源替換,不支持修改remoteView。例如transition動畫,notification icon以及桌面圖標。

bugly補丁管理系統

Tinker熱修復是通過打補丁下發補丁的方式來進行的,那麼對於廣大開發者來說就面臨一個問題,那就是補丁包的後臺管理,如果你團隊中的後臺開發人員實力夠強,那麼完全可以自己做一個補丁管理系統,但我想應該沒多少人願意花精力在這個後臺管理系統的開發上面吧,且開發有時候就是在造bug,鬼知道會挖出一個多大的坑呢?對於這樣的一個問題,據我所知,市面上有3種Tinker的補丁管理系統,如下:

  • Bugly:熱修復
  • GitHub:tinker-manager
  • tinkerpatch(Android 熱更新服務平臺)

其中「Bugly」和「tinker-manager」是免費的,「tinkerpatch」是收費的,因爲「tinkerpatch」收費,所以暫時不做考慮。Bugly由騰訊團隊開發並維護,穩定性肯定沒得說,而「tinker-manager」是GitHub上個人開發者開發維護的,穩定性沒法保證(我沒有貶低開發者的意思,畢竟勢單力薄,人多力量大嘛),故本人覺得,Bugly是目前最優的Tinker熱修復解決方案。

開始集成

1,獲取app id

首先我們需要去Bugly的官網去註冊一個賬號,然後創建一個應用,獲取到對應的App ID,過程也比較簡單,保存好申請的App ID就好了,後面會用到。

2,添加插件依賴

項目的build.gradle:

classpath "com.tencent.bugly:tinker-support:1.1.2"

3,集成SDK

app的build.gradle:

// ...
apply from: 'tinker-support.gradle'
android {
    defaultConfig {
        ...
        // 開啓multidex
        multiDexEnabled true
    }
    // recommend
    dexOptions {
        jumboMode = true
    }
    // 簽名配置
    signingConfigs {
        release {
            try {
                storeFile file("./keystore/release.keystore")
                storePassword "testres"
                keyAlias "testres"
                keyPassword "testres"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }

        debug {
            storeFile file("./keystore/debug.keystore")
        }
    }

    // 構建類型
    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    ...
    compile "com.android.support:multidex:1.0.1" // 多dex配置
    // 遠程倉庫集成方式(推薦)
    compile 'com.tencent.bugly:crashreport_upgrade:1.3.4'
}

上面的簽名配置部分根據實際情況修改。
Tinker的最新版本,請留意 Tinker github,強烈建議同學們使用最新的版本,因爲tinker 的wiki上面提到最新版本支持應用加固:
在這裏插入圖片描述
關於Tinker加固和多渠道打包參見博客:https://blog.csdn.net/zengke1993/article/details/80376108 ,

4,配置Tinker

在app的build.gradle文件同級目錄下創建一個tinker-support.gradle文件,內容如下:

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

def bakPath = file("${buildDir}/bakApk/")

//  該屬性在每次生成補丁包時需要修改爲基準包所在的文件名稱,打基準包時不用修改,系統自動以當前時間戳來命名目錄
def baseApkDir = "app-1211-16-01-34"

// 用於生成基準包id(不用修改)
def myTinkerId = "base-" + versions.versionName 

// 用於生成補丁包id(每次生成補丁包都要修改一次,最好是 patch-${versionName}.x.x)
//def myTinkerId = "patch-" + versions.versionName + ".0.0" 

/**
 * 對於插件各參數的詳細解析請參考
 */
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 = "${myTinkerId}"

    // 構建多渠道補丁時使用
    // 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的分配
    }
}

注意注意注意:
上面配置 tinkerSupport 中 { ... tinkerId = "${myTinkerId }" ... } 相當於針對基準包和補丁包的命名在最開始定義了一個變量,然後在下面以 tinkerId = “${myTinkerId}” 的方式去引用該變量,如果使用這種方式來命名,切記要修改tinkerId的取值,不要定義成一個常亮,否則最開始的設置不會生效

上面配置部分屬性解釋如下:

1,overrideTinkerPatchConfiguration

當overrideTinkerPatchConfiguration = true時,tinkerPatch可以省略不寫,Bugly會加載默認的Tinker配置。但請注意,如果你的so文件不是存放在libs目錄下(與src目錄同級),又或者資源文件的存放在你自定義的目錄中,那麼這時你要小心了,這些文件在製作補丁包時不會被檢測,也就是說這些so文件和資源文件將不會被熱修復,這種情況下就需要將overrideTinkerPatchConfiguration = false,bugly就會加載tinkerPatch中的相關配置,在其中就有lib和res屬性的配置。

2,baseApkDir

baseApkDir是基準包(也稱基線包)的目錄,在生產補丁時需要根據基準包在bakApk下具體文件夾名字修改,如:bakApk/xxxx,到時生成補丁包時要將baseApkDir的值改爲xxxx。(xxxx是Tinker自動生成的,根據時間戳來命名)。
也就是說該屬性的值只有在打補丁包時才需要設置爲該補丁包所對應的基準包所在的目錄名稱,在打基準包時該屬性不起作用,系統會自動以當前時間戳來命名。
在這裏插入圖片描述3,tinkerId

tinkerId是Bugly熱修復方案最最重要的一個因素,一般取值爲git版本號、versionName等等(我習慣用versionName),它會將補丁包與基準包產生對應關係,假設基準包的tinkerId爲 base-1.0,則生成的補丁包中的YAPATCH.MF文件關係如下:
在這裏插入圖片描述
Bugly要求baseApk(基準包)的tinkerId與補丁包的tinkerId要不一樣。所以,在生成基準包時,請用如下tinkerId:

def myTinkerId = "base-" + versions.versionName // 用於生成基準包(不用修改)

當生成補丁包時,請使用如下tinkerId:

def myTinkerId = "patch-" + versions.versionName + ".0.0" // 用於生成補丁包(每次生成補丁包都要修改一次,最好是 patch-${versionName}.x.x)

對於同一個基準包,我們可能會多次生成補丁包上傳到Bugly的熱修復管理後臺,這時,這些補丁包的tinkerId也要不一樣,不然的話,當客戶手機上的App在獲取補丁時,會錯亂(親測,當同個基準包的補丁包的tinkerId一樣時,App每次重啓都會獲取不同的補丁包,導致tinkerId相同的補丁包輪流下發)。所以,“patch-” + rootProject.ext.android.versionName + “.0.0"中的”.0.0"(稱爲計數)就是爲了區分每次生成的補丁包,如.0.1,.0.2等等,建議versionName更新時計數重置。

4,versions.versionName

因爲Tinker的配置放在了tinker-support.gradle文件中,與app的build.gradle不在同一個文件中,所以沒辦法通過android.defaultConfig.versionName直接獲取App的versionName,這裏我使用了config.gradle來提取共同的屬性,具體做法如下:
在app的build.gradle文件同級目錄下創建一個config.gradle文件,內容如下:

ext.versions = [
        versionName : "1.0.3" // 當前基準包對應的版本名稱
]

然後在app的build.gradle文件中引入該gradle配置,方式和引入tinker-support.gradle是一樣的:

apply from: 'config.gradle'

然後就可以在tinker-support.gradle中通過versions.versionName獲取到我們在config.gradle中配置的versionName屬性的變量值了,當然了這只是gradle之間共享變量的一種方式,具體的用法參見博客:gradle使用技巧~rootProject.ext 添加全局變量

5、補丁新舊判定規則

def myTinkerId = "patch-" + rootProject.ext.android.versionName + ".0.0" // 用於生成補丁包(每次生成補丁包都要修改一次,最好是 patch-${versionName}.x.x)

對於一個基準包,可以在Bugly上發佈多個補丁包(切記tinkerid不同),這裏或許會讓你誤以爲計數越大,表明補丁越新,這是錯誤的,這個計數僅僅只是區分不同的補丁包而已,它沒有標記補丁新舊的作用,補丁新舊由Bugly來判定,最後上傳的補丁便是最新的補丁,舉個例子,我在昨天上傳了tinkerid爲"patch-1.0.0.9"的補丁1,在今天上傳了tinkerid爲"patch-1.0.0.1"的補丁2,雖然補丁2的計數比補丁1小,但補丁2比補丁1晚上傳,所以補丁2是最新的補丁,即補丁新舊與計數無關。Bugly會下發並應用最新的補丁(即補丁2),但還是建議計數從小到大計算,這裏僅僅只是說明Bugly如何判定補丁新舊罷了。

5,初始化SDK

Bugly的初始化工作需要在Application中完成,但對原生Tinker來說,默認的Application是無法實現熱修復的。因此我們需要對Application進行改造,bugly爲我們提供瞭如下兩種改造的方案:

  • 1,使用原來的自定義Application,Bugly通過反射爲App動態生成新的Application。
  • 2,使用「繼承TinkerApplication + DefaultApplicationLike」

兩種方式分別對應tinker-support.gradle文件中enableProxyApplication的值爲true和false,其中第二種方式是bugly推薦的改造方式,兩種方式的處理如下:

enableProxyApplication = true方式的改造
Bugly將通過反射的方式針對項目中自定義的Application動態生成新的Application,下圖是源碼中的AndroidManifest.xml和編譯好的apk中的AndroidManifest.xml:
在這裏插入圖片描述
既然將enableProxyApplication的值設置爲true,那接下來的重點就是完成Bugly的初始化工作了。需要在自定義的Application的onCreate()中進行Bugly的配置,在attachBaseContext()中進行Bugly的安裝:

public class MyApplication extends Application {

    private Context mContext;

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = getApplicationContext();
        // 這裏實現SDK初始化,appId替換成你的在Bugly平臺申請的appId
        // 調試時,將第三個參數改爲true
        configTinker();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        // you must install multiDex whatever tinker is installed!
        MultiDex.install(mContext);
        // 安裝tinker
        // 此接口僅用於反射Application方式接入。
        Beta.installTinker();
    }

}

注意:
1,Bugly的安裝必須在attachBaseContext()方法中,否則將無法從Bugly服務器獲取最新補丁。
2,tinker需要你開啓MultiDex,你需要在dependencies中進行配置compile "com.android.support:multidex:1.0.1"纔可以使用MultiDex.install方法。

最後在清單文件中,聲明使用我們自定義的Application即可:

<application
    android:name=".app.MyApplication"
    ...>

enableProxyApplication = false方式的改造

這是Bugly推薦的方式,穩定性有保障(因爲第1種方式使用的是反射,可能會存在不穩定的因素),它需要對Application進行改造,首先就是繼承TinkerApplication,然後在默認的構造函數中,將第2個參數修改爲你項目中的ApplicationLike繼承類的全限定名稱:

public class SampleApplication extends TinkerApplication {
    public SampleApplication() {
        super(ShareConstants.TINKER_ENABLE_ALL, "com.lqr.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

接着就是創建ApplicationLike繼承類:

public class SampleApplicationLike extends DefaultApplicationLike {

    public static final String TAG = "Tinker.SampleApplicationLike";
    private Application mContext;

    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();
        mContext = getApplication();
        configTinker();
    }

    @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
        Beta.installTinker(this);
    }

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

    @Override
    public void onTerminate() {
        super.onTerminate();
        Beta.unInit();
    }
}

注意:
SampleApplicationLike這個類是Application的代理類,以前所有在Application的實現必須要全部拷貝到這裏,在onCreate方法調用SDK的初始化方法,在onBaseContextAttached中調用Beta.installTinker(this)。

最後在清單文件中,聲明改造好的Application(注意不是ApplicationLike):

<application
    android:name=".app.SampleApplication"
    ...>

6,配置Bugly

這是Bugly官方給出的配置,應有盡有,註釋也很nice,請仔細看看,對項目的功能拓展與用戶體驗有幫助:

private void configTinker() {
    // 設置是否開啓熱更新能力,默認爲true
    Beta.enableHotfix = true;
    // 設置是否自動下載補丁,默認爲true
    Beta.canAutoDownloadPatch = true;
    // 設置是否自動合成補丁,默認爲true
    Beta.canAutoPatch = true;
    // 設置是否提示用戶重啓,默認爲false
    Beta.canNotifyUserRestart = true;
    // 補丁回調接口
    Beta.betaPatchListener = new BetaPatchListener() {
        @Override
        public void onPatchReceived(String patchFile) {
            Toast.makeText(mContext, "補丁下載地址" + patchFile, Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onDownloadReceived(long savedLength, long totalLength) {
            Toast.makeText(mContext,
                    String.format(Locale.getDefault(), "%s %d%%",
                            Beta.strNotificationDownloading,
                            (int) (totalLength == 0 ? 0 : savedLength * 100 / totalLength)),
                    Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onDownloadSuccess(String msg) {
            Toast.makeText(mContext, "補丁下載成功", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onDownloadFailure(String msg) {
            Toast.makeText(mContext, "補丁下載失敗", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onApplySuccess(String msg) {
            Toast.makeText(mContext, "補丁應用成功", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onApplyFailure(String msg) {
            Toast.makeText(mContext, "補丁應用失敗", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onPatchRollback() {

        }
    };

    // 設置開發設備,默認爲false,上傳補丁如果下發範圍指定爲“開發設備”,需要調用此接口來標識開發設備
    Bugly.setIsDevelopmentDevice(mContext, false);
    // 多渠道需求塞入
    // String channel = WalleChannelReader.getChannel(getApplication());
    // Bugly.setAppChannel(getApplication(), channel);
    // 這裏實現SDK初始化,appId替換成你的在Bugly平臺申請的appId
    Bugly.init(mContext, "e9d0b7f57f", true);
}

這裏就用到了一開始獲取到的App ID了,將其傳入Bugly.init()方法的第二個參數,切記,用你自己的App ID。
其中如下兩個方法很重要:
1,Bugly.setIsDevelopmentDevice()
設置當前設備是不是開發設備,這跟Bugly上傳補丁包時所選的"下發範圍"有關。
在這裏插入圖片描述
2,Bugly.init(context, appid, isDebug)
這個方法除了設置App ID外,還可以設置是否輸出Log,可以觀察到Bugly在App啓動時做了哪些聯網操作。
在這裏插入圖片描述

7、AndroidManifest.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"/>
<uses-permission android:name="android.permission.READ_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配置

<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/file_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/file_paths"
        tools:replace="name,resource"/>
</provider>

4,升級SDK下載路徑配置

在res目錄新建xml文件夾,創建file_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>

注:1.3.1及以上版本,可以不用進行以上配置,aar已經在AndroidManifest配置了,並且包含了對應的資源文件。

8,混淆


-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.**{*;}

好了,集成完畢,接下來就是製作基準包、補丁包和上傳補丁包了。


開始製作基準包

在app編碼完成並測試完成後,就是打包上線了,上線前打的包就是基準包啦,下面我們就來製作基準包,分3步:

  1. 打開app下的tinker-support.gradle文件。
  2. 將帶"base"的tinkerId註釋解開,並註釋掉帶"patch"的tinkerId。
  3. 雙擊運行主Module爲"app"目錄下的build目錄下的assembleRelease。(視具體的項目而定,一般情況下主module都爲app,下圖中對應的主module爲tinker-bugly)
    在這裏插入圖片描述AS在執行assembleRelease指令時,就是在編譯基準包了,當編譯完成時,app的build目錄下會自動生成基準包文件夾,系統自動以時間戳來命名的(也就是說,每次執行assembleRelease指令系統都會基於當前時間戳自動在build目錄創建不同的基準包文件夾)。
    在這裏插入圖片描述這3個文件對之後製作補丁包來說是相當重要的,你需要做的就是將這3個文件保存好,可以保存到雲盤、Git服務器上等等,但就不要讓它就這麼放着,因爲在你再次執行builder時,app的build目錄會被刪除,這樣基準包及mapping與R文件都會丟失。
    到這裏,你就可以把它(基準包:tinker-bugly-release.apk)上架到應用市場了。

進階:多渠道打基準包

關於多渠道打基準包的方案,bugly官方推薦通過多渠道打包框架快速打多渠道包的方式,推薦使用美團walle來打多渠道包,不僅能夠快速打包,也能夠輕鬆實現一個補丁修復所有渠道。
關於具體實現參見本人另一篇博客:Android多渠道打包教程


開始製作補丁包

現在要動態修復App了,對於代碼修復、so庫修復、資源文件修復,修復過程無非是改代碼,替換so文件,替換資源文件,完了之後開始製作補丁包,先將tinker-support.gradle文件打開。

1,基準包命名
確保基準包及相關文件的命名與配置文件中的一致:
在這裏插入圖片描述
2、修改baseApkDir與tinkerId

  • 修改baseApkDir的值爲基準包所有文件夾的名字。
  • 註釋掉帶"base"的tinkerId,取消帶"patch"的tinkerId的註釋(多次生成補丁時,記得修改"計數",區分不同的補丁)。
    在這裏插入圖片描述

3、執行編譯,生成補丁

打開側邊的Gradle標籤,找到項目的主Module,雙擊tinker-support下的buildTinkerPatchRelease指令,生成補丁包。
在這裏插入圖片描述
當編譯完成後,在app的build/outputs/patch目錄下會在"patch_singed_7zip.apk"文件,它就是補丁包,雙擊打開它,可以看到其中有一個YAPATCH.MF,裏面記錄了基準包與補丁包的tinkerId(兩者是肯定不同,如果一樣則說明配置有問題了)。
在這裏插入圖片描述


上傳補丁包

1、流程圖解
首先,點擊進入「Bugly產品頁面」,或點擊“我的產品 ”查看我的產品。
在這裏插入圖片描述
點擊你要管理的產品後,依次點擊"應用升級"、“熱更新”,可以查看到該產品的補丁下發情況(這個產品我還沒上傳過補丁,故一片空白)。
在這裏插入圖片描述
按下圖順序操作即可上傳補丁包:
在這裏插入圖片描述2、上傳失敗分析
有可能你在上傳完補丁包時,頁面會提示"未匹配到可應用補丁包的App版本,請確認補丁包的基線版本是否已經發布"。
在這裏插入圖片描述
遇到這種情況請先冷靜,首先來說明一件事:Bugly怎麼知道基線版本是否已經發布?
通常按我們理解的,基準包發佈就是上架到應用市場,但應用市場又不會通知Bugly某某產品已經上架了,對吧。其實,Bugly的上架通知是這樣的:當基準包在手機上啓動時,Bugly框架就會讓App聯網通知Bugly的服務器,同時上傳當前App的版本號、tinkerId等信息,它這麼做的目的有如下兩個:

  • 標記某個tinkerId的基準包已經被安裝到手機上使用了(即發佈)。
  • 獲取該tinkerId的基準包最新的補丁信息。

所以,當出現了"未匹配到可應用補丁包的App版本,請確認補丁包的基線版本是否已經發布"這樣的提示時,可以確定,這個基準包的tinkerId等信息沒有被上傳到Bugly服務器,對此,鄙人將踩過的坑總結起來,摸索出了自己的解決方法,分如下幾步:

  • 檢查App是否能夠聯網。
  • 檢查App ID是否正確。
  • 結合enableProxyApplication的取值,檢查AndroidManifest.xml中聲明的Application是否寫對。
  • 檢查Bugly的安裝是不是在attachBaseContext()或onBaseContextAttached()方法中完成。

像我就犯過這樣的錯,明明在tinker-support.gradle文件中設置了enableProxyApplication = true,結果在AndroidManifest.xml中卻聲明瞭TinkerApplication的繼承類。
在這裏插入圖片描述所以這裏只需要將AndroidManifest.xml中聲明我們自定義的Application即可(MyApplication)。

除了聯網問題以外,其他的幾種情況都需要重新生成基準包。這裏再分享一個可以快速確定App是否有上傳過版本信息的方法:
在這裏插入圖片描述3、上傳成功
先驗證下上面的方法,當我把問題解決掉之後,把重新生成的基準包安裝到手機上打開(此時Bugly框架會上傳App的版本號、tinkerId到服務器),再查看"版本管理",出現了,版本號爲"1.0"(其實就是App的versionName)。
在這裏插入圖片描述
再回頭來看看上傳補丁,這次又會有什麼不同呢?
在這裏插入圖片描述
耶,成功。點擊"立即下發",可以看到現在補丁處於"下發中"狀態:
在這裏插入圖片描述
隨便來看看用戶手中的App是什麼反應吧(真正將補丁下發到用戶手機上的這段時間可能會有點久,不是立即下發的):
在這裏插入圖片描述
再回頭看看Bugly服務器上的補丁下發情況:
在這裏插入圖片描述


補丁管理

Bugly服務器除了可以上傳下發補丁外,還可以對補丁進行管理:
在這裏插入圖片描述

  • 停止下發:不再把該補丁下發到客戶手機上(停止後可重新開啓)。
  • 撤回:將Bugly服務器上的某個補丁刪掉,這個操作是不可逆的(不知道用戶手機上被成功打上的補丁是否也會被卸載)。
  • 編輯:可以修改"下發範圍"(開發設備、全量設備、備註等等)。
  • 歷史:查看修改記錄。

所有過程中需要注意的地方

1,一個基準包可以有多個補丁包,Bugly會將最新的補丁進行下發(舊補丁默認會變成"停止下發狀態"),客戶手機上的App的舊補丁會被新補丁覆蓋。
2,製作基礎包時,請使用帶"base"的tinkerId,執行的是assembleRelease指令。
3,製作基礎包後,一定要將baseApk、mapping.txt、R.txt保存好,不能弄丟了。
4,製作補丁包時,先將baseApkDir的值修改爲基準包所有文件夾的名字,然後啓用帶"patch"的tinkerId,同時修改"計數",執行的是buildTinkerPatchRelease指令。
5,製作補丁包後,最後打開它檢查YAPATCH.MF文件中的from和to信息,檢查該補丁包對應的基準包的tinkerId是否正確。
6,建議上線的基準包將Bugly的Log輸出關閉:Bugly.init(mContext, AppID, false);
7,如果是測試補丁包是否用效果,建議設置爲開發設備:Bugly.setIsDevelopmentDevice(mContext, true);
8,so文件需要手動先調用一下 TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI) 方法才能生效。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章