Tinker-自定义扩展与流程分析(下)

前言

上一篇我们讲解了Tinker的使用,现在我们讲解下一些功能的扩展与从源码角度查看流程分析。


功能扩展

在扩展功能之前我们要先来了解下。我们可以扩展那些功能。下面我们重Tinker的初始化函数入手。修改TinkerManager代码如下:

  /**
     * 完成Tinker初始化
     *
     * @param applicationLike
     */
    public static void installedTinker(ApplicationLike applicationLike) {
        mApplicationLike = applicationLike;
        if (isInstalled) {
            return;
        }
//        TinkerInstaller.install(mApplicationLike);
        mPatchListener = new DefaultPatchListener(getApplicationContext());//一些补丁文件的校验工作
        //这两个是监听patch文件安装的日志上报结果 也就是补丁文件安装监听
        LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());//一些在加载补丁文件时的回调
        PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());//补丁文件在合成时一些事件的回调

        AbstractPatch abstractPatch = new UpgradePatch();//决定patch文件安装策略  不会去修改与自定义
        TinkerInstaller.install(mApplicationLike,
                loadReporter,
                patchReporter,
                mPatchListener,
                CustomResultService.class,//我们自定义的
                abstractPatch);
        isInstalled = true;
    }

可以看到我们把上一篇的初始化函数注释掉,而是采用6个参数的注册方法。这些参数的作用在官方文档中都非常的详细自定义扩展。我这里全都是使用的默认的。这里根据实际开发区决定要自定义那些内容。我就不过多介绍了。不过我重写了CustomResultService类。我们看下:


/**
 * 功能   :决定在patch安装以后的后续操作 默认实现是杀死进程
 */

public class CustomResultService extends DefaultTinkerResultService {
    private static final String TAG = "Tinker.DefaultTinkerResultService";
    @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));
           <!--  if (checkIfNeedKill(result)) {
                android.os.Process.killProcess(android.os.Process.myPid());
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }-->
        }
    }
}

我们知道,Tinker在补丁文件安装完成后默认是杀死当前进程的,显然这样做的效果不是很好。所以我们重写了DefaultTinkerResultService,并将原本杀死进程的代码注释掉,这样我们就可以在用户无感知的情况下完成补丁的安装,并且在用户下次启动的时候生效。其他的代码不变,重新走一遍流程。就会看到效果的生成


多渠道打包修复

还记得我们在第一篇中我们将所有关于多渠道打包的代码都注释了,现在我们将注释的代码放开,然后按照步骤。
首先先打出多渠道签名文件包

图片.png

第二步配置gradle脚本。

图片.png

可以看到多渠道打包后,基准包路径的配置还是有些不同的。(这里我没有开启混淆。不过是没有影响的)

第三步就是修改代码,然后生成补丁文件

图片.png

生成后的目录:
图片.png

这样就针对两个渠道的签名包,生成补丁文件,剩下的流程就与之前一样了。


从源码的角度分析流程

Tinker的源码是比较的复杂的尤其的它的Dexdiff算法。本身我还是个菜鸟,所以只能够源码角度理清下流程。有人可能问,一个框架,能使用有效果,问题能排查就好啦。确实我个人觉得一个技术或是框架日新月异。总有些新的技术出现并且我也不是大神,感觉有些浪费时间。不过我还是逼自己去看源码。从中我认为最好的好处有两个:1.能更好的定位为题,和自定义扩展功能 2.能学习优秀框架的代码格式书写。使自己写出高质量的代码,这也是我认为最重要的。

从Tinker的注册方法可以看到,他用了外观模式。所以我们也从 TinkerInstaller.install()进去

public class TinkerInstaller {
    private static final String TAG = "Tinker.TinkerInstaller";

    /**
     * install tinker with default config, you must install tinker before you use their api
     * or you can just use {@link TinkerApplicationHelper}'s api
     *
     * @param applicationLike
     */
    public static Tinker install(ApplicationLike applicationLike) {
        Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build();
        Tinker.create(tinker);
        tinker.install(applicationLike.getTinkerResultIntent());
        return tinker;
    }

    /**
     * install tinker with custom config, you must install tinker before you use their api
     * or you can just use {@link TinkerApplicationHelper}'s api
     *
     * @param applicationLike
     * @param loadReporter
     * @param patchReporter
     * @param listener
     * @param resultServiceClass
     * @param upgradePatchProcessor
     */
    public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter,
                               PatchListener listener, Class<? extends AbstractResultService> resultServiceClass,
                               AbstractPatch upgradePatchProcessor) {

        Tinker tinker = new Tinker.Builder(applicationLike.getApplication())
            .tinkerFlags(applicationLike.getTinkerFlags())
            .loadReport(loadReporter)
            .listener(listener)
            .patchReporter(patchReporter)
            .tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build();

        Tinker.create(tinker);
        tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor);
        return tinker;
    }

    /**
     * clean all patch files!
     *
     * @param context
     */
    public static void cleanPatch(Context context) {
        Tinker.with(context).cleanPatch();
    }

    /**
     * new patch file to install, try install them with :patch process
     *
     * @param context
     * @param patchLocation
     */
    public static void onReceiveUpgradePatch(Context context, String patchLocation) {
        Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
    }

    /**
     * set logIml for TinkerLog
     *
     * @param imp
     */
    public static void setLogIml(TinkerLog.TinkerLogImp imp) {
        TinkerLog.setTinkerLogImp(imp);
    }
}

