Tinker热修复原理以及组件化封装

需要创建以下类

  • TinkerManager(对Tinker进行组件化再次封装,减少对系统的侵入)
  • CustomApplicationLike(委托(代理)模式:Tinker需要对Application的生命周期进行监听,通过ApplicationLike进行委托)
  • CustomPatchListener(自定义PatchListener监听patch receiver事件)
  • CustomResultService(自定义TinkerResultService改变patch安装后的行为)
  • TinkerService(功能:应用程序Tinker更新服务: 1.检查补丁更新 2.从服务器下载patch文件 3.使用TinkerManager完成patch文件加载 4.patch文件会在下次进程启动时生效)

项目展示

多渠道打补丁
tinker插件

TinkerManager

/**
 * 作者:wujie on 2018/12/1 14:49
 * 邮箱:705030268@qq.com
 * 功能:对Tinker进行组件化再次封装,减少对系统的侵入
 */

public class TinkerManager {

    private static boolean isInstalled = false;

    private static ApplicationLike mApplicationLike;

    private static CustomPatchListener mPatchListener;


    /**
     * 对Tinker的初始化
     * @param applicationLike
     */
    public static void installTinker(ApplicationLike applicationLike) {
        mApplicationLike = applicationLike;
        if(isInstalled) {
            return;
        }

        mPatchListener = new CustomPatchListener(getApplicationContext());
        LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());
        PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());
        AbstractPatch upgradePatchProcessor = new UpgradePatch();

//        TinkerInstaller.install(mApplicationLike); //对Tinke初始化
        TinkerInstaller.install(applicationLike,
                loadReporter,
                patchReporter,
                mPatchListener,
                CustomResultService.class,
                upgradePatchProcessor); //完成Tinker初始化

        isInstalled = true;
    }

    //完成Patch文件的加载
    public static void loadPatch(String localPath) {
        if(Tinker.isTinkerInstalled()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), localPath);
        }
    }

    //加个md5校验规则,防止补丁文件被篡改
    //服务端返回原始文件的md5值,客户端下载patch文件以后,获取patch文件的MD5值与服务端返回的md5值比较是否相等
    public static void loadPatch(String path, String md5Value) {
        if (Tinker.isTinkerInstalled()) {
            mPatchListener.setCurrentMD5(md5Value);
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
        }
    }

    private static Context getApplicationContext() {
        if(null != mApplicationLike) {
            return mApplicationLike.getApplication().getApplicationContext();
        }
        return null;
    }
}

CustomApplicationLike

/**
 * 作者:wujie on 2018/12/1 15:07
 * 邮箱:705030268@qq.com
 * 功能:委托(代理)模式
 *      Tinker需要对Application的生命周期进行监听,通过ApplicationLike进行委托,在ApplicationLike中对Tinker进行监听
 */
@DefaultLifeCycle(application = ".MyTinkerApplication",   //通过注解生成Application
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class CustomApplicationLike extends ApplicationLike {

    public CustomApplicationLike(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);
        //使应用支持分包
        MultiDex.install(base);
        TinkerManager.installTinker(this);
    }
}

自定义PatchListener

/**
 * @function: 自定义PatchListener监听patch receiver事件
 * 1.较验patch文件是否合法(自定义规则)  2.启动Service去安装patch文件
 */
public class CustomPatchListener extends DefaultPatchListener {

    private String currentMD5;

    public void setCurrentMD5(String md5Value) {

        this.currentMD5 = md5Value;
    }

    public CustomPatchListener(Context context) {
        super(context);
    }


    @Override
    protected int patchCheck(String path, String patchMd5) {
        //patch文件md5较验
        if (!Utils.isFileMD5Matched(path, currentMD5)) {

            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        return super.patchCheck(path, patchMd5);

    }
}

自定义TinkerResultService

/**
 * @function 自定义TinkerResultService改变patch安装后的行为
 * 本类的作用:决定在patch安装完以后的后续操作,默认实现是杀进程
 */
public class CustomResultService extends DefaultTinkerResultService {
    private static final String TAG = "Tinker.SampleResultService";

