框架安卓之美Android学习

Android热修复工具Tinker集成

2017-04-16  本文已影响1403人  HolenZhou

Tinker介绍

Tinker是微信团队开源的Android热修复工具,支持dex, library和resources的热更新。关于Tinker的基本的接入方法、Api和原理等,在官方wiki中有非常详细的介绍。我这里重点描述一下基于我们项目的接入流程(客户端和后台),使用姿势和遇到的问题,以及如何在Jenkins上构建补丁包。

Tinker接入

Tinker是目前热修复方案中稳定性和兼容性最好的,毕竟源于微信团队嘛!但也正是为了提高稳定性和兼容性,Tinker在接入成本上做了妥协,它不像以往的Andfix那样可以一键接入,必须改造自己的Application,详细可参考自定义Application类。其实改造的过程也并不复杂,只是多了一点学习成本。
但使用对Tinker进行再次封装的第三方平台的SDK还是可以实现一键接入的,比如TinkerPatch平台Bugly热更新功能,但这种方式对Application进行了反射,是有风险的:

TinkerPatch 平台通过自动反射 Application,可以实现无缝接入。事实上,对于反射失败的情况,我们会自动回退到代理 Application 生命周期模式,防止因为反射失败而造成应用无法启动的问题。
通过线上统计,大约有 1/1W的反射失败率。我们更加推荐大家使用 Tinker 的方式改造自身的 Application, 使兼容性高。

而且我们需要自己搭建后台来管理补丁包,所以不会使用第三方SDK,而是自己封装了一套SDK,其实就是将Tinker的调用API和与后台接口的通信功能进行了整合而已,封装方式和后台搭建也是基于github上的一个开源项目的:https://github.com/baidao/tinker-manager

我们的客户端SDK已经放在了公司内部的Maven仓库中:compile 'com.****.tinkerutils:utils:${version}'
我们已经搭建好的补丁管理平台测试地址是:http://172.22.34.201/hotfix-console/

开始接入
gradle是Tinker推荐的接入方式,如果要使用命令行接入请参考这里
第一步,引入Tinker插件和依赖
添加tinker-gradle-plugin到工程根目录下的build.gradle的dependencies中:

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')
    }
}

然后在你的主module中的build.gradle文件里apply插件:

apply plugin: 'com.tencent.tinker.patch'

注意:这里的主module是指应用的启动Application所在的module,即含有apply plugin: 'com.android.application'这句话的module,对应我们项目就是MyMoney,否则Tinker会抛出Exception

然后加入Tinker的lib依赖:

dependencies {
    //optional, help to generate the final application
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker's main Android lib
    compile('com.tencent.tinker:tinker-android-lib:1.7.7') 
}

但因为我们使用自己的SDK,SDK中已经有了Tinker lib的依赖,所以,我们加入SDK的依赖即可:

dependencies {
    //optional, help to generate the final application
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //our tinker SDK
    compile 'com.****.tinkerutils:utils:${version}'
}

第二步,改造项目原有的Application
将我们现有的AppApplication直接继承Tinker提供的DefaultApplicationLike类,参考自定义Application类,这样我们的AppApplication就成了真实的Application(RealApplication,自定义或者通过注解自动生成)的代理类,这样做就是为了将RealApplication隔离起来,防止误修改,如此一来,在RealApplication中所做的所有初始化工作也就相当于转移到了代理类中,间接实现了Application可修改进行热修复的目的。
比如我们原来的AppApplication如下:

public class AppApplication extends Application {

    private static final String TAG = "AppApplication";

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            MultiDex.install(base);
            context = this;
        }catch (Exception e){
            DebugUtil.exception(TAG,e);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 全局初始化代码
        ... ...
    }

    // 复写了Application的方法
    @Override
    public Resources getResources() {
        Resources res = super.getResources();
        if(res.getConfiguration().fontScale != 1){
            Configuration newConfig = res.getConfiguration();
            newConfig.fontScale = 1;
            res.updateConfiguration(newConfig, res.getDisplayMetrics());
        }
        return res;
    }

    @Override
    public void startActivities(Intent[] intents) {
        // do some option
        ... ...
        super.startActivities(intents);
    }
}

那我们改造后应该是这样:

@DefaultLifeCycle(application = "${yourpackage}.RealApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL)
public class AppApplication extends DefaultApplicationLike {

    private static final String TAG = "AppApplication";

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

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        try {
            //you must install multiDex whatever tinker is installed!
            MultiDex.install(base);
            //此处通过getApplication()拿到的其实就是RealApplication
            context = getApplication();
            //install tinker
            TinkerUtils.installTinker(getApplication(), this);
        }catch (Exception e){
            DebugUtil.exception(TAG,e);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 设置tinker参数并向后台请求补丁包
        TinkerUtils.setUpTinker(context);
        // 全局初始化代码
        ... ...
    }

    @Override
    public Resources getResources(Resources res) {
        if(res.getConfiguration().fontScale != 1){
            Configuration newConfig = res.getConfiguration();
            newConfig.fontScale = 1;
            res.updateConfiguration(newConfig, res.getDisplayMetrics());
        }
        return res;
    }
    public Resources getResources() {
        return getApplication().getResources();
    }
}

上面是通过注解的方式来自动生成RealApplication,如果使用自定义的方式,则直接新建RealApplication类继承TinkerApplication并创建对应构造方法即可,不用注解,也不用引入注解依赖。

public class RealApplication extends TinkerApplication {
    public RealApplication() {
      super(
        //tinkerFlags, tinker支持的类型,dex,library,还是全部都支持!
        ShareConstants.TINKER_ENABLE_ALL,
        //ApplicationLike的实现类,只能传递字符串 
        "tinker.sample.android.app.SampleApplicationLike",
        //Tinker的加载器,一般来说用默认的即可
        "com.tencent.tinker.loader.TinkerLoader",
        //tinkerLoadVerifyFlag, 运行加载时是否校验dex,lib与res的Md5
        false);
    }  
}

官方提示:除了构造方法之外,你最好不要引入其他的类,这将导致它们无法通过补丁修改。

注意:改造完成后要用RealApplication替换掉AndroidManifest.xml中原来的AppApplication:

    <application
        android:name=".RealApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        ... ...

另外因为我们的SDK中有自定义AbstractResultService类,即TinkerResultService,所以也需要在清单文件中加上它,否则补丁合成会出问题

     <service
            android:name="com.feidee.tinkerutils.TinkerResultService"
            android:exported="false"/>

参考TinkerApplication源码可以知道为什么如此修改Application:

    ... ...
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
        onBaseContextAttached(base);
    }

    private void onBaseContextAttached(Context base) {
        applicationStartElapsedTime = SystemClock.elapsedRealtime();
        applicationStartMillisTime = System.currentTimeMillis();
        loadTinker();
        ensureDelegate();
        //此处的applicationLike现在就是我们的AppApplication类
        applicationLike.onBaseContextAttached(base);
        //reset save mode
        if (useSafeMode) {
            String processName = ShareTinkerInternals.getProcessName(this);
            String preferName = ShareConstants.TINKER_OWN_PREFERENCE_CONFIG + processName;
            SharedPreferences sp = getSharedPreferences(preferName, Context.MODE_PRIVATE);
            sp.edit().putInt(ShareConstants.TINKER_SAFE_MODE_COUNT, 0).commit();
        }
    }
    ... ...

    @Override
    public void onCreate() {
        super.onCreate();
        ensureDelegate();
        //此处的applicationLike现在就是我们的AppApplication类
        applicationLike.onCreate();
    }
    ... ...

    @Override
    public Resources getResources() {
        Resources resources = super.getResources();
        if (applicationLike != null) {
            //此处的applicationLike现在就是我们的AppApplication类
            return applicationLike.getResources(resources);
        }
        return resources;
    }
    ... ...

改造原来AppApplication复写的startActivities方法时,我发现DefaultApplicationLike类中并没有类似getResources的代理方法,所以我只有将这个复写放到了RealApplication中。

第三步,增加Tinker的gradle配置
Tinker的gradle参数配置很灵活,具体的参数设置事例可参考官方sample中的app/build.gradle,为了使gradle文件不至于太混杂,我将Tinker相关的配置单独抽取出来放在新建的tinker_support.gradle文件中,然后在MyMoney/build.gradle文件中加入下面一行即可:

// tinker config
apply from: 'tinker_support.gradle'

为了更方便的构建,主要修改了以下配置参数:

//基准apk包的备份路径,这里仅作备份用,每次构建apk时会将生成的apk文件、mapping和R文件自动拷贝一份到这个目录下去
def bakPath = file("${buildDir}/bakApk/")
//构建补丁时获取基准apk包的文件名
def getTinkerBaseApkFileName(def defaultName) {
    return hasProperty("TINKER_BASE_APK_NAME") ? TINKER_BASE_APK_NAME : defaultName
}
/**
 * you can use assembleRelease to build you base apk
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * add apk from the build/bakApk
 */
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = isRelease();
    tinkerBaseApkFileName = getTinkerBaseApkFileName("Mymoney_base.apk")// todo 构建时需要在此配置基准包的filename
    //proguard mapping file to build patch apk
    tinkerMappingFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerSymbolFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-R.txt"
}
/**
 * mapping 文件 路径取得是 rootDir/tinker/mapping/
 * @return
 */