这个类也比较简单,就是2个注册的方法,前面我们也都用到了,还有就是加载补丁的方法和清除补丁的方法。可以看到Tinker处理的核心类是Tinker类。Tinker类中就是利用单例和构建者模式创建Tinker,并将我们传入的参数利用构建者进行初始化。下面我们就看下加载补丁的方法onReceiveUpgradePatch。这里调用 Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);方法并将我们补丁文件的路径出入进去。点击去发现是一个接口。下面我们就来看下这个接口的实现类。还记得我们在参数中传入的DefaultPatchListener吗?这个就是实现这个方法的类。

public class DefaultPatchListener implements PatchListener {
    protected final Context context;

    public DefaultPatchListener(Context context) {
        this.context = context;
    }

    /**
     * when we receive a patch, what would we do?
     * you can overwrite it
     *
     * @param path
     * @return
     */
    @Override
    public int onPatchReceived(String path) {
        //对补丁文件的校验
        int returnCode = patchCheck(path);

        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            //启动加载补丁文件并修复BUG的服务
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
        }
        return returnCode;

    }

    //对补丁文件的校验 我们可以重写这个方法,去实现我们自己的校验,比如MD5检验等 也可以重写ShareConstants来实现我们自己的错误码
    protected int patchCheck(String path) {
        Tinker manager = Tinker.with(context);
        //check SharePreferences also
        if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {
            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        File file = new File(path);

        if (!SharePatchFileUtil.isLegalFile(file)) {
            return ShareConstants.ERROR_PATCH_NOTEXIST;
        }

        //patch service can not send request
        if (manager.isPatchProcess()) {
            return ShareConstants.ERROR_PATCH_INSERVICE;
        }

        //if the patch service is running, pending
        if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
            return ShareConstants.ERROR_PATCH_RUNNING;
        }
        return ShareConstants.ERROR_PATCH_OK;
    }

}

在这个onPatchReceived方法中启动了一个服务我们继续跟踪 TinkerPatchService.runPatchService(context, path);这个方法:

public class TinkerPatchService extends IntentService {
  
    ......
    
    public static void runPatchService(Context context, String path) {
        try {
            Intent intent = new Intent(context, TinkerPatchService.class);
            intent.putExtra(PATCH_PATH_EXTRA, path);
            intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
            context.startService(intent);
        } catch (Throwable throwable) {
            TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);
        }
    }

    ......

    @Override
    protected void onHandleIntent(Intent intent) {
        final Context context = getApplicationContext();
        Tinker tinker = Tinker.with(context);
        tinker.getPatchReporter().onPatchServiceStart(intent);

        if (intent == null) {
            TinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");
            return;
        }
        String path = getPatchPathExtra(intent);
        if (path == null) {
            TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
            return;
        }
        File patchFile = new File(path);

        long begin = SystemClock.elapsedRealtime();
        boolean result;
        long cost;
        Throwable e = null;

        increasingPriority();
        PatchResult patchResult = new PatchResult();
        try {
            if (upgradePatchProcessor == null) {
                throw new TinkerRuntimeException("upgradePatchProcessor is null.");
            }
            //处理补丁文件核心方法
            result = upgradePatchProcessor.tryPatch(context, path, patchResult);
        } catch (Throwable throwable) {
            e = throwable;
            result = false;
            //将处理的结果通知到我们传入的DefaultPatchListener中
            tinker.getPatchReporter().onPatchException(patchFile, e);
        }

        cost = SystemClock.elapsedRealtime() - begin;
        tinker.getPatchReporter().
            onPatchResult(patchFile, result, cost);

        patchResult.isSuccess = result;
        patchResult.rawPatchFilePath = path;
        patchResult.costTime = cost;
        patchResult.e = e;
        
        //开启加载补丁完成后服务,这就是我们上面重写安装补丁后的如何自定义行为的service类,默认是杀死当前进程。
        AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

    }

....

}

可以看到TinkerPatchService是一个自带子线程的服务开启这个服务后,就会走到onHandleIntent方法中,里面做了一些异常的判断。处理补丁文件的方法就是upgradePatchProcessor.tryPatch(context, path, patchResult);这个方法。点击入也是一个抽象的方法。我们来看他的默认的实现类UpgradePatch:

public class UpgradePatch extends AbstractPatch {
    private static final String TAG = "Tinker.UpgradePatch";

        .....

        //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
        //修复dex文件的核心方法入口
        if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
            return false;
        }
        //修复lib文件的核心方法入口
        if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
            return false;
        }
        //修复资源文件的核心方法入口
        if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
            return false;
        }
}

这个类的核心方法就是上面提到的三个方面的修复,再往里走下去就比较底层,涉及到Dexdiff算法,有兴趣的可以看下鸿洋大神关于diff算法的文章这个就不分析了。下面我们来用流程图总结下:

Tinker流程分析.png

大致的流程就是这样,具体的大家也可以自行研究下。

引入Tinker后的代码管理

在引入Tinker可以在git分支上专门创建一个hotFix分支专门是用来修改bug的分支,与开发分支(如dev)和主分支(master)区分开。具体情况以自己实际情况为主。


结语

经过几篇文章,分别讲解了AndFix与Tinker。相信大家对他们的差别也有了一定的认识。Tinker在使用中感觉还是有不少坑的,但是相对于AndFix,Tinker支持的比较全,并且支持在微信上也在使用。同时现在又支持Android热更新服务平台Bulgy,也更加方便。本人是菜鸟,难免分析不那么细致。如果有错误欢迎大家指出。如果有什么问题也可以留言大家一起解决。

源码地址 里面包含本文代码和Tinker-1.9.1版本的配置源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容