微信热修复 Tinker 和 flavor 更新

本文详细介绍了如何在AS3.0.1环境下集成微信的热修复库Tinker,并在使用flavor时进行热修复的配置与实践。通过步骤演示了从集成Tinker库、自定义MyApplicationLike、打包apk,到多版本更新和flavor更新的全过程,包括Gradle配置、权限设置、补丁打包与应用等关键操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

好久都没有写博客了,最近看了看关于热更新的内容。主要有阿里的,腾讯的,而实验的则是使用的是微信的热修复 Tinker ,以及在使用了 flavor 后的 热修复。

本文的内容是基于 AS3.0.1,注意:本例所截图的内容可能会和讲解的不一样,原因是,讲解的是按照versionCode  1,versionName "1.0"来的,截图是经过多次版本更新后来的


一.  首先集成微信的 Tinker,其github的链接:https://github.com/Tencent/tinker ,参考sample进行配置:
    (1)在 project 目录下的 build.gradle 中的 dependencies 中添加
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')

    (2)在 module 目录下的 build.gradle 中的 dependencies 中添加:
        // 可选,用于生成application类
        compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1') { changing = true }
        // tinker的核心库
        implementation('com.tencent.tinker:tinker-android-lib:1.9.1') { changing = true }
        annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
        其中的几个库引用后面的 { changing = true }不能少

    (3)在 gradle.properties 中添加:
        android.enableAapt2=false
        在 module 的 build.gradle 中的 defaultConfig 中添加 :
        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
        这样在编译的时候才不报~~错误


    (4)添加 keystore.jks 到项目中配置好 signingConfigs,该 signingConfigs 要在 buildTypes 之前,否则报错


     (5)在 module 的 build.gradle 中添加如下配置:(该配置值包含热更新,不包含多渠道打包)

        直接添加在最外层

