Tinker基本使用

Tinker简介与核心原理

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

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

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

Tinker基本使用_第1张图片
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:


    
        
            

            
        
    

    
    

    
    


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

命令行方式生成Patch

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



    
        
        
        
    

    
        
        
        
        
        
    

    
        
    

    
        
        
        
        
        
        

    

    
        

        
    

    
        
        
        
        
    


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

  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 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基本使用)