Tinker热修复之 —— Gradle接入

Posted 郭霖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tinker热修复之 —— Gradle接入相关的知识,希望对你有一定的参考价值。



今日科技快讯


10月17日,据路透社报道,中国智能手机制造商华为公司发布了一款新的旗舰智能手机Mate 10系列,其改进功能甚至可与苹果或三星的高端手机相媲美,而性价比更高。华为的目标是通过技术进步来凸显自己,这将使其在高端市场竞争中处于有利位置,进而提高盈利能力。


作者简介


本篇来自 BigSweetee 的投稿,讲解了如何用 gradle接入热修复tinker,希望对大家有所帮助!

http://blog.csdn.net/qq_15527709/


前言


今天研究了一天的热修复,热修复,简单的来讲就是在不需要发包的情况下,修改你线上应用的bug,接入使用后对于我这种小白来说还是很神奇的,同时也考虑了一下,要不要接入我们的项目中,这样就不用因为一个小BUG而去再次发包了,不过,就算要接入项目中,也还有很多坑需要踩,tinker有俩种接入方式,一种命令行接入,一种是gradle接入,本篇只讲 gradle接入,下篇我在补充命令行,主要用于自己做个记录,把踩得坑和感想写下来. 


正文


将这个 C/C++编译链接生成二进制文件的这个过程是谁做的?

首先,基本的配置 

在 project 的 build中配置如下

dependencies { 
        classpath 'com.android.tools.build:gradle:2.2.2' 
        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}" 
        // NOTE: Do not place your application dependencies here; they belong 
        // in the individual module build.gradle files 
    }

TINKER_VERSION 需要在 gradle.properties 中进行配置

TINKER_VERSION=1.7.7

https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle 

于是我全部 copy 了过来,不过里面的几个地方还是需要修改的 。

这里因为源码太多,有兴趣的同学可以点击下方阅读全文,去原文中进行查看学习。

主要修改了下面几个地方

 buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""

官方文档默认是将git提交的记录作为think_id记录下来,这里我不需要,所以我改成下图

buildConfigField "String", "TINKER_ID", "\"1.0\""

我直接写死,需要git提交记录作为tinker_id的也可以按照官方文档推荐的。同时,这些地方也可以相应的替换掉

//废弃 /*def getTinkerIdValue() {    return hasProperty("TINKER_ID") ? TINKER_ID : gitSha() }*/ tinkerId = "1.0"/*getTinkerIdValue()*/

接下来修改自己的签名文件,我这里配置的文件路径是自己电脑上面的,这个位置大家需要修改为自己的签名文件路径,也可以按照我的去生成一个签名文件,我相信这个还是很简单的。

//配置自己的签名文件,签名文件的生成和导入可以去百度,本篇不讲解 signingConfigs { 
    release { 
         try { 
             keyAlias 'china' 
             keyPassword '123456' 
             storeFile file('D:/work/release.jks') 
             storePassword '123456' 
         } catch (ex) { 
             throw new InvalidUserDataException(ex.toString()) 
         } 

   }    debug {         storeFile file('D:/work/debug.jks')         keyAlias 'china'         keyPassword '123456'         storePassword '123456'    } }
buildTypes { 
     release { 
         minifyEnabled true 
         signingConfig signingConfigs.release 
         proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 
     } 
     debug { 
         debuggable true 
         minifyEnabled false 
         signingConfig signingConfigs.debug 
         proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 
     } 
 }

官网文档没有设置 debug 的 proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’ 也就是说没有应用混淆文件,这样是不会生成 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") 
                        } 
                    } 
                } 
            } 
        }

这下面主要是设置生成的文件所在的目录是什么。配置完上面之后,build就配置好了,接下来配置 application, 先上代码:

package com.anlaiye.swt.gradletest; 

import android.app.Application; 
import android.content.Context; 
import android.content.Intent; 
import android.support.multidex.MultiDex; 

import com.tencent.tinker.anno.DefaultLifeCycle; 
import com.tencent.tinker.lib.tinker.TinkerInstaller; 
import com.tencent.tinker.loader.app.ApplicationLike; 
import com.tencent.tinker.loader.shareutil.ShareConstants; 

@DefaultLifeCycle(application = ".SimpleTinkerInApplication", 
        flags = ShareConstants.TINKER_ENABLE_ALL, 
        loadVerifyFlag =true) 
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); 
        MultiDex.install(base); 
        TinkerInstaller.install(this); 

    } 

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