def gitSha() {
            try {
                String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
                if (gitRev == null) {
                    throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
                }
                return gitRev
            } catch (Exception e) {
                throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
            }
        }
        // 热更新时,旧的 .apk,R.txt 目录就在本目录下
        def bakPath = file("./tinker-old/")
        ext {
            // 每次更新的 tinkerId 都保持了不同,所以这里使用了 versionName 和 versionCode
            // tinkerId 可以随意填写
            tinkerId = "tinker_id_" + android.defaultConfig.versionName + "_" + android.defaultConfig.versionCode
            //是否打开tinker的功能
            tinkerEnabled = true
            // 旧的 .apk 既本次更新前的版本,该版本必须存在,否则编译不过
            // 在 bakPath 目录下相应的版本版本号填写在此处
            tinkerOldApkPath = "${bakPath}/app-release-1.5-6.apk"
            // 该版本为更新的版本,在 bakPath 目录下相应的版本版本号填写在此处
            tinkerApplyMappingPath = "${bakPath}/app-release-1.6-7-mapping.txt"
            // 该版本为更新的版本,在 bakPath 目录下相应的版本版本号填写在此处
            tinkerApplyResourcePath = "${bakPath}/app-release-1.6-7-R.txt"
        }

        def getOldApkPath() {
            return ext.tinkerOldApkPath
        }

        def getApplyMappingPath() {
            return ext.tinkerApplyMappingPath
        }

        def getApplyResourceMappingPath() {
            return ext.tinkerApplyResourcePath
        }

        def getTinkerIdValue() {
            return ext.tinkerId
        }

        def buildWithTinker() {
            return ext.tinkerEnabled
        }

        def getTinkerBuildFlavorDirectory() {
            return ext.tinkerBuildFlavorDirectory
        }

        if (buildWithTinker()) {
            // 必须有
            apply plugin: 'com.tencent.tinker.patch'

            tinkerPatch {
                oldApk = getOldApkPath()
                ignoreWarning = true
                useSign = true
                buildConfig {
                    applyMapping = getApplyMappingPath()
                    applyResourceMapping = getApplyResourceMappingPath()
                    tinkerId = getTinkerIdValue()
                    /**
                     * 是否开启加固 ,默认 false
                     */
                    isProtectedApp = false
                }

                dex {
                    dexMode = "jar"
                    pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]
                }

                lib {
                    pattern = ["lib/*/*.so"]
                }

                res {
                    pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
                    ignoreChange = ["assets/sample_meta.txt"]
                    largeModSize = 100
                }

                packageConfig {
                    configField("patchMessage", "tinker is sample to use")
                    configField("platform", "all")
                    configField("patchVersion", "1.0")
                }
                sevenZip {
                    zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
                }
            }

            android.applicationVariants.all { variant ->
                def taskName = variant.name

                tasks.all {
                    if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                        it.doLast {
                            copy {
                                def fileNamePrefix = "${project.name}-${variant.baseName}"
                                def newFileNamePrefix = "${fileNamePrefix}-" + android.defaultConfig.versionName + "-" + android.defaultConfig.versionCode

        //                        def destPath = bakPath
                                def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                                from variant.outputs.first().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. bakPath 的路径可以自己指定;
            2. ext 目录下的版本号(如果是新项目,没有更新的,则所有的版本
                内容都相同,如:app-release-1.0-1.apk",app-release-1.0-1-mapping.txt,app-release-1.0-1-R.txt,其中 1 为
                versionCode ,1.0 为 versionName),待更新的版本的 .apk 一定不能少;
            3. tinkerPatch 下的 ignoreWarning 这里设置的为 true ,tinkerId 要有值,要不然无法通过;
            4. applicationVariants 下面的 copy 方法中的 from variant.outputs.first().outputFile 要注意,不要写成了
                from variant.outputs.outputFile 否则在 更新的时候会出错,def destPath = bakPath 在热更新时也可以,
                但是在使用 flavor 打包时,容易出现 .apk 找不到,所有还是建议写成上文中的方式。


二. 上文中添加了 compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1') { changing = true } 用于生产 application 类,
    所以自定义了一个 MyApplicationLike 类,具体的参照 MyApplicationLike 该类:

@DefaultLifeCycle(application = ".MyApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
    public class MyApplicationLike extends DefaultApplicationLike {

        public MyApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                     long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
            super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
        }

        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
        @Override
        public void onBaseContextAttached(Context base) {
            super.onBaseContextAttached(base);

            TinkerInstaller.install(this);

        }

        @Override
        public void onCreate() {
            super.onCreate();
            // 原有Application逻辑可以写在这里
        }
    }
    

其中注解当中 DefaultLifeCycle 里面的 MyApplication 为本项目的 application 的 name。


三. 在 AndroidManifest 的 application 里面添加上文 MyApplicationLike 注解当中的类: android:name=".MyApplication",并添加
    相应的读写存储的权限:
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>


四. 打包,根据 module 目录下 build.gradle 中的相应文件进行打包,打包好后,在项目 app 目录下会增加一个 release 文件夹,该
    文件下就有打包好的 app-release.apk。同时在 app 目录下还会生成一个 tinker-old 目录,该目录就为上文中配置的 bakPath。
    本目录下的东西暂时不要删除,当有新版本更新时,这时的版本为旧版本,在更新时,需要使用。


五. 安装好打包的 .apk 后,修改部分代码;再修改 versionCode 和 versionName 的值,如 versionCode 2   versionName "1.1";
    再次进行打包,打包好后就在 tinker-old 目录中就会新增: app-release-1.1-2.apk,app-release-1.1-2-R.txt。


本图中的 apk 为多层更新后的版本。


六. 配置 module 目录下 build.gradle 中的 ext 当中的版本号,如:
     tinkerOldApkPath = "${bakPath}/app-release-1.0-1.apk"
     tinkerApplyMappingPath = "${bakPath}/app-release-1.1-2-mapping.txt"
     tinkerApplyResourcePath = "${bakPath}/app-release-1.1-2-R.txt"


图中的版本为多次更新后的。


七. 可以在 AndroidStudio 左下角的  Terminal 当中输入 gradlew tinkerPatchRelease ,或者在 Gradle projects 中找到相应的 project 运行即可。
    当看到 Terminal 当中更新成功后,会有个 Success 什么的提示。这时就只需把 app/build/outputs/apk/tinkerPatch 下的
    patch_signed_7zip.apk 复制到相应的文件当中(由于本例是使用 File file = new File(getExternalCacheDir(), "/patch_signed_7zip.apk");
    创建的文件,所以在手机当中 Android/data/"包名"/cache 当中)。点击 “下载补丁” 即可,更新成功后,需要再次杀死进程,再次
    启动即可看到更新后的版本。

MainActivity:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

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

        initViews();
    }

    private void initViews() {
        final File file = new File(getExternalCacheDir(), "/patch_signed_7zip.apk");

        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

   //     final String str = Environment.getExternalStorageDirectory().getAbsolutePath() +  "/patch_signed_7zip.apk";

        Button loadPatchButton = findViewById(R.id.loadPatch);
        Log.d("TAG", "file=" + file.getAbsolutePath());


        loadPatchButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
 //                 TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), str);
                if (file.exists()) {
                    TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), file.getPath());
                } else {
                    Toast.makeText(MainActivity.this, "目录不存在", Toast.LENGTH_SHORT).show();
                }
            }
        });

        Button loadLibraryButton = findViewById(R.id.loadLibrary);

        loadLibraryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // #method 1, hack classloader library path
                //            TinkerLoadLibrary.installNavitveLibraryABI(getApplicationContext(), "armeabi");
                //             System.loadLibrary("stlport_shared");
            }
        });

        Button cleanPatchButton = findViewById(R.id.cleanPatch);

        cleanPatchButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Tinker.with(getApplicationContext()).cleanPatch();
            }
        });

        Button killSelfButton = findViewById(R.id.killSelf);

        killSelfButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
                android.os.Process.killProcess(android.os.Process.myPid());
            }
        });

        Button buildInfoButton = findViewById(R.id.showInfo);

        buildInfoButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                boolean b = showInfo(MainActivity.this);
                if (b) {

                }
            }
        });
	// 修改后,需要更新的内容
        findViewById(R.id.main_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, TwoActivity.class));
            }
        });
    }

    public boolean showInfo(Context context) {
        // add more Build Info
        final StringBuilder sb = new StringBuilder();
        Tinker tinker = Tinker.with(getApplicationContext());
        if (tinker.isTinkerLoaded()) {
            sb.append(String.format("[patch is loaded] \n"));
//            sb.append(String.format("[buildConfig TINKER_ID] %s \n", BuildInfo.TINKER_ID));
//            sb.append(String.format("[buildConfig BASE_TINKER_ID] %s \n", BaseBuildInfo.BASE_TINKER_ID));

//            sb.append(String.format("[buildConfig MESSSAGE] %s \n", BuildInfo.MESSAGE));
            sb.append(String.format("[TINKER_ID] %s \n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName(ShareConstants.TINKER_ID)));
            sb.append(String.format("[packageConfig patchMessage] %s \n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName("patchMessage")));
            sb.append(String.format("[TINKER_ID Rom Space] %d k \n", tinker.getTinkerRomSpace()));

        } else {
            sb.append(String.format("[patch is not loaded] \n"));
//            sb.append(String.format("[buildConfig TINKER_ID] %s \n", BuildInfo.TINKER_ID));
//            sb.append(String.format("[buildConfig BASE_TINKER_ID] %s \n", BaseBuildInfo.BASE_TINKER_ID));

//            sb.append(String.format("[buildConfig MESSSAGE] %s \n", BuildInfo.MESSAGE));
            sb.append(String.format("[TINKER_ID] %s \n", ShareTinkerInternals.getManifestTinkerID(getApplicationContext())));
        }
        sb.append(String.format("[BaseBuildInfo Message] %s \n", BaseBuildInfo.TEST_MESSAGE));

        final TextView v = new TextView(this);
        v.setText(sb);
        v.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
        v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10);
        v.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        v.setTextColor(0xFF000000);
        v.setTypeface(Typeface.MONOSPACE);
        final int padding = 16;
        v.setPadding(padding, padding, padding, padding);

        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setCancelable(true);
        builder.setView(v);
        final AlertDialog alert = builder.create();
        alert.show();
        return true;
    }

    @Override
    protected void onResume() {
        super.onResume();
        Utils.setBackground(false);

    }

    @Override
    protected void onPause() {
        super.onPause();
        Utils.setBackground(true);
    }
}
参照官方的 sample 修改了少量代码

八. flavor 更新,build.gradle 当中
    (1)在 build.gradle 的相应位置添加 :
        //  多dex 打包的类库
        implementation "com.android.support:multidex:1.0.2"


    (2)在 defaultConfig 中添加 :
        flavorDimensions "versionCode"
        multiDexEnabled true
        /**
         * buildConfig can change during patch!
         * we can use the newly value when patch
         */
         buildConfigField "String", "MESSAGE", "\"I am the base apk\""
         /**
          * client version would update with patch
          * so we can get the newly git version easily!
          */
         // buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
         buildConfigField "String", "PLATFORM", "\"all\""
        其中:flavorDimensions "versionCode" 是为了防止 AS3.0 以后会出现 Error:All flavors must now belong to a named flavor dimension 异常


    (3)在 android 下添加:

dexOptions {
                jumboMode = true
         }
         //用于测试 flavors
         productFlavors {
                 flavor {
                     applicationId 'com.anti.updateonline'
                 }
                 flavor1 {
                     applicationId 'com.anti.updateonline.flavor1'
                 }
                 flavor2 {
                     applicationId 'com.anti.updateonline.flavor2'
                 }
         }
注:此处采用了微信 sample 当中的方法,网上还有其他方法在  productFlavors 中进行添加


  (4)在 if (buildWithTinker()) {} 方法中添加 :

List<String> flavors = new ArrayList<>()
            project.android.productFlavors.each { flavor ->
                flavors.add(flavor.name)
            }
            boolean hasFlavors = flavors.size() > 0
            def date = new Date().format("MMdd-HH-mm-ss")

            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"
                                }

                            }
                        }
                    }
                }
 在 buildConfig 下添加:keepDexApply = false
 其中:def date = new Date().format("MMdd-HH-mm-ss") 的时间为, tinker-old 目录下的 app-0101-19-47-34 文件夹名后部分;


  (5)在 ext 中添加:

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

            project.android.productFlavors.each { flavor ->
                tinkerOldApkPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor-release-1.8-9.apk"
                tinkerApplyMappingPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor-release-1.9-10-mapping.txt"
                tinkerApplyResourcePath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor-release-1.9-10-R.txt"

                tinkerOldApkPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor1-release-1.8-9.apk"
                tinkerApplyMappingPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor1-release-1.9-10-mapping.txt"
                tinkerApplyResourcePath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor1-release-1.9-10-R.txt"

                tinkerOldApkPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor2-release-1.8-9.apk"
                tinkerApplyMappingPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor2-release-1.9-10-mapping.txt"
                tinkerApplyResourcePath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor2-release-1.9-10-R.txt"
            }
