来自微信的地精修补匠--Tinker

前言

最近公司新上一个项目,天使用户试用阶段每天各种各样的需求要让改。改一次发一个版本改一次发一个版本简直苦不堪言,大多数时候只是界面上的调整改动,交易结果的排版这些问题。毕竟太高频次又没有多大实质变化的更新让人很反感,因此得想一些改进办法。于是决定接入热更新技术,小问题采用发补丁包的方式。定期发布版本对改动进行整体迭代。Android热更新技术已经有很多框架可以直接使用了,至于怎么选择见仁见智,选择自己项目适合的方式。本文主要记录一下我自己项目接入微信的Tinker热修复框架的过程。

使用之前一定仔细阅读Wiki文档和sample中的代码!!!
Tinker Github地址

快速接入过程

一、引入依赖
参考Tinker-接入指南
项目的build.gradle中添加

    repositories {
    //mavenLocal要特别注意别漏了,否则更改资源文件会出空指针异常
        mavenLocal()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.1'
        //集成Tinker,热修复
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.0')
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
    allprojects {
        repositories {
            mavenLocal()
            jcenter()
        }
    }

APP的build.gradle中添加

    dependencies {
        //可选,用于生成application类,(版本号换成最新的发布版)
        compile('com.tencent.tinker:tinker-android-anno:1.7.0')
        //tinker的核心库
        compile('com.tencent.tinker:tinker-android-lib:1.7.0') 
    }
    ...
    ...
    //apply tinker插件
    apply plugin: 'com.tencent.tinker.patch'
}

二、其他配置
比较快速的接入方式是直接对比自己app的build.gradle 文件和sample项目的build.gradle文件将自己的配置和tinker需要的配置合并。修改defaultConfig中的applicationId为自己的包名,注意signConfig中的签名文件路径(Gradle打包签名用的)。修改tinkerPatch中loader数组中的Application为自己的Application。
build.gradle中各个配置的解释wiki文档中都有说明
这里我说一说我在接入时遇到的几个问题(文档里面也都有说明,但不仔细看文档又一定会遇到的问题)

  • 1.tinkerId的问题
TinkerId报错

如果项目没有初始化git并commit,官方sample如果不是采用git clone的方式获取也会有这个问题。因为sample配置中

    tinkerId = getTinkerIdValue()
    ...
    ...
    def getTinkerIdValue() {
        return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
    }

是通过git的版本sha来设置tinkerId的。
解决办法:①直接设置一个任意的字符串②初始化git并提交一次。为了避免补丁混乱,每个基础包发布版本(不是每一次补丁包发布)都对应一个唯一的tinkerId,打补丁时补丁和基础包根据tinkerId关联。可采用发布版的git版本号或者versionName来作为tinkerId。我自己是试用的版本号来作为tinkerId的这样一个版本对应的是一个id

//修改tinkerid的值
def getTinkerIdValue() {
    return android.defaultConfig.versionName
}
  • 2.keep_in_main_dex.txt 找不到的问题
    /**
    * not like proguard, multiDexKeepProguard is not a list, so we can't just
    * add for you in our task. you can copy tinker keep rules at
    * build/intermediates/tinker_intermediates/tinker_multidexkeep.pro
    */
    multiDexKeepProguard file("keep_in_main_dex.txt")

sample中copy过来的配置中有上面这么一条,在sample中的对应目也有这个文件。里面是一些keep规则,复制txt文件到自己项目对应目录即可

  • 3.需要注意的是tinker插件需要读写文件的权限,Android6.0之下mianfest中配置即可,6.0之后还需Java代码中申请权限。
    
    
  • 4.bapApk目录下不能生成mapping文件的问题

a.

    /**
    * task type, you want to bak
    * taskName与编译选择的方式要一致
    */
    def taskName = "release"
    

b.直接使用AndroidStudio菜单栏build打包也会出现这种问题,这里使用gradle命令打包的方式。

Gradle打包步骤

点击AndroidStudio的右边栏打开Gradle命令菜单展开build接点如图,根据配置的taskName选择debug方式还是release方式打包,release方式即为要发布的打包版本。如需打包签名的apk则需要配置app的gradle.build中的签名文件信息

    /**
     * Gradle打包签名配置
     */
    signingConfigs {
        release {
            storeFile file('E:/PandaQ/tinkerDemo/test.jks')
            keyAlias 'tinker测试'
            keyPassword 'tinkertest'
            storePassword 'tinkertest'
        }
        debug {
            storeFile file('C:/Users/PandaQ/.android/debug.keystore')
        }
    }

