热修复——Bugly让热修复变得如此容易上手

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。


一、简述

我们平常开发项目肯定少不了会有一些问题会被忽略,比如一些文字设置错误,布局不对称等等,这是如果我们在重新打包让用户下载更新的话肯定是体验不好的,这时我们就用到热更新了。据我所知,市面上有3种Tinker的补丁管理系统,如下:

  • Bugly:热修复
  • GitHub:tinker-manager
  • tinkerpatch(Android 热更新服务平台)

「Bugly」和「tinker-manager」是免费的,「tinkerpatch」是收费的,因为「tinkerpatch」收费,所以暂时不做考虑。Bugly由腾讯团队开发并维护,稳定性肯定没得说,而「tinker-manager」是GitHub上个人开发者开发维护的,稳定性没法保证(我没有贬低开发者的意思,毕竟势单力薄,人多力量大嘛),故本人觉得,Bugly是目前最优的Tinker热修复解决方案。在开始进入Bugly集成之前,你可以先去bugly官网下载demo试试看看。

二、获取App ID

要使用Bugly的热修复功能,首先得注册并登录Bugly,然后点击进入「Bugly产品页面」,或点击“我的产品 ”。

填写必要的信息后,点击“保存”。

通过“产品设置”,选择刚刚创建的产品(图中第3步),可以查看到产品对应的App ID。

这个App ID很重要,先记录好,后续会用到。

三、添加项目依赖插件

项目的build.gradle:

dependencies {
    classpath 'com.android.tools.build:gradle:3.0.0'
    // tinkersupport插件(1.0.3以上无须再配置tinker插件)
    classpath "com.tencent.bugly:tinker-support:1.1.1"
}

四、集成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 {
    ...
    implementation "com.android.support:multidex:1.0.1" // 多dex配置
    implementation 'com.tencent.bugly:crashreport_upgrade:1.3.4'// 远程仓库集成方式(推荐)
}

签名配置部分请根据你项目的实际情况修改。

这里要注意一个问题:classpath里的版本一定要和app的build.gradle的sdk版本相对称,否则就会发生我第一次热更新成功,重新上传新补丁包,下载成功但是不会应用的问题。一定要注意!一定要注意!一定要注意! 亲测。

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)

lib文件下的so库要拷贝进来,不然会报java.lang.UnsatisfiedLinkError so库找不到错误

热修复——Bugly让热修复变得如此容易上手_第1张图片

 

五、配置Tinker

在app的build.gradle文件同级目录下创建一个tinker-support.gradle文件,内容如下:

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

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

/**
 * 此处填写每次构建生成的基准包目录
 */
def baseApkDir = "app-0821-08-18-11"

/**
 * 对于插件各参数的详细解析请参考
 */
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"

    // 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持)
    isProtectedApp = false

    // 是否开启反射Application模式  如果开启就需要用反射的application
    enableProxyApplication = false

    // 是否支持新增非export的Activity(注意:设置为true才能修改AndroidManifest文件)
    supportHotplugComponent = true

    // 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
     tinkerId = "base-1.0.1"  // 用于生成基准包(不用修改)
    // tinkerId = "patch-3.1.1" // 用于生成补丁包(每次生成补丁包都要修改一次,最好是     
    patch-${versionName}.x.x)

    // 构建多渠道补丁时使用
    // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"

}

