热更新Tinker研究(一):运行tinker-sample-android

热更新Tinker研究(一):运行tinker-sample-android
热更新Tinker研究(二):结合源码学习Dex格式
热更新Tinker研究(三):加载补丁
热更新Tinker研究(四):TinkerLoader
热更新Tinker研究(五):Application的隔离
热更新Tinker研究(六):TinkerPatchPlugin
热更新Tinker研究(七):Dex的patch文件生成
热更新Tinker研究(八):res和so的patch文件生成
热更新Tinker研究(九):Dex文件的patch
热更新Tinker研究(十):Res文件的patch
热更新Tinker研究(十一):so文件的patch

热更新Tinker研究(一):运行tinker-sample-android

一、导入项目

运行sample项目只需要导入tinker-sample-android工程即可,

enter description here

整个项目的操作流程分为一下几个步骤:

Created with Raphaël 2.1.0 开始 编译baseApk(基准apk包) 修改源码,生成patchApk 推送补丁 加载补丁 结束

二、编译baskApk

1,生成baskApk

以下都采用release方式打包,运行assembleRelease

enter description here

或者采用命令行的方式

    ./gradlew assembleRelease

输出结果:
除了在app/build/outputs/apk下生成app-release.apk外,还会在bak文件下生成相关备份

enter description here

这里默认根据日期和时间生成apk

然后将原始apk安装到设备上

enter description here

运行:

enter description here

2,指定OldApk相关属性

打开app/build.gradle

enter description here

这里的tinkerOldApkPath、tinkerApplyMappingPath、tinkerApplyResourcePath三个属性需要和bak文件下baseApk的一致

三、修改源码,生成patchApk

1, 对工程源码进行修改

这里我只对MainActivity和activity_main.xml进行修改

在MainActivity的onCreate()加入

   Toast.makeText(this, "I am Patched", Toast.LENGTH_LONG).show();

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingBottom="@dimen/activity_vertical_margin"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                tools:context=".app.MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sample patch!"/>

    <Button
        android:id="@+id/loadPatch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/textView"
        android:text="load patch"/>

    <Button
        android:id="@+id/loadLibrary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/loadPatch"
        android:text="load library"/>

    <Button
        android:id="@+id/cleanPatch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/loadLibrary"
        android:text="clean patch"/>
    <Button
        android:id="@+id/killSelf"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/cleanPatch"
        android:text="kill self"/>
    <Button
        android:id="@+id/showInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/killSelf"
        android:text="show info"/>

    <!-- add by patch-->
    <TextView
        android:layout_below="@id/showInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="I am a patch app"/>
</RelativeLayout>

2,进行打包并生成patch.apk

运行任务tinkerPatchRelease

enter description here

得到新的apk和patch.apk文件

enter description here
分别输出在app/build/bakApk和 app/build/outputs/tinkerPatch/release/patch_signed_7zip.apk下

四、推送补丁

运行

adb push patch_signed_7zip.apk /storage/sdcard0/

将patch_signed_7zip.apk推送到内部存储的根目录

五、加载补丁

点击之前运行程序的LOAD PATCH按钮,然后点击KILL PATCH,重新运行程序,发现已经变成了我们修改之后的样子。

enter description here
可以看到有了我们添加的toast提示和I am a patch app布局,patch已经生效。

六、相关说明

1,*-mapping.txt

截取app-release-0314-10-10-21-mapping.txt片段

android.support.annotation.Keep -> android.support.annotation.Keep:
android.support.multidex.MultiDex -> android.support.multidex.a:
    java.lang.String SECONDARY_FOLDER_NAME -> a
    java.util.Set installedApk -> b
    boolean IS_VM_MULTIDEX_CAPABLE -> c
    92:181:void install(android.content.Context) -> a
    188:205:android.content.pm.ApplicationInfo getApplicationInfo(android.content.Context) -> b
    215:234:boolean isVMMultidexCapable(java.lang.String) -> a
    240:249:void installSecondaryDexes(java.lang.ClassLoader,java.io.File,java.util.List) -> a
    256:261:boolean checkValidZipFiles(java.util.List) -> a
    273:288:java.lang.reflect.Field findField(java.lang.Object,java.lang.String) -> b
    302:317:java.lang.reflect.Method findMethod(java.lang.Object,java.lang.String,java.lang.Class[]) -> b
    331:338:void expandFieldArray(java.lang.Object,java.lang.String,java.lang.Object[]) -> b
    341:364:void clearOldDexDir(android.content.Context) -> c
    57:57:java.lang.reflect.Field access$300(java.lang.Object,java.lang.String) -> a
    57:57:void access$400(java.lang.Object,java.lang.String,java.lang.Object[]) -> a
    57:57:java.lang.reflect.Method access$500(java.lang.Object,java.lang.String,java.lang.Class[]) -> a
    63:76:void <clinit>() -> <clinit>
