Android -热更新(2)- Tinker初步接入

版权声明:1、本BLOG的目的、形式及内容。   此BLOG为个人维护BLOG,内容均来自 原创及互连网转载。最终目的为收集整理自己需要的文章技术等内容,不涉及商业用途。\r\n 2、有关原创文章的版权   本BLOG上原创文章未经本人许可,不得用于商业用途及传统媒体。网络媒体转载请注明出处,否则属于侵权行为。\r\n 3、有关本站侵权   本BLOG所转载的内容,均是本人未发现有对文章版权声明的文章且 https://blog.csdn.net/shijianduan1/article/details/84791249

转载请声明:本文来自 https://blog.csdn.net/shijianduan1/article/details/84791249


参考:
Tinker官方说明 Github:Tinker 接入指南
Android实战——Tinker的集成和使用

Demo路径:项目已经上传GitHub

前言

这里主要说下Tinker不适用的地方,这样万一完全不兼容自己项目的话,也好提前有个准备。

2、Tinker缺点
Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
在Android N上,补丁对应用启动时间有轻微的影响;
不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
--------------------- 
作者:Hensen_ 
来源:CSDN 
原文:https://blog.csdn.net/qq_30379689/article/details/78575473 
版权声明:本文为博主原创文章,转载请附上博文链接!

本文Demo没有使用混淆,需要的话移步官网查看相关教程。

build.gradle

  1. 不知道从哪里说起,就先从build.gradle这个项目配置项说起吧。
  2. 这里 我使用 参考链接2 的方法,使用buildTinker.gradle 将Tinker的所有配置项都放在一个文件里面,方便修改(当然,这个文件就相对多了一层嵌套了)
  3. 下面的build.gradle是需要添加的
apply plugin: 'com.android.application'
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
// 加入Tinker生成补丁包的gradle
apply from: "buildTinker.gradle"

android {
    defaultConfig {
        //使用注解
        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
    }
}

dependencies {
    //tinker 添加
    //可选,用于生成application类,不打包到最后的apk 生成
    compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1')
    //tinker的核心库
    implementation('com.tencent.tinker:tinker-android-lib:1.9.1')
    implementation "com.android.support:multidex:1.0.3"
}

buildTinker.gradle

1.本文件是和 moudle的 builld.gradle同级
2.本文件是 Tinker的配置文件,相关配置均在这里
3.bakPath 此文件夹是用于存放老版本的apk和***.R.txt文件
4.需要打包patch的时候,需要修改 exttinkerOldApkPathtinkerApplyResourcePath 这两个需要手动,修改,R.txt文件和apk文件需要一一对应,不然会报class not found 的错误

//指定生成apk文件的存放位置
def bakPath = file("${buildDir}/bakApk/")
//参数配置
ext {
    //开启Tinker
    tinkerEnable = true
    //旧的apk位置,需要我们手动指定
    tinkerOldApkPath = "${bakPath}/tinker-release.apk"
    //旧的混淆映射位置,如果开启了混淆,则需要我们手动指定
    tinkerApplyMappingPath = "${bakPath}/"
    //旧的resource位置,需要我们手动指定
    tinkerApplyResourcePath = "${bakPath}/tinker-release-2018-12-10-14-34-47-R.txt"
    tinkerID = "1.0"
}

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
}

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        oldApk = getOldApkPath() //指定old apk文件路径
        ignoreWarning = false //不忽略tinker警告,出现警告则中止patch文件生成
        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.handsome.thinker.AppLike.MyTinkerApplication"] //指定加载patch文件时用到的类
        }
        lib {
            pattern = ["libs/*/*.so"] //指定so文件目录
        }
        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] //指定资源文件目录
            ignoreChange = ["assets/sample_meta.txt"] //指定不受影响的资源路径
            largeModSize = 100 //资源修改大小默认值
        }
        packageConfig {
            configField("patchMessage", "fix the 1.0 version's bugs")
            configField("patchVersion", "1.0")
        }
    }

    /**
     * 是否配置了多渠道
     */
    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0

    /**
     * 复制apk包和其它必须文件到指定目录
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("yyyy-MM-dd-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")
//                            outputFileName = fileName
                            variant.outputs.all {
                                outputFileName = "${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")
                        }
                    }
                }
            }
        }
    }
}

AndroidManifest.xml

  1. 这个文件要修改的是 指向的application,Tinker中的application是代码生成的,
    2.需要添加读写存储卡的权限,无论是下载patch,保存patch,以及删除patch,都需要权限。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="hotfix.sjd.tinker">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:name=".MyTinkerApplication">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

CustomTinkerLike.java

1.上面AndroidManifest.xml 中定义的 application .name :MyTinkerApplication就是在这里定义的

@DefaultLifeCycle(application = ".MyTinkerApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class CustomTinkerLike extends ApplicationLike {

    public CustomTinkerLike(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);
    }
}

CustomResultService.java

这个文件是 patch结束后的返回结果的接口

public class CustomResultService extends DefaultTinkerResultService {

    private static final String TAG = "Tinker.SampleResultService";

    /**
     * patch文件的最终安装结果,默认是安装完成后杀掉自己进程
     * 此段代码主要是复制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) {
            //删除patch包
            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!");
            }
        }
    }
}

CustomPatchListener.java

这个文件是校验 下载的patch.apk 是否是完整的

public class CustomPatchListener extends DefaultPatchListener {

    private String currentMD5;

    public void setCurrentMD5(String md5Value) {
        this.currentMD5 = md5Value;
    }

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

    /**
     * patch的检测
     *
     * @param path
     * @return
     */
    @Override
    protected int patchCheck(String path, String patchMd5) {
        //MD5校验的工具可以网上查找
        //这里要求我们在初始化Tinker的时候加上MD5的参数
        //增加patch文件的md5较验
        if (!isFileMD5Matched(path, currentMD5)) {
            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        return super.patchCheck(path, patchMd5);
    }


    public static boolean isFileMD5Matched(String path, String currentMD5) {
        String r = getFileMd5(new File(path));
        return TextUtils.equals(r, currentMD5);
    }

    /**
     * FileInputStream字节流方式
     *
     * @param file 文件
     * @return 文件MD5
     */
    public static String getFileMd5(File file) {
        MessageDigest messageDigest;
        FileInputStream fis = null;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
            if (file == null) {
                return "";
            }
            if (!file.exists()) {
                return "";
            }
            int len = 0;
            fis = new FileInputStream(file);
            //普通流读取方式
            byte[] buffer = new byte[1024 * 1024 * 10];
            while ((len = fis.read(buffer)) > 0) {
                //该对象通过使用 update()方法处理数据
                messageDigest.update(buffer, 0, len);
            }
            BigInteger bigInt = new BigInteger(1, messageDigest.digest());
            String md5 = bigInt.toString(16);
            while (md5.length() < 32) {
                md5 = "0" + md5;
            }
            return md5;
        } catch (NoSuchAlgorithmException | IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                    fis = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "";
    }
}