/**
 * 一般来说,我们无需对下面的参数做任何的修改
 * 对于各参数的详细介绍请参考:
 * 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的分配
    }
}

 

1、overrideTinkerPatchConfiguration
当overrideTinkerPatchConfiguration = true时,tinkerPatch可以省略不写,Bugly会加载默认的Tinker配置。但请注意,如果你的so文件不是存放在libs目录下(与src目录同级),又或者资源文件的存放在你自定义的目录中,那么这时你要小心了,这些文件在制作补丁包时不会被检测,也就是说这些so文件和资源文件将不会被热修复,这种情况下就需要将overrideTinkerPatchConfiguration = false,并设置tinkerPatch的lib和res属性。

其它具体的配置与说明可以查看「Tinker-接入指南」。

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:

tinkerId = "base-1.0.1"  // 用于生成基准包(不用修改)

当生成补丁包时,请使用如下tinkerId:

tinkerId = "patch-3.1.1" // 用于生成补丁包(每次生成补丁包都要修改一次,最好是 patch-${versionName}.x.x)

对于同一个基准包,我们可能会多次生成补丁包上传到Bugly的热修复管理后台,这时,这些补丁包的tinkerId也要不一样,不然的话,当客户手机上的App在获取补丁时,会错乱(亲测,当同个基准包的补丁包的tinkerId一样时,App每次重启都会获取不同的补丁包,导致tinkerId相同的补丁包轮流下发),所以每发布一个补丁包都要修改thinkerId。

 

六、初始化SDK

Bugly的初始化工作需要在Application中完成,但对原生Tinker来说,默认的Application是无法实现热修复的。看过Tinker官方Wiki的人应该知道,Tinker针对Application无法热修复的问题,给予开发者两个选择,分别是:

  • 使用「继承TinkerApplication + DefaultApplicationLike」。
  • 使用「DefaultLifeCycle注解 + DefaultApplicationLike」。

这2种选择都需要对自定义的Application进行改造,对于自定义Application代码不多的情况来说还可以接受,但有些情况还是比较”讨厌”这2种选择的,对此,Bugly给出了它的2种解决方法,分别如下:

  • 使用原来的自定义Application,Bugly通过反射为App动态生成新的Application。
  • 使用「继承TinkerApplication + DefaultApplicationLike」。

分别对应tinker-support.gradle文件中enableProxyApplication的值:true或false。

 

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

接着就是创建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();
    }
}

注意:tinker需要你开启MultiDex,你需要在dependencies中进行配置compile "com.android.support:multidex:1.0.1"才可以使用MultiDex.install方法; SampleApplicationLike这个类是Application的代理类,以前所有在Application的实现必须要全部拷贝到这里,在onCreate方法调用SDK的初始化方法,在onBaseContextAttached中调用Beta.installTinker(this);

最后在清单文件中,声明改造好的Application(注意不是ApplicationLike):

注意:这里如果你补丁下发成功并且应用成功 点击网络请求报错的话,就需要把在原本application里初始化的网络请求放到ApplicationLike的onCreate方法里,并且在bugly的init方法之前,不然会报java.lang.ExceptionInInitializerError错误,这是亲测发现的,亲测,亲测!如下图:

热修复——Bugly让热修复变得如此容易上手_第2张图片

enableProxyApplication = true 的情况

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();
    }

}

注:无须你改造Application,主要是为了降低接入成本,我们插件会动态替换AndroidMinifest文件中的Application为我们定义好用于反射真实Application的类(需要您接入SDK 1.2.2版本 和 插件版本 1.0.3以上)。

配置Bugly

这是Bugly官方给出的配置,应有尽有,注释也很nice,请仔细看看,对项目的功能拓展与用户体验有帮助:

 // 设置是否开启热更新能力,默认为true
        Beta.enableHotfix = true;
        // 设置是否自动下载补丁,默认为true
        Beta.canAutoDownloadPatch = true;
        // 设置是否自动合成补丁,默认为true
        Beta.canAutoPatch = true;
        // 设置是否提示用户重启,默认为false
        Beta.canNotifyUserRestart = true;

        // 补丁回调接口   改回调写在init之后不生效
        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) {
//                Beta.applyDownloadedPatch();//用户手动合成补丁
                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() {
//                Toast.makeText(getApplication(), "补丁回滚", Toast.LENGTH_SHORT).show();
            }
        };

        // 设置开发设备,默认为false,上传补丁如果下发范围指定为“开发设备”,需要调用此接口来标识开发设备
        Bugly.setIsDevelopmentDevice(mContext, false);
        // 多渠道需求塞入
        // String channel = WalleChannelReader.getChannel(getApplication());
        // Bugly.setAppChannel(getApplication(), channel);
        // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
        Bugly.init(mContext, "bd4f53b14f", true);

 

 

bugly.init方法后面的boolean是决定是否输入log的,切记在正式发布的时候设置为false关闭。

更详细的配置项参考tinker-support配置说明

六、AndroidManifest.xml

1、 权限配置








2、Activity配置


3、FileProvider配置


    

如果你使用的第三方库也配置了同样的FileProvider, 可以通过继承FileProvider类来解决合并冲突的问题,不过这个代码utils.BuglyFileProvider是官方给的,具体provider也没有给,需要自己写。示例如下:


    

这里要注意一下,FileProvider类是在support-v4包中的,检查你的工程是否引入该类库。

在res目录新建xml文件夹,创建provider_paths.xml文件如下:



    
    
    
    

这里配置的两个外部存储路径是升级SDK下载的文件可能存在的路径,一定要按照上面格式配置,不然可能会出现错误。

注:1.3.1及以上版本,可以不用进行以上配置,aar已经在AndroidManifest配置了,并且包含了对应的资源文件。

七:混淆配置

为了避免混淆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.**{*;}

好了,集成完毕,接下来就是制作基准包、补丁包和上传补丁包了。

 

八:补丁打包

1.制作基准包

配置基准包的tinkerId 把thinker_support.gradle文件里的base的thinkerid打开,把patch关闭,如下图:

热修复——Bugly让热修复变得如此容易上手_第3张图片

tinkerId最好是一个唯一标识,例如git版本号、versionName等等。 如果你要测试热更新,你需要对基线版本进行联网上报。联网上报(意思是指基准包已经安装并且联网使用,如果已经安装呢么bugly便会上报服务器,通知可以添加补丁包)。

这里强调一下,基线版本配置一个唯一的tinkerId,而这个基线版本能够应用补丁的前提是集成过热更新SDK,并启动上报过联网,这样我们后台会将这个tinkerId对应到一个目标版本,例如tinkerId = "bugly_1.0.0" 对应了一个目标版本是1.0.0,基于这个版本打的补丁包就能匹配到目标版本。

执行assembleRelease编译生成基准包:

热修复——Bugly让热修复变得如此容易上手_第4张图片

这个会在build/outputs/bakApk路径下生成每次编译的基准包、混淆配置文件、资源Id文件,如下图所示:

热修复——Bugly让热修复变得如此容易上手_第5张图片

 

实际应用中,请注意保存线上发布版本的基准apk包、mapping文件、R.txt文件,如果线上版本有bug,就可以借助我们tinker-support插件进行补丁包的生成

注意:如果你的bakApk文件夹下少了一个mapping.txt文件,不要慌这个不是必要的,这个文件只有你在混淆文件里加了混淆才会生成。

2.制作补丁包

修改你需要修改的bug之后:

热修复——Bugly让热修复变得如此容易上手_第6张图片

如果你要生成不同编译环境的补丁包,只需要执行TinkerSupport插件生成的task,比如buildTinkerPatchRelease就能生成release编译环境的补丁包。 注:TinkerSupport插件版本低于1.0.4的,需要使用tinkerPatchRelease来生成补丁包 

生成的补丁包在build/outputs/patch目录下:

热修复——Bugly让热修复变得如此容易上手_第7张图片

九、上传补丁包

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,这时你重启之后,bug自然就解决啦,哈哈哈...

十、其他

1、补丁管理
Bugly服务器除了可以上传下发补丁外,还可以对补丁进行管理:

  • 停止下发:不再把该补丁下发到客户手机上(停止后可重新开启)。
  • 撤回:将Bugly服务器上的某个补丁删掉,这个操作是不可逆的(不知道用户手机上被成功打上的补丁是否也会被卸载)。
  • 编辑:可以修改”下发范围”(开发设备、全量设备、备注等等)。
  • 历史:查看修改记录。

2、强调一下一些需要注意的地方

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

3、Bugly官方文档

  • Bugly Android热更新使用指南
  • Bugly Android热更新详解
  • Bugly Android 热更新常见问题
  • 热更新API接口
  • Bugly多渠道热更新解决方案
     

 

你可能感兴趣的:(热修复,android热修复,thinker,bugly,补丁)