android.support.multidex.MultiDex$V14 -> android.support.multidex.a$a:
    445:449:void install(java.lang.ClassLoader,java.util.List,java.io.File) -> b
    459:462:java.lang.Object[] makeDexElements(java.lang.Object,java.util.ArrayList,java.io.File) -> a
    434:434:void access$100(java.lang.ClassLoader,java.util.List,java.io.File) -> a

文件中存放的是类混淆前后的对应关系,比如android.support.multidex.MultiDex -> android.support.multidex.a表示MultiDex会被混淆成 android.support.multidex.a.class。通过反编译app-release-0314-10-10-21.apk进行验证,

enter description here

2, *-R.txt

int anim abc_fade_in 0x7f050000
int anim abc_fade_out 0x7f050001
int anim abc_grow_fade_in_from_bottom 0x7f050002
int anim abc_popup_enter 0x7f050003
int anim abc_popup_exit 0x7f050004
int anim abc_shrink_fade_out_from_bottom 0x7f050005
int anim abc_slide_in_bottom 0x7f050006
int anim abc_slide_in_top 0x7f050007
int anim abc_slide_out_bottom 0x7f050008
int anim abc_slide_out_top 0x7f050009
int attr actionBarDivider 0x7f010063
int attr actionBarItemBackground 0x7f010064
int attr actionBarPopupTheme 0x7f01005d

R文件的记录

3,app/build.gradle

需要用到patch插件

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

以下转载于http://www.jianshu.com/p/d50817b6d622

tinkerPatch {
        // 基准apk包的路径,必须输入,否则会报错。
        oldApk = getOldApkPath()
        /**
         * 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。
         * 因为这些情况可能会导致编译出来的patch包带来风险:
         * case 1: minSdkVersion小于14,但是dexMode的值为"raw";
         * case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
         * case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
         * case 4:  定义在dex.loader用于加载补丁的类出现修改;
         * case 5: resources.arsc改变,但没有使用applyResourceMapping编译。
         */
        ignoreWarning = false

        /**
         * 运行过程中需要验证基准apk包与补丁包的签名是否一致,是否需要签名。
         */
        useSign = true

        /**
         * optional,default 'true'
         * whether use tinker to build
         */
        tinkerEnable = buildWithTinker()

        /**
         * 编译相关的配置项
         */
        buildConfig {
            /**
             * 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。
             * 这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
             */
            applyMapping = getApplyMappingPath()
            /**
             * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配。
             * 这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。
             * 这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
             */
            tinkerId = getTinkerIdValue()

            /**
             * 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。
             * 若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
             */
            keepDexApply = false
        }
        /**
         * dex相关的配置项
         */
        dex {
            /**
             * 只能是'raw'或者'jar'。
             * 对于'raw'模式,我们将会保持输入dex的格式。
             * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。
             * 如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。
             * 默认我们并不会去校验md5,一般情况下选择jar模式即可。
             */
            dexMode = "jar"

            /**
             * 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。
             * 这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
             * 这里需要定义的类有:
             * 1. 你自己定义的Application类;
             * 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
             * 3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
             * 4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。
             *    这里需要注意的是,这些类的直接引用类也需要加入到loader中。
             *    或者你需要将这个类变成非preverify。
             * 5. 使用1.7.6版本之后版本,参数1、2会自动填写。
             *
             */
            loader = [
                    // Tinker库中用于加载补丁包的部分类
                    "com.tencent.tinker.loader.*",
                    // 自己定义的Application类;
                    "com.tinker.app.AppContext",
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }
        /**
         * lib相关的配置项
         */
        lib {
            /**
             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...
             */
            pattern = ["lib/*/*.so"]
        }
        /**
         * res相关的配置项
         */
        res {
            /**
             * 需要处理res路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...,
             * 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。
             * 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。
             * 这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
             */
            largeModSize = 100
        }
        /**
         * 用于生成补丁包中的'package_meta.txt'文件
         */
        packageConfig {
            /**
             * configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
             * 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
             * 但是建议直接通过修改代码来实现,例如BuildConfig。
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             * just a sample case, you can use such as sdkVersion, brand, channel...
             * you can parse it in the SamplePatchListener.
             * Then you can use patch conditional!
             */
            configField("platform", "all")
            /**
             * 配置patch补丁版本
             */
            configField("patchVersion", "1.0.0")
        }
        /**
         * 7zip路径配置项,执行前提是useSign为true
         */
        sevenZip {
            /**
             * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用。
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * 系统中的7za路径,例如"/usr/local/bin/7za"。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。
             */
            // path = "/usr/local/bin/7za"
        }
    }

备份apk到bak文件夹下

    /**
    * bak apk and 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.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
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值