def getMappingFilePath() {
//    String baseMappingPath = project.projectDir.toString() + "/document/mapping/"
    String baseMappingPath = "${rootDir}/tinker/mapping/"
    String tailPath = ext.tinkerMappingFileName
    String middlePath = ""
    if (hasProperty("channelCode")) {
        middlePath = channelCode + "/"
    }
    return baseMappingPath + middlePath + tailPath
}
/**
 * R 文件 路径取得是 rootDir/tinker/symbol/
 */
def getSymbolFilePath() {
    String baseSymbolPath = "${rootDir}/tinker/symbol/"
    String tailSymbolPath = ext.tinkerSymbolFileName
    String middleSymbolPath = ""
    if (hasProperty("channelCode")) {
        middleSymbolPath = channelCode + "/"
    }
    return baseSymbolPath + middleSymbolPath + tailSymbolPath
}
/**
 * 基准apk文件  rootDir/tinker/apk/
 * @return
 */
def getBaseApkFilePath() {
    String baseApkPath = "${rootDir}/tinker/apk/"
    String tailApkPath = ext.tinkerBaseApkFileName
    String middleApkPath = ""
    if (hasProperty("channelCode")) {
        middleApkPath = channelCode + "/"
    }
    return baseApkPath + middleApkPath + tailApkPath
}

// tinkerId是唯一标识,这里默认指定为apk的版本号
def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : ext.apkVersionName
... ...
//如果在本地目录下找不到基准apk,就去我们放基准包的地方去下载
def downloadBaseApkFile(def address, def savePath) {
    new File(savePath).withOutputStream { out ->
        out << new URL(address).openStream()
    }
}

以上更改把基准包的获取路径改到了根目录下的tinker目录中,把tinkerEnabled的值付给isRelease()函数,这样在实际应用发布版本时,我们手动在创建一个tinker目录及其相应文件夹(只需创建一次即可),然后将我们构建好的apk、相应的mapping和R文件(需改名为${tinkerBaseApkFileName}-mapping.txt和${tinkerBaseApkFileName}-R.txt),这样做的好处时基准包可以放在本地不被clean掉,方便在本地手动构建时进行管理。

第四步,安装和初始化Tinker
上面改造后的AppApplication中有两行代码用于Tinker的安装和初始化:

//install tinker 
TinkerUtils.installTinker(getApplication(), this);
// 设置tinker参数并向后台请求补丁包
TinkerUtils.setUpTinker(context);

TinkerUtils代码如下:

public class TinkerUtils {
    private static final String TAG = "Tinker";
    public static void installTinker(Context context, ApplicationLike applicationLike) {
        // 安装tinker
        SampleTinkerManager.initCurrentChannelValue(ChannelUtil.getChannel());
        SampleTinkerManager.setTinkerApplicationLike(applicationLike);
        SampleTinkerManager.initFastCrashProtect();
        //should set before com.dx168.patchsdk.sample.tinker is installed
        SampleTinkerManager.setUpgradeRetryEnable(true);
        //installTinker after load multiDex
        //or you can put com.tencent.com.dx168.patchsdk.sample.tinker.** to main dex
        SampleTinkerManager.installTinker(applicationLike);
        Tinker.with(context);
        //使用Hack的方式,如果补丁中有so库 那么直接加载补丁中的armeabi下的so库(将tinker library中的armeabi注册到系统的library path中。)
        TinkerLoadLibrary.installNavitveLibraryABI(context, "armeabi");
    }
    public static void setUpTinker(Context context) {
        if (ChannelUtil.isGoogleVersion()) {
            return;
        }
        //在补丁管理后台注册的id和key,参数值配置在gradle文件中
        String appId = BuildConfig.TINKER_APP_ID;
        String appSecret = BuildConfig.TINKER_APP_SECRET;
        String tinkerUrl = BuildConfig.TINKER_PATCH_URL;
        PatchManager.getInstance().init(context, tinkerUrl, appId, appSecret, new ActualPatchManager() {
            @Override
            public void cleanPatch(Context context) {
                TinkerInstaller.cleanPatch(context);
                DebugUtil.debug(TAG, "local patch sdk >>>>> cleanPatch");
            }
            @Override
            public void applyPatch(Context context, String patchPath) {
                TinkerInstaller.onReceiveUpgradePatch(context, patchPath);
                DebugUtil.debug(TAG, "local patch sdk >>>>> applyPatch: " + patchPath);
            }
        });
        PatchManager.getInstance().setTag(ChannelUtil.getChannel());//可用于灰度发布
        PatchManager.getInstance().setChannel(ChannelUtil.getChannel());
        PatchManager.getInstance().queryAndApplyPatch(new PatchListener() {
            @Override
            public void onQuerySuccess(String response) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onQuerySuccess response={ignore in log}");
            }
            @Override
            public void onQueryFailure(Throwable e) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onQueryFailure e=" + Log.getStackTraceString(e));
            }
            @Override
            public void onDownloadSuccess(String path) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onDownloadSuccess path=" + path);
            }
            @Override
            public void onDownloadFailure(Throwable e) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onDownloadFailure e=" + Log.getStackTraceString(e));
            }
            @Override
            public void onApplySuccess() {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onApplySuccess");
            }
            @Override
            public void onApplyFailure(String msg) {
                DebugUtil.debug(TAG, "local patch sdk >>>>> onApplyFailure msg=" + msg);
            }
            @Override
            public void onCompleted() {
                DebugUtil.debug(TAG, "local patch sdk >>>>> onCompleted");
            }
        });
    }
}

