当前位置:首页 > 青鸟知识 > android

Android热修复技巧:Tinker接入源代码

来源:长沙it培训 发布日期:2017-05-04

  Android的热修复是用户直接下载一个补丁包,而不用重新下载一个新的apk安装,通过补丁来替换一些出现bug的类, 当然下载补丁的过程用户一般是感觉不到的,表面上看是直接修复了bug。

  Android热修复类似与插件开发,此前有对Android插件开发原理进行过分析,介绍了一下java中的类加载器和android中的类加载器,热修复就是利用android中的 DexClassLoader 类加载器,动态加载补丁dex,替换有bug的类。

  接入Tinker

  接入tinker目前给了两种方式,一种是基于命令行的方式,类似于AndFix的接入方式;一种就是gradle的方式。

  考虑早期使用Andfix的app应该挺多的,以及很多人对gradle的相关配置还是觉得比较繁琐的,下面对两种方式都介绍下。

  (1)命令行接入

  接入之前我们先考虑下,接入的话,正常需要的前提(开启混淆的状态)。

  对于API

  一般来说,我们接入热修库,会在Application#onCreate中进行一下初始化操作。然后在某个地方去调用类似loadPatch这样的API去加载patch文件。

  对于patch的生成

  简单的方式就是通过两个apk做对比然后生成;需要注意的是:两个apk做对比,需要的前提条件,第二次打包混淆所使用的mapping文件应该和线上apk是一致的。

  最后就是看看这个项目有没有需要配置混淆;

  有了大致的概念,我们就基本了解命令行接入tinker,大致需要哪些步骤了。

  依赖引入

dependencies {
    // ...
    //可选,用于生成application类
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker的核心库
    compile('com.tencent.tinker:tinker-android-lib:1.7.7')
}

  顺便加一下签名的配置:

android{
  //...
    signingConfigs {
        release {
            try {
                storeFile file("release.keystore")
                storePassword "testres"
                keyAlias "testres"
                keyPassword "testres"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            debuggable true
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

  文末会有demo的下载地址,可以直接参考build.gradle文件,不用担心这些签名文件去哪找。

  API引入

  API主要就是初始化和loadPacth。

  正常情况下,我们会考虑在Application的onCreate中去初始化,不过tinker推荐下面的写法:

@DefaultLifeCycle(application = ".SimpleTinkerInApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class SimpleTinkerInApplicationLike extends ApplicationLike {
    public SimpleTinkerInApplicationLike(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);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        TinkerInstaller.install(this);
    }
}

  ApplicationLike通过名字你可能会猜,并非是Application的子类,而是一个类似Application的类。

  tinker建议编写一个ApplicationLike的子类,你可以当成Application去使用,注意顶部的注解:@DefaultLifeCycle,其application属性,会在编译期生成一个SimpleTinkerInApplication类。

  所以,虽然我们这么写了,但是实际上Application会在编译期生成,所以AndroidManifest.xml中是这样的:

  android:name=".SimpleTinkerInApplication"

  .../>

  编写如果报红,可以build下。

  这样其实也能猜出来,这个注解背后有个Annotation Processor在做处理,如果你没了解过,可以看下:

  Android 如何编写基于编译时注解的项目

  通过该文会对一个编译时注解的运行流程和基本API有一定的掌握,文中也会对tinker该部分的源码做解析。

  上述,就完成了tinker的初始化,那么调用loadPatch的时机,我们直接在Activity中添加一个Button设置:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


    public void loadPatch(View view) {
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
    }
}

  我们会将patch文件直接push到sdcard根目录;

  所以一定要注意:添加SDCard权限,如果你是6.x以上的系统,自己添加上授权代码,或者手动在设置页面打开SDCard读写权限。

  除以以外,有个特殊的地方就是tinker需要在AndroidManifest.xml中指定TINKER_ID。

  android:name="TINKER_ID"

  android:value="tinker_id_6235657" />

  //...

  到此API相关的就结束了,剩下的就是考虑patch如何生成。

  patch生成

  tinker提供了patch生成的工具,源码见:tinker-patch-cli,打成一个jar就可以使用,并且提供了命令行相关的参数以及文件。

  命令行如下:

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

  需要注意的就是tinker_config.xml,里面包含tinker的配置,例如签名文件等。

  这里我们直接使用tinker提供的签名文件,所以不需要做修改,不过里面有个Application的item修改为与本例一致:

  大致的文件结构如下:

安卓热键修复.gif

  可以在tinker-patch-cli中提取,或者直接下载文末的例子。

  上述介绍了patch生成的命令,最后需要注意的就是,在第一次打出apk的时候,保留下生成的mapping文件,在/build/outputs/mapping/release/mapping.txt。

  可以copy到与proguard-rules.pro同目录,同时在第二次打修复包的时候,在proguard-rules.pro中添加上:

  -applymapping mapping.txt

  保证后续的打包与线上包使用的是同一个mapping文件。

  tinker本身的混淆相关配置,可以参考:

  tinker_proguard.pro

  如果,你对该部分描述不了解,可以直接查看源码即可。

  测试

  首先随便生成一个apk(API、混淆相关已经按照上述引入),安装到手机或者模拟器上。然后,copy出mapping.txt文件,设置applymapping,修改代码,再次打包,生成new.apk。两次的apk,可以通过命令行指令去生成patch文件。

  如果你下载本例,命令需要在[该目录]下执行。

  最终会在output文件夹中生成产物:

output文件夹中生成产物.jpg

  我们直接将patch_signed.apk push到sdcard,点击loadpatch,一定要观察命令行是否成功。

  本例修改了title。

  点击loadPatch,观察log,如果成功,应用默认为重启,然后再次启动即可达到修复效果。

  到这里命令行的方式就介绍完了,和Andfix的接入的方式基本上是一样的。

  值得注意的是:该例仅展示了基本的接入,对于tinker的各种配置信息,还是需要去读tinker的文档(如果你确定要使用)tinker-wiki。

  (2)gradle接入

  gradle接入的方式应该算是主流的方式,所以tinker也直接给出了例子,单独将该tinker-sample-android以project方式引入即可。

  引入之后,可以查看其接入API的方式,以及相关配置。

  在你每次build时,会在build/bakApk下生成本地打包的apk,R文件,以及mapping文件。

  如果你需要生成patch文件,可以通过:

  ./gradlew tinkerPatchRelease // 或者 ./gradlew tinkerPatchDebug

  生成

  生成目录为:build/outputs/tinkerPatch

生成目录.gif

  需要注意的是,需要在app/build.gradle中设置相比较的apk(即old.apk,本次为new.apk),

ext {
    tinkerEnabled = true
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/old.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/old-mapping.txt"
}

  提供的例子,基本上展示了tinker的自定义扩展的方式。所以,如果你使用命令行方式接入,也不要忘了学习下其支持哪些扩展。

  三、Application是如何编译时生成的

  从注释和命名上看:

  //可选,用于生成application类

  provided('com.tencent.tinker:tinker-android-anno:1.7.7')

  明显是该库,其结构如下:

结构.jpg

  典型的编译时注解的项目,源码见tinker-android-anno。

  入口为com.tencent.tinker.anno.AnnotationProcessor,可以在该services/javax.annotation.processing.Processor文件中找到处理类全路径。

  再次建议,如果你不了解,简单阅读下Android 如何编写基于编译时注解的项目该文。

  直接看AnnotationProcessor的process方法:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    processDefaultLifeCycle(roundEnv.getElementsAnnotatedWith(DefaultLifeCycle.class));
    return true;
}
直接调用了processDefaultLifeCycle:

private void processDefaultLifeCycle(Set<? extends Element> elements) {
        // 被注解DefaultLifeCycle标识的对象
        for (Element e : elements) {
          // 拿到DefaultLifeCycle注解对象
            DefaultLifeCycle ca = e.getAnnotation(DefaultLifeCycle.class);

            String lifeCycleClassName = ((TypeElement) e).getQualifiedName().toString();
            String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf('.'));
            lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf('.') + 1);

            String applicationClassName = ca.application();
            if (applicationClassName.startsWith(".")) {
                applicationClassName = lifeCyclePackageName + applicationClassName;
            }
            String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf('.'));
            applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf('.') + 1);

            String loaderClassName = ca.loaderClass();
            if (loaderClassName.startsWith(".")) {
                loaderClassName = lifeCyclePackageName + loaderClassName;
            }

             // /TinkerAnnoApplication.tmpl
            final InputStream is = AnnotationProcessor.class.getResourceAsStream(APPLICATION_TEMPLATE_PATH);
            final Scanner scanner = new Scanner(is);
            final String template = scanner.useDelimiter("\\A").next();
            final String fileContent = template
                .replaceAll("%PACKAGE%", applicationPackageName)
                .replaceAll("%APPLICATION%", applicationClassName)
                .replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName)
                .replaceAll("%TINKER_FLAGS%", "" + ca.flags())
                .replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName)
                .replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());
                JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName);
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Creating " + fileObject.toUri());
          Writer writer = fileObject.openWriter();
            PrintWriter pw = new PrintWriter(writer);
            pw.print(fileContent);
            pw.flush();
            writer.close();

        }
    }

  代码比较简单,可以分三部分理解:

  步骤1:首先找到被DefaultLifeCycle标识的Element(为类对象TypeElement),得到该对象的包名,类名等信息,然后通过该对象,拿到@DefaultLifeCycle对象,获取该注解中声明属性的值。

  步骤2:读取一个模板文件,读取为字符串,将各个占位符通过步骤1中的值替代。

  步骤3:通过JavaFileObject将替换完成的字符串写文件,其实就是本例中的Application对象。

  我们看一眼模板文件:

package %PACKAGE%;

import com.tencent.tinker.loader.app.TinkerApplication;

/**
 *
 * Generated application for tinker life cycle
 *
 */
public class %APPLICATION% extends TinkerApplication {

    public %APPLICATION%() {
        super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
    }

}

  对应我们的SimpleTinkerInApplicationLike,

  @DefaultLifeCycle(application = ".SimpleTinkerInApplication",

  flags = ShareConstants.TINKER_ENABLE_ALL,

  loadVerifyFlag = false)

  public class SimpleTinkerInApplicationLike extends ApplicationLike {}

  主要就几个占位符:

  包名,如果application属性值以点开始,则同包;否则则截取。类名,application属性值中的类名

  %TINKER_FLAGS%对应flags

  %APPLICATION_LIFE_CYCLE%,编写的ApplicationLike的全路径

  “%TINKER_LOADER_CLASS%”,这个值我们没有设置,实际上对应@DefaultLifeCycle的loaderClass属性,默认值为com.tencent.tinker.loader.TinkerLoader

  %TINKER_LOAD_VERIFY_FLAG%对应loadVerifyFlag

  于是最终生成的代码为:

/**
 *
 * Generated application for tinker life cycle
 *
 */
public class SimpleTinkerInApplication extends TinkerApplication {

    public SimpleTinkerInApplication() {
        super(7, "com.zhy.tinkersimplein.SimpleTinkerInApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

}

  tinker这么做的目的,文档上是这么说的:

  为了减少错误的出现,推荐使用Annotation生成Application类。

  这样大致了解了Application是如何生成的。

  接下来我们大致看一下tinker的原理。

  四、原理

原理.gif

 


拒绝套路 试听有礼

数据已加密保证您的信息安全