TinkerManager.java

统一管理Tinker各个方法接口文件

public class TinkerManager {

    private static boolean isInstalled = false;
    // 这里的ApplicationLike可以理解为Application的载体
    private static ApplicationLike mAppLike;
    private static CustomPatchListener mPatchListener;

    /**
     * 默认初始化Tinker
     *
     * @param applicationLike
     */
    public static void installTinker(ApplicationLike applicationLike) {
        mAppLike = applicationLike;
        if (isInstalled) {
            return;
        }

        TinkerInstaller.install(mAppLike);
        isInstalled = true;
    }

    /**
     * 初始化Tinker,带有自定义模块
     * <p>
     * 1、CustomPatchListener
     * 2、CustomResultService
     *
     * @param applicationLike
     * @param md5Value        服务器下发的md5
     */
    public static void installTinker(ApplicationLike applicationLike, String md5Value) {
        mAppLike = applicationLike;
        if (isInstalled) {
            return;
        }

        mPatchListener = new CustomPatchListener(getApplicationContext());
        mPatchListener.setCurrentMD5(md5Value);

        // Load补丁包时候的监听
        LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());
        // 补丁包加载时候的监听
        PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());
        AbstractPatch upgradePatchProcessor = new UpgradePatch();
        TinkerInstaller.install(applicationLike,
                loadReporter,
                patchReporter,
                mPatchListener,
                CustomResultService.class,
                upgradePatchProcessor);
        isInstalled = true;
    }

    /**
     * 增加补丁包
     *
     * @param path
     */
    public static void addPatch(String path) {
        if (Tinker.isTinkerInstalled()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
        }
    }

    /**
     * 获取上下文
     *
     * @return
     */
    private static Context getApplicationContext() {
        if (mAppLike != null) {
            return mAppLike.getApplication().getApplicationContext();
        }
        return null;
    }
}

实际调用

这里默认patch在 /sdcard/data/app/包名/cache下

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
    }

    private String mPath;


    /**
     * 加载Tinker补丁
     *
     * @param view
     */
    public void Fix(View view) {
        File patchFile = new File(mPath, "patch_signed.apk");
        if (patchFile.exists()) {
            TinkerManager.addPatch(patchFile.getAbsolutePath());
            Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
        }
    }
}

效果图

  1. 编译第一个版本,apk文件 和 R.txt文件
    在这里插入图片描述

  2. 将apk文件 和 R.txt文件 均移到指定文件夹(buildTinker.gradle中配置的,需要完全对应)
    在这里插入图片描述

  3. 点击右侧gradle里的 tinker/tinkerPatchRelease脚本来生成patch

  4. 在这里插入图片描述

  5. 最后在 out/tinkerPatch路径 下面会有patch生成,patch有两个一个签名 一个未签名,看情况使用。

最后

  1. 注意: 我这边编译的是 release签名版本,然后打patch的时候也是release的;其他情况好像是失败的,具体还在摸索中。
  2. 看了下QZone的热更新,感觉像是Tinker的前身,而且很显然Tinker更成熟适用,所以QZone 就不学习了

好吧 感觉有点敷衍了事,仅仅将别人的方法实现了一遍,就结束了。
等后面熟悉了,再来回顾一下。

欢迎各位看官 解惑。

猜你喜欢

转载自blog.csdn.net/shijianduan1/article/details/84791249