其中的 tinkerBuildFlavorDirectory = "${bakPath}/app-0101-19-47-34" 为(4)中介绍的目录;project.android.productFlavors.each { } 方法用于遍历 (3)处的 flavor1 等。本文最后会给出完整的 build.gradle 


  (6)拷贝官方 sample 当中的 TinkerManager,SampleResultService 等并在 AndroidManifest 进行相应的配置,MyApplicationLike类当中添加相应的代码。

MyApplicationLike 类:

@DefaultLifeCycle(application = ".MyApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class MyApplicationLike extends DefaultApplicationLike {

    public MyApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);

        MultiDex.install(base);

        TinkerManager.setTinkerApplicationLike(this);
        TinkerManager.initFastCrashProtect();
        //should set before tinker is installed
        TinkerManager.setUpgradeRetryEnable(true);

//        TinkerInstaller.install(this);
        TinkerManager.installTinker(this);
        Tinker tinker = Tinker.with(getApplication());
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 原有Application逻辑可以写在这里
    }
}
AndroidManifest 中:

<service
            android:name=".service.SampleResultService"
            android:exported="false"/>

   (7)打包,在 app/build/outputs/apk 下生成 flavor, flavor1, flavor2 文件夹,其下还有相应的 apk,将生成的几个 apk 安装
        在手机中,并且一定要将这生成的几个 apk 拷贝到 tinker-old/app-xxxx 目录下相同的包名目录中,如 flavor 到
        tinker-old/app-xxxx/flavor 目录下,这是旧的 apk,不能拷贝更新后打包的 apk 到该目录下,否则不能不能实现更新成功