这个 application 官网有提供的,也可以copy我的,ApplicationLike 并不是一个application,真正的 application 是 @DefaultLifeCycle(application = “.SimpleTinkerInApplication” 这个,所以在 androidmanifest 中配置 applica的 name:

<application    android:name=".SimpleTinkerInApplication"    android:allowBackup="true"    android:icon="@mipmap/ic_launcher"    android:label="@string/app_name"    android:supportsRtl="true"    android:theme="@style/AppTheme"> 
    <activity android:name=".MainActivity"> 
        <intent-filter> 
            <action android:name="android.intent.action.MAIN"/> 

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

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

同时配置读取sd卡的权限,接下来测试:

<?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:id="@+id/activity_main"    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="com.anlaiye.swt.gradletest.MainActivity">    <TextView        android:id="@+id/tv"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="111"/>    <Button        android:layout_below="@+id/tv"        android:onClick="loadPath"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="更新"/>
</RelativeLayout>

第一次我设置 textview 的值是111,然后设置按钮调用 onclick:

package com.anlaiye.swt.gradletest; 

import android.os.Bundle; 
import android.os.Environment; 
import android.support.v7.app.AppCompatActivity; 
import android.view.View; 
import android.widget.Toast; 

import com.tencent.tinker.lib.tinker.TinkerInstaller; 

import java.io.File; 

public class MainActivity extends AppCompatActivity { 

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

    //加载补丁 
    public void loadPath(View view) { 
        String path = Environment.getExternalStorageDirectory().getAbsolutePath()+"/patch_signed_7zip.apk"; 
        File file = new File(path); 
        if (file.exists()){ 
            Toast.makeText(this, "补丁已经存在", Toast.LENGTH_SHORT).show(); 
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path); 
        }else { 
            Toast.makeText(this, "补丁已经不存在", Toast.LENGTH_SHORT).show(); 
        } 
    } 

}

然后我运行APP,会生成如下的目录结构: 

Tinker热修复之 —— Gradle接入 

同时界面效果 

Tinker热修复之 —— Gradle接入 

接下来做如下修改:

//老版本的文件所在的位置,大家也可以动态配置,不用每次都在这里修改 ext { 
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build? 
    tinkerEnabled = true 

    //for normal build 
    //old apk file to build patch apk 
    tinkerOldApkPath = "${bakPath}/app-release-0313-16-49-55.apk" 
    //proguard mapping file to build patch apk 
    tinkerApplyMappingPath = "${bakPath}/app-release-0313-16-49-55-mapping.txt" 
    //resource R.txt to build patch apk, must input if there is resource changed 
    tinkerApplyResourcePath = "${bakPath}/app-release-0313-16-49-55-R.txt" 

    //only use for build all flavor, if not, just ignore this field 
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47" }

注意这里是 release 版本,不能 debug 版本,release 版本混着来,比如 oldapk 是 debug 版本,新的 apk 是 release版本,这样是不行的,统一用 release 版本。

这里我将对应路径改为 bakapk 中第一次运行生成的apk文件名字,比如第一次生成的 apk文件名为 app-release-0313-16-49-55.apk,所以我把 tinkerOldApkPath 的路径后面的名字修改为对应的 app-release-0313-16-49-55.apk,mapping 文件和 R 文件路径也要对应修改,最后一项 tinkerBuildFlavorDirectory 可以忽略,我一直觉得这里太死板,应该像设置tinker_id 那样对这里进行动态的配置,不用每次都来修改。

接下来我把 textview 改成2222,用来和第一次作区别。然后点击: 

Tinker热修复之 —— Gradle接入

生成目录如下

Tinker热修复之 —— Gradle接入 

注意箭头所指的文件 ,我们在 onclick 中执行的文件名就是这个,这个文件就是我们需要的最终的文件,然后将这个文件导入SD卡的最外层目录,点击更新按钮,点击后如下 :

Tinker热修复之 —— Gradle接入 

因为模拟器不好导入patch的apk所以直接截图了,成功。

本篇只是简单的演示了整个流程,整理了一下容易踩得坑,可以优化的地方还有很多,大家可以在这个基础上自行发挥。


总结


需要准备的工作:

总结一下自己碰到的坑, 

  • 首先没有在gradle.properties中配置tinker_id,会提示错误tinker_id not set!!! 

  • debug 没有配置 proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’ 

导入生成的apk一直没有mapping文件,找了半天才发现。

最后说下热修复的大概流程 

最终我们需要的是一个patch.apk文件,这个文件是通过老apk和新apk对比生成的, 具体怎么对比和生成,是tinker控制的,所以我们需要导入他的依赖包,新APK和老APK的mapping 必须是一致的,所以我们需要把老APK的mapping保存起来,方便新APK与他对比。 这样就能得到最终的patch,然后调用 sdk 的TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path),这样就实现了热修复的目的。

http://download.csdn.net/download/qq_15527709/10017530 

https://github.com/BigSweet/TinkerGradleDemo


欢迎长按下图 -> 识别图中二维码

以上是关于Tinker热修复之 —— Gradle接入的主要内容,如果未能解决你的问题,请参考以下文章

Android 热修复 Tinker 源码分析之DexDiff / DexPatch

tinker接入

Android热修复之微信Tinker使用初探

Tinker 热修复接入

Tinker 热修复接入

Android 热修复 Tinker接入及源码浅析