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) 方法才能生效。

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