美文网首页
Tinker基本使用

Tinker基本使用

作者: 小楠总 | 来源:发表于2017-12-01 09:57 被阅读237次

    Tinker简介与核心原理

    之前的文章中,我们学会了使用AndFix进行线上BUG的热修复。但是有一些BUG可能是因为资源文件、配置文件等非方法引起的BUG的时候,AndFix就无能为力了。因此这里有必要介绍Tinker。

    关于什么是TInker,Tinker的官方文档里面有一句话:Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。

    关于Tinker的优势,除了有微信的大量用户认可之外(兼容性好),在下面的一张图里面也可以看到Tinker的功能强大:

    image.png

    有关更多的介绍,请参考官方文档,这里不再赘述:

    https://github.com/Tencent/tinker/wiki

    下面简单介绍一下Tinker的核心原理:

    1. 基于Android原生的类加载器,研发了自己的类加载器,用来加载Patch文件中的字节码文件。并且通过AssetManager来加载Patch文件中的资源。
    2. 基于Android原生的AAPT,研发了自己的AAPT
    3. 基于于Android的Dex文件格式,研发了自己的一套DexDiff算法

    Tinker基本接入

    鉴于Tinker官方文档的晦涩难懂,我们在这里做一个简单的介绍。

    首先在app的gradle中引入tinker的核心库:

    //可选,用于生成application类
    //provided "com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}"
    
    //tinker的核心库
    compile "com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"
    

    其中需要注意的是TINKER_VERSION是在gradle.properties中配置的参数:

    TINKER_VERSION=1.9.0
    

    然后,我们封装一个管理类:

    public class TinkerManager {
    
        private static TinkerManager sInstance;
        private ApplicationLike mApplicationLike;
        private boolean mIsInstall = false;
        private static CustomPatchListener sCustomPatchListener;
    
        private TinkerManager() {
        }
    
        public static TinkerManager getInstance() {
            if (sInstance == null) {
                synchronized (TinkerManager.class) {
                    if (sInstance == null) {
                        sInstance = new TinkerManager();
                    }
                }
            }
            return sInstance;
        }
    
        public void install(ApplicationLike applicationLike) {
            if (!mIsInstall) {
                mApplicationLike = applicationLike;
                sCustomPatchListener = new CustomPatchListener(getApplication());
                TinkerInstaller.install(mApplicationLike);
    
                mIsInstall = true;
            }
        }
    
        public void addPatch(String path, String md5) {
            if (Tinker.isTinkerInstalled()) {
                sCustomPatchListener.setCurrentMD5(md5);
                TinkerInstaller.onReceiveUpgradePatch(getApplication(), path);
            }
        }
    
        private Context getApplication() {
            if (mApplicationLike != null) {
                return mApplicationLike.getApplication().getApplicationContext();
            }
            return null;
        }
    
    }
    

    其中:

    1. install方法中主要通过调用TinkerInstaller的方法进行初始化。有关ApplicationLike的知识在下面进行介绍。
    2. install方法中还初始化了一个CustomPatchListener对象,这个对象主要跟Patch文件的加载有关。
    3. addPatch方法主要是用于加载补丁文件,主要是调用了TinkerInstaller的onReceiveUpgradePatch方法进行补丁加载。

    然后我们创建一个自定义的TinkerApplicationLike类,这个类主要是关联了应用的Application的生命周期,降低了代码的入侵性。然后在onBaseContextAttached回调中对TinkerManager进行初始化:

    public class TinkerApplicationLike extends ApplicationLike {
    
        public TinkerApplicationLike(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);
            TinkerManager.getInstance().install(this);
        }
    }
    

    最后,我们创建自己的Application(也可以通过注解的方式生成,这里不做介绍),通过继承TinkerApplication并通过super方法关联上面的TinkerApplicationLike:

    public class App extends TinkerApplication {
    
        public App() {
            super(ShareConstants.TINKER_ENABLE_ALL,
                    "com.nan.tinkerdemo.tinker.TinkerApplicationLike",
                    "com.tencent.tinker.loader.TinkerLoader",
                    false);
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            //自己的逻辑
        }
    }
    

    通过委托ApplicationLike监听Application的声明周期(代理),使得Tinker的接入更加简单,降低耦合。

    最后,记得在清单文件中配置Application以及TINKER_ID:

    <application
        android:name=".App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name="com.nan.tinkerdemo.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
    
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    
        <service android:name=".tinker.TinkerService" />
        <service android:name=".tinker.CustomResultService" />
    
        <!--一定要记得设置tinker id-->
        <meta-data
            android:name="TINKER_ID"
            android:value="tinker_id_1234567" />
    
    </application>
    

    这个TINKER_ID是十分有用的,当Tinker需要进行Patch加载的时候,如果TINKER_ID不一致,就不会进行加载。

    命令行方式生成Patch

    下面先从简单的入手,先到Tinker的官方Github下载patch生成工具。

    <?xml version="1.0" encoding="UTF-8"?>
    <tinkerPatch>
        <issue id="property">
            <ignoreWarning value="false"/>
            <useSign value="true"/>
            <sevenZipPath value="/usr/local/bin/7za"/>
        </issue>
    
        <issue id="dex">
            <dexMode value="jar"/>
            <pattern value="classes*.dex"/>
            <pattern value="assets/secondary-dex-?.jar"/>
            <loader value="com.tencent.tinker.loader.*"/>
            <loader value="com.nan.tinkerdemo.App"/>
        </issue>
    
        <issue id="lib">
            <pattern value="lib/armeabi/*.so"/>
        </issue>
    
        <issue id="resource">
            <pattern value="res/*"/>
            <pattern value="assets/*"/>
            <pattern value="resources.arsc"/>
            <pattern value="AndroidManifest.xml"/>
            <ignoreChange value="assets/sample_meta.txt"/>
            <largeModSize value="100"/>
    
        </issue>
    
        <issue id="packageConfig">
            <configField name="platform" value="all"/>
    
            <configField name="patchMessage" value="classes.dex"/>
        </issue>
    
        <issue id="sign">
            <path value="release.keystore"/>
            <storepass value="testres"/>
            <keypass value="testres"/>
            <alias value="testres"/>
        </issue>
    
    </tinkerPatch>
    

    主要需要修改的地方有两处:

    1. value="com.nan.tinkerdemo.App"这里的Application全名需要改为自己项目的名字。
    2. sign配置里面要改为自己的配置。注意貌似这种方式只支持.keystore格式的签名文件。

    然后我们打两个包,一个有BUG的old.apk,一个没有BUG的new.apk,然后通过下面的命令进行补丁文件的生成:

    java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out out/
    

    最后在指定的out/文件下,找到patch_signed.apk,就是补丁文件。然后拷贝到已经安装好old.apk的手机中,通过下面的代码进行补丁加载即可:

    TinkerManager.getInstance().addPatch(path);
    

    Gradle方式生成Patch

    上面命令行的方式接入Tinker还是比较麻烦的,而且每次都要手动去执行命令,十分麻烦,最重要的是不支持jks格式的签名文件。因此下面介绍怎么通过Gradle的方式进行引入。

    首先,需要在项目的顶层gradle文件中引入Tinker的相关gradle脚本:

    classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
    

    app的Gradle文件如下:

    apply plugin: 'com.android.application'
    
    ////指定基准文件存放位置:在app/build/bakApk
    def bakPath = file("${buildDir}/bakApk")
    
    android {
        compileSdkVersion 26
        defaultConfig {
            applicationId "com.nan.tinkerdemo"
            minSdkVersion 15
            targetSdkVersion 26
            versionCode 1
            versionName "1.0"
            flavorDimensions "versionCode"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    
        sourceSets {
            main {
                jniLibs.srcDirs = ['libs']
            }
        }
    
        //recommend
        dexOptions {
            jumboMode = true
        }
    
        signingConfigs {
            //签名打包配置
            release {
                storeFile file("../nan.jks")
                storePassword "123456"
                keyAlias "nan"
                keyPassword "123456"
            }
        }
    
        buildTypes {
            release {
                minifyEnabled true
                signingConfig signingConfigs.release
                //这里要注意混淆规则
                proguardFiles getDefaultProguardFile('proguard-android.txt'), '../TinkerTool/tinker_proguard.pro'
            }
        }
    }
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'com.android.support:appcompat-v7:26.1.0'
        
        //tinker的核心库
        compile "com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"
    
        //支持Dex分包
        compile "com.android.support:multidex:1.0.2"
    
        compile 'com.squareup.okhttp3:okhttp:3.3.0'
    }
    
    ext {
        tinkerEnable = true
        tinkerID = "1.0"
    
        //下面需要注意的是,需要指定为生成基准包的具体位置
    
        tinkerOldApkPath = "${bakPath}/XXX"
        tinkerApplyMappingPath = "${bakPath}/XXX"
        tinkerApplyResourcePath = "${bakPath}/XXX"
        tinkerBuildFlavorDirectory = "${bakPath}/XXX"
    }
    
    def buildWithTinker() {
        return ext.tinkerEnable
    }
    
    def getOldApkPath() {
        return ext.tinkerOldApkPath
    }
    
    def getApplyMappingPath() {
        return ext.tinkerApplyMappingPath
    }
    
    def getApplyResourceMappingPath() {
        return ext.tinkerApplyResourcePath
    }
    
    def getTinkerIdValue() {
        return ext.tinkerID
    }
    
    def getTinkerBuildFlavorDirectory(){
        return ext.tinkerBuildFlavorDirectory
    }
    
    if (buildWithTinker()) {
        //启用tinker
        //apply tinker插件
        apply plugin: 'com.tencent.tinker.patch'
    
        //所有tinker相关的参数配置
        tinkerPatch {
            oldApk = getOldApkPath() //指定old apk的文件路径
            ignoreWarning = false //不忽略tinker的警告
            useSign = true//patch文件使用签名
            tinkerEnable = buildWithTinker()//指定是否启用tinker
    
            buildConfig {
                applyMapping = getApplyMappingPath()  //指定old apk打包时所使用的混淆文件
                applyResourceMapping = getApplyResourceMappingPath()  //指定old apk的资源文件
                tinkerId = getTinkerIdValue() //指定TinkerID
                keepDexApply = false
            }
    
            dex {
                dexMode = "jar" //jar、raw
                pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目录
                loader = ["com.nan.tinkerdemo.App"] //指定加载patch文件时用到的类
            }
    
            lib {
                pattern = ["libs/*/*.so"]
            }
    
            res {
                pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
                //指定tinker可以修改的资源路径
                ignoreChange = ["assets/sample_meta.txt"] //指定不受影响的资源路径
                largeModSize = 100 //资源修改大小默认值
            }
    
            packageConfig {
                configField("patchMessage", "fix the 1.0 version's bugs")
                configField("patchVersion", "1.0")
            }
    
        }
    
        //是否有多渠道
        List<String> flavors = new ArrayList<>()
        project.android.productFlavors.each { flavor ->
            flavors.add(flavor.name)
        }
        boolean hasFlavors = flavors.size() > 0
    
        //拷贝生成的apk文件以及mapping文件
        android.applicationVariants.all { variant ->
            /**
             * task type, you want to bak
             */
            def taskName = variant.name
            def date = new Date().format("MMdd-HH-mm-ss")
    
            tasks.all {
                if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
    
                    it.doLast {
                        copy {
                            def fileNamePrefix = "${project.name}-${variant.baseName}"
                            def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
    
                            def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                            from variant.outputs[0].outputFile
                            into destPath
                            rename { String fileName ->
                                fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                            }
    
                            from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                            into destPath
                            rename { String fileName ->
                                fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                            }
    
                            from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                            into destPath
                            rename { String fileName ->
                                fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                            }
                        }
                    }
                }
            }
        }
    }
    

    在这个文件中,我们主要干了下面的一些事情:

    1. 在tinkerPatch中对Tinker生成Patch的参数进行配置,配置都比较简单,关于tinker更多的配置请参考官方文档。
    2. 对签名进行配置
    3. 拷贝生成的apk文件以及mapping文件到指定的基准目录,这里是参考官方Demo的

    需要注意的是,这里配置了tinker ID,那么在清单文件中就不需要重复配置了。

    然后通过:

    ./gradlew assembleRelease
    

    进行基准包的生成,然后将上述脚本的基准包的具体信息替换脚本中的“XXX”处,最后执行Tinker的Gradle Task进行Patch文件生成:

    ./gradlew tinkerPatchRelease
    

    生成的Patch文件会在build/output/apk/中找到。

    下一篇文章将介绍tinker的一些进阶使用。

    相关文章

      网友评论

          本文标题:Tinker基本使用

          本文链接:https://www.haomeiwen.com/subject/loyibxtx.html