    //返回patch文件的最终安装结果
    @Override
    public void onPatchResult(PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());

        //first, we want to kill the recover process
        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

        // if success and newPatch, it is nice to delete the raw file, and restart at once
        // only main process can load an upgrade patch!
        if (result.isSuccess) {
            deleteRawPatchFile(new File(result.rawPatchFilePath));
            //默认加载patch以后,app闪退
//            if (checkIfNeedKill(result)) {
//                android.os.Process.killProcess(android.os.Process.myPid());
//            } else {
//                TinkerLog.i(TAG, "I have already install the newly patch version!");
//            }
        }

    }
}

TinkerService

/**
 * 作者:wujie on 2018/11/23 23:36
 * 邮箱:705030268@qq.com
 * 功能:应用程序Tinker更新服务:
 * 1.检查补丁更新
 * 2.从服务器下载patch文件
 * 3.使用TinkerManager完成patch文件加载
 * 4.patch文件会在下次进程启动时生效
 */

public class TinkerService extends Service {
    private String mPatchFileDir;
    private String mPatchFile;
    private AppVersion mAppVersion;
    private final int CHECK_PATCH_UPDATE = 0x01;
    private final int DOWNLOAD_PATH = 0x02;
    private final String PATH_DOT = ".apk"; //补丁文件后缀

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case CHECK_PATCH_UPDATE:
                    checkPatchUpdate();
                    break;
                case DOWNLOAD_PATH:
                    downPatch();
                    break;
                default:
                    break;
            }
        }
    };

    //对外提供启动servcie方法
    public static void runTinkerService(Context context) {
        try {
            Intent intent = new Intent(context, TinkerService.class);
            context.startService(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //用来与被启动者通信的接口
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        init(); //初始化Patch文件目录
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mHandler.sendEmptyMessage(CHECK_PATCH_UPDATE); //检查是否有补丁
        return START_NOT_STICKY; //START_NOT_STICKY表示service如果被系统回收不重启
    }

    private void checkPatchUpdate() {
        ApiService.getInstance()
                .getLatestVersion()
                .subscribe(appVersion -> {
                    //如果当前不是最新版本,则不去下载补丁包
                    //原则上补丁包只是应付当前版本的紧急bug
                    if(StringUtils.isEmpty(appVersion.patchurl)) {
                        stopSelf();
                        return;
                    }
                    mAppVersion = appVersion;
                    mHandler.sendEmptyMessage(DOWNLOAD_PATH);
                }, new ErrorAction() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        super.accept(throwable);
                        stopSelf();
                    }
                });
    }


    private void downPatch() {
        //保存的补丁文件命名规则:xxx/1.0.0.1.apk 1.0.0是正常版本 .1是补丁版本号
        if(null != mAppVersion) {
            mPatchFile = mPatchFileDir.concat(mAppVersion.patch_versionname).concat(PATH_DOT);
            //判断该补丁是否存在,如果已经存在,不需要重复下载
//            File apatchFile = new File(mPatchFileDir);
//            if(apatchFile == null || !apatchFile.exists()) {
               //下载补丁
                Logger.d("正在下载");
                FileDownloadService.getInstance(new IDownloadListener() {
                    @Override
                    public void onDownloadSuccess() {
                        ToastUtils.showShortSafe("文件下载成功");
                        TinkerManager.loadPatch(mPatchFile);
//                        TinkerManager.loadPatch(mPatchFile, md5);
                    }

                    @Override
                    public void onDownloadFail(Exception exception) {
                        ToastUtils.showShortSafe("文件下载失败");
                    }

                    @Override
                    public void onProgress(int progress) {
                        Logger.d("文件下载%d", progress);
                    }
                }).download(mAppVersion.patchurl, mPatchFile);
//            }
        }
    }


    public void init() {
        if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            //sd卡已挂载
            mPatchFileDir = getExternalCacheDir().getAbsolutePath() + "/patch/";
            File patchDir = new File(mPatchFileDir);
            try {
                if(null == patchDir || !patchDir.exists()) {
                    patchDir.mkdir();
                }
            } catch (Exception e) {
                e.printStackTrace();
                stopSelf();
            }
        } else {
            ToastUtils.showShortSafe("SD卡未挂载");
            stopSelf();
        }
    }
}