。(这只是对于本例的配置而言)


  (8)修改部分代码,改变 versionCode 和 versionName,再按照 tinker-old 中 app- 目录修改 ext 当中 project.android.productFlavors.each 方法中的 tinkerOldApkPath 等(如 flavor 文件中的 1.8-9.apk,新的 tinkerApplyMappingPath 按照 versionCode 和 versionName 的改变进行修改)。


    (9)运行 AS 右上角的 Gradle tinker 下的 tinkerPatchAllFlavorRelease 即可得到所有flavor的补丁包。


(10)app/build/outputs/apk/flavor/tinkerPatch/flavor/release 目录下就有 patch_signed_7zip.apk 补丁包,拷贝各自的
        patch_signed_7zip.apk 补丁包到各自的 cache 当中,运行,点击更新即可各自更新。


本例完整的 build.gradle

apply plugin: 'com.android.application'

def gitSha() {
    try {
        String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        if (gitRev == null) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
        }
        return gitRev
    } catch (Exception e) {
        throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
    }
}

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.anti.updateonline"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 10
        versionName "1.9"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }

        flavorDimensions "versionCode"

        multiDexEnabled true
        /**
         * buildConfig can change during patch!
         * we can use the newly value when patch
         */
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
        /**
         * client version would update with patch
         * so we can get the newly git version easily!
         */
 //       buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
        buildConfigField "String", "PLATFORM", "\"all\""
    }
    //recommend
    dexOptions {
        jumboMode = true
    }
    signingConfigs {
        release {
            storeFile file("")
            storePassword ""
            keyAlias ""
            keyPassword ""
            v2SigningEnabled false
        }
    }

    aaptOptions{
        cruncherEnabled false
    }

    //use to test flavors support
    productFlavors {
        flavor {
            applicationId 'com.anti.updateonline'
        }
        flavor1 {
            applicationId 'com.anti.updateonline.flavor1'
        }
        flavor2 {
            applicationId 'com.anti.updateonline.flavor2'
        }
    }

    lintOptions {
        checkReleaseBuilds false
        abortOnError false
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
        debug {
            signingConfig signingConfigs.release
        }
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    //可选,用于生成application类
    compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1') { changing = true }
    //tinker的核心库
    implementation('com.tencent.tinker:tinker-android-lib:1.9.1') { changing = true }
    annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
    implementation "com.android.support:multidex:1.0.2"
}