配置后运行gradle打包命令就会按上面的签名文件对应用进行打包,但直接将签名文件密码放在配置中不安全,可以设置在打包过程中输入。具体方式可以看这里

  • 5.clean或者rebuild工程后bakApk文件夹被清除问题
    由于打包补丁的时候需要配置基础包:
    /**
 * 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 = true
    //you should bak the following files
    //old apk file to build patch apk
    //app-debug-1017-11-29-32.apk
    tinkerOldApkPath = "${bakPath}/app-release-1024-16-46-49.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-1024-16-46-49-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-1024-16-46-49-R.txt"
}

默认的输出目录为bakApk目录,但是每次clean或者rebuild工程时这个文件夹都会被清理,因此需要手动将发布出去的基础包和对应的mapping.txt及R.txt备份。或者修改Gradle配置中的输出目录。
三、Application类
参考Tinker-自定义扩展

官方文档截图

Tinker的接入文档中已经对Application类做了说明,为了使真正的Application能被修改将Application中的所有逻辑都移动到ApplicationLike代理类中来。Application继承TinkerApplication并只做一个super的空实现,或者通过在ApplicationLike代理中添加注解直接动态生成Application类,防止不小心在里面写了其他代码。

@DefaultLifeCycle(
        application = "com.seuic.seuickp.KPApplication",             //application name to generate
        flags = ShareConstants.TINKER_ENABLE_ALL)
public class KPApplicationLike extends DefaultApplicationLike {

    public static Context mContext;
    public static ImageLoader mImageLoader;
    public static DisplayImageOptions sOptions;

    public KPApplicationLike(Application application, int tinkerFlags, 
    boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime,Intent tinkerResultIntent, Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,tinkerResultIntent, resources, classLoader, assetManager);
    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        MultiDex.install(base);
        TinkerManager.setTinkerApplicationLike(this);
        TinkerManager.installTinker(this);
    }

    public static ImageLoader getmImageLoader() {
        return mImageLoader;
    }

    public static DisplayImageOptions getOptions() {
        return sOptions;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = getApplication().getApplicationContext();
        DBTools.init(mContext);
        mImageLoader = ImageLoader.getInstance();
        ImageLoaderConfiguration config = new ImageLoaderConfiguration
                .Builder(mContext)
                .threadPoolSize(4)
                .imageDownloader(new AuthImageDownloader(getApplication().getApplicationContext()))
                .build();
        mImageLoader.init(config);
        sOptions = new DisplayImageOptions
                .Builder()
                .showImageForEmptyUri(R.drawable.loading_pic)
                .showImageOnFail(R.drawable.loading_pic)
                .showImageOnLoading(R.drawable.loading_pic)
                .cacheInMemory(true)
                .cacheOnDisk(true)
//                .displayer(new RoundedBitmapDisplayer(10))
                .build();
        //初始化腾讯Bugly
        CrashReport.initCrashReport(getApplication().getApplicationContext(), "900053571", false);
    }

    public static Context getContext() {
        return mContext;
    }

    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }
}

上面是我接入tinker的一个小项目的Application,将Application的oncreate()方法直接移入ApplicationLike中,在onBaseContextAttached()方法中对tinker进行初始化操作。然后将所有关于Application的引用换成ApplicationLike.getApplication()即可

完成上述操作,配置部分就已经基本完成,接下来就可以测试一下打包更新了。

打包测试

根据配置文件在Gradle的build中选择合适的打包方式(上面图中有标注),打包后未修改默认输出路径的情况下会在工程app->build目录下生产bakAPK文件夹和outputs文件夹。

注意bakapk和outputs
  • a 备份bakAPK中的三个文件,很重要!生成补丁包的时候需要的
  • b outputs-->abp-->app-release.apk为需要发布的版本
  • c 打包发布版补丁:将bakApk中的文件配置到build.gradle中
    /**
 * 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 = true
    //you should bak the following files
    //old apk file to build patch apk
    //app-debug-1017-11-29-32.apk
    tinkerOldApkPath = "${bakPath}/app-debug-1024-20-00-21.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-1024-20-02-47-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-1024-20-00-21-R.txt"
}

只修改了方法的情况下只需配置OldApkPath即可,修改了资源文件时还需要配置mapping.txt和R.txt

  • d 选择Gradle-->tinker-->tinkerPatchRelease/Debug打包生成补丁,打包后会在outputs中生成tinkerPatch文件夹,将生成的patch_signed.apk或者patch_signed_7zip.apk(选择小的)推送到手机的文件系统中(文件名可以不以apk结尾)。
  • e 在服务中或者通过事件调用加载补丁
        loadPatchButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/seuic/kp/patch_signed_7zip.apk");
            }
        });

默认情况下初始化tinker有两种方式查看TinkerInstaller类的install()方法可以发现未指定ResultService时使用DefaultResultService,加载成功后会自动结束进程应用直接关闭。显然不太友好,因此我们可以继承DefaultResultService然后重写他的onPatchResult()方法,来达到在加载完成补丁后进行的操作(弹个对话框让用户重启,因为补丁加载后需进程重启才有效果)。

  • f 一个基础包可以打多次补丁,打不定时配置中的oldApk不变始终为基础包,不能使用前面的补丁包作为后面补丁的oldApk。

上面就是接入tinker热更新的全部步骤。需要更多的场景或者接入按上面方式不适用可以去研究一下tinker的源码和仔细看一看github上的wiki文档跟常见问题一般都能解决

Tinker仓库地址

你可能感兴趣的:(来自微信的地精修补匠--Tinker)