gradle生成热修复脚本

apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda' //retrolambda表达式
apply plugin: 'com.jakewharton.butterknife' //butterknife View注解

def javaVersion = JavaVersion.VERSION_1_8
def bakPath = file("${buildDir}/bakApk/")

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.wj.tinkerutils"
        minSdkVersion 18
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        manifestPlaceholders = [
                UMENG_APPKEY_VALUE: "xxxx",
                UMENG_CHANNEL_VALUE: "xxxx"
        ]
    }
    signingConfigs {
        release {
            try {
                storeFile file("test.jks")
                keyAlias 'xxx'
                keyPassword 'xxx'
                storePassword 'xxx'
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }
    }
    buildTypes {
        release {
            minifyEnabled true 
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    configurations.all {
        resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
    }
    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }
    //多渠道脚本支持
    productFlavors {
        googleplayer {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"]
        }
        baidu {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
        }
        productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:multidex:1.0.1'
    //dex分包
    //butterknife View注解
    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
    //tinker
    //optional, help to generate the final application
    //provided参与编译,不参与打包,这样可以减少apk的大小
    provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    //tinker's main Android lib
    compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    compile 'com.umeng.analytics:analytics:latest.integration'
    //添加友盟统计
    compile 'com.blankj:utilcode:1.7.1'
    //android工具类
    compile project(':good_http_utils')
    //防止RxJava内存泄漏
    compile 'com.trello:rxlifecycle:0.8.0'
    compile 'com.trello:rxlifecycle-components:0.8.0'
    compile 'com.tbruyelle.rxpermissions:rxpermissions:0.9.1@aar' //android6+特殊权限申请
}

def appTinkerVersion = "app-20181201-21-44-37"

/**
 * ext相关配置
 */
ext {
    tinkerEnabled = true
    // 基础版本apk
    tinkerOldApkPath = "${bakPath}/" + appTinkerVersion
    // 未开启混淆的话mapping可以忽略,如果开启混淆mapping要保持一致。
    tinkerApplyMappingPath = "${bakPath}/" + appTinkerVersion
    // 与基础版本一起生成的R.text文件
    tinkerApplyResourcePath = "${bakPath}/" + appTinkerVersion
    // 只用于构建所有的Build,如果不是,此处可忽略。
    tinkerBuildFlavorDirectory = "${bakPath}/" + appTinkerVersion
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

//基础APK的位置
def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

// Mapping的位置
def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

// ResourceMapping的位置
def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

// 用来获取TinkerId(当前版本号就是TinkerId)
def getTinkerIdValue() {
    return android.defaultConfig.versionName
}

//多渠道
def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    //如果启用,加载Tinker插件,插件封装了些tinker脚本
    apply plugin: 'com.tencent.tinker.patch'
    //所有tinker相关参数配置
    tinkerPatch {
        oldApk = getOldApkPath() //基准apk包的路径,必须输入,否则会报错
        /**
         * 如果出现以下的情况,并且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 //不忽略tinker的警告,有则中止patch文件的生成
        useSign = true //强制patch文件也需要签名
        tinkerEnable = buildWithTinker() //是否启用tinker
        //编译相关的配置项
        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"]
            //todo
            loader = ["com.wj.tinkerutils.tinker.MyTinkerApplication"] //指定加载patch文件用到的类
        }
        lib {
            /**
             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...
             */
            pattern = ["lib/*/*.so"]
        }
        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。
             */
            //todo
            configField("patchMessage", "tinker is sample to use")
            configField("platform", "all")
            /**
             * 配置patch补丁版本
             */
            configField("patchVersion", "1.0.0_1")
        }
        //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"
//        }
    }
    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("yyyyMMdd-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")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
                    }
                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }
                }
            }
        }
    }
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值