def bakPath = file("./tinker-old/")

ext {
    tinkerId = "tinker_id_" + android.defaultConfig.versionName + "_" + android.defaultConfig.versionCode
    tinkerEnabled = true
    tinkerOldApkPath = "${bakPath}/app-release-1.5-6.apk"
    tinkerApplyMappingPath = "${bakPath}/app-release-1.6-7-mapping.txt"
    tinkerApplyResourcePath = "${bakPath}/app-release-1.6-7-R.txt"

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

    project.android.productFlavors.each { flavor ->
        tinkerOldApkPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor-release-1.8-9.apk"
        tinkerApplyMappingPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor-release-1.9-10-mapping.txt"
        tinkerApplyResourcePath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor-release-1.9-10-R.txt"

        tinkerOldApkPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor1-release-1.8-9.apk"
        tinkerApplyMappingPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor1-release-1.9-10-mapping.txt"
        tinkerApplyResourcePath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor1-release-1.9-10-R.txt"

        tinkerOldApkPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor2-release-1.8-9.apk"
        tinkerApplyMappingPath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor2-release-1.9-10-mapping.txt"
        tinkerApplyResourcePath = "$tinkerBuildFlavorDirectory/${flavor.name}/app-flavor2-release-1.9-10-R.txt"
    }

}

def getOldApkPath() {
    return ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return ext.tinkerId
}

def buildWithTinker() {
    return ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

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

    tinkerPatch {
        oldApk = getOldApkPath()
        ignoreWarning = true
        useSign = true
        buildConfig {
            applyMapping = getApplyMappingPath()
            applyResourceMapping = getApplyResourceMappingPath()
            tinkerId = getTinkerIdValue()
            /**
             * if keepDexApply is true, class in which dex refer to the old apk.
             * open this can reduce the dex diff file size.
             */
            keepDexApply = false
            /**
             * 是否开启加固 ,默认 false
             */
            isProtectedApp = false
        }

        dex {
            dexMode = "jar"
            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]
        }

        lib {
            pattern = ["lib/*/*.so"]
        }

        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            ignoreChange = ["assets/sample_meta.txt"]
            largeModSize = 100
        }

        packageConfig {
            configField("patchMessage", "tinker is sample to use")
            configField("platform", "all")
            configField("patchVersion", "1.0")
        }
        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
    }


    List<String> flavors = new ArrayList<>()
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    def date = new Date().format("MMdd-HH-mm-ss")

    android.applicationVariants.all { variant ->
        def taskName = variant.name

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = "${fileNamePrefix}-" + android.defaultConfig.versionName + "-" + android.defaultConfig.versionCode

//                        def destPath = bakPath
                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.first().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"
                    }

                }
            }
        }
    }
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值