构建补丁包

如上个步骤所说,构建好基准包后,将apk、mapping和R文件改好名字后放在tinker相应目录中,就可以开始构建补丁包了。打包方式:

直接使用task:tinkerPatchVariantName(例如tinkerPatchDebug、tinkerPatchRelease)即可自动根据Variant选择相应的编译类型,同时它还贴心的为我们完成以下几个操作:
1.将TINKER_ID自动插入AndroidManifest的meta项,输出路径为build/intermediates/tinker_intermediates/AndroidManifest.xml;
2.如果minifyEnabled为true,将自动将Tinker的proguard规则添加到proguardFiles中,输出路径为build/intermediates/tinker_intermediates/tinker_proguard.pro,这里你不需要将它们拷贝到自己的proguard配置文件中;
3.如果multiDexEnabled为true,将自动生成Tinker需要放在主dex的keep规则。在tinker 1.7.6版本之前,你需要手动将生成规则拷贝到自己的multiDexKeepProguard文件中。例如Sample中的multiDexKeepProguard file("keep_in_main_dex.txt")。在1.7.6版本之后,这里会通过脚本自动处理,无须手动填写。
4.把dexOptions的jumboMode打开。

我们构建Release包时,直接执行下面命令即可:

./gradlew tinkerPatchRelease

构建很快,输出目录为build/outputs/tinkerPatch/release,会产生两个带签名的apk格式的补丁patch_signed.apkpatch_signed_7zip.apk,构建log会提示我们哪个补丁更小并建议我们使用小的。更改代码和资源文件造成的改动量会影响补丁包的大小,只改一行代码的情况下,补丁包大约为4k。

测试

测试主要从以下几个方面进行:

在后台创建好app,拿到对应的key配置到项目中,并创建对应基准apk的版本,上传对应版本的补丁包(后台会自动改名,所以下发的补丁包不会包含.apk的后缀名),选择是否灰度等,即可下发补丁。

补丁管理后台

Debug打印日志可以看到补丁的拉取和合成过程


补丁下载及合成过程

Jenkins构建支持

因为构建补丁包时,有三个变量,即基准apk、mapping和R文件,所以我们可以使用Jenkins提供的参数化构建。

构建命令如下,配置TINKER_BASE_APK_NAME为Mymoney_base.apk

修改构建的输出目录为:

MyMoney/build/outputs/tinkerPatch/release/patch_signed_7zip.apk,MyMoney/build/outputs/tinkerPatch/release/patch_signed.apk

在开始构建之前,我们需要上传相应文件来设置我们添加的三个文件参数,因为Jenkins会将我们设置好的文件参数指向我们上传的文件,而参数名称已经根据TINKER_BASE_APK_NAME写死而且符合规范,所以我们每次构建都无需对项目配置和Jenkins配置做任何修改,只需要上传对应基准文件即可(也无需修改文件名了)。


构建完成后就可以生成相应的补丁包:

遇到的问题

  1. 在Debug构建测试Tinker时,会出现不能断点调试的情况,这是因为我在测试时Debug模式将minifyEnabled设置为了true,所以无法在断点时识别代码。

  2. 在Release模式时,将tinkerEnabled设置为false,会报找不到Application的错误:



    原因也是开启了混淆,不过官方的demo也一样有这个问题。鉴于在Release的情况下,似乎不会将Tinker关闭,可忽略这个问题。

  3. 提示有png被修改,但是其实没改过。wiki中有提到这个问题,除了将cruncherEnabled关闭外,可能的原因是使用Run的方式构建了apk。

  4. 集成Tinker后第一次启动app崩溃,并且不打印任何错误堆栈。原来以为是分包的问题,经过多次测试,发现应该是在Tinker安装之前进行了多余的操作,另外Tinker的依赖最好放在启动Application所在的module中,因为Tinker的安装和构建都依赖Application。如果将其依赖放在其他lib库所在的module中,可能引起未知crash。

其他可能遇到的问题参考wiki:常见问题

扩展

上一篇下一篇

猜你喜欢

热点阅读