Android-Tinker熱修復(fù)接入實(shí)踐問(wèn)題記錄(自己認(rèn)為的較完善的理解)

上一篇我們實(shí)踐了美團(tuán)的方法級(jí)別的熱修復(fù)MonkeyLei:Android-美團(tuán)Robust熱修復(fù)接入實(shí)踐問(wèn)記錄 胳嘲,之后項(xiàng)目用這個(gè)就足夠了。至于17,18年一些人提到的其他的已經(jīng)幾年沒(méi)維護(hù)的方法級(jí)別熱修復(fù)框架烈疚,就不建議用了适揉。 如果有更復(fù)雜的需求留攒,可以試試騰訊的Tencent/tinker, Bugly集成的應(yīng)該也是這個(gè)啦。

Let's go....

1. 按照官方先配置一波 https://github.com/Tencent/tinker#getting-started

image

1.1 這一步?jīng)]問(wèn)題嫉嘀,不過(guò)我的環(huán)境是 - 其他的環(huán)境沒(méi)試過(guò)哈,不過(guò)按照最新的集成方式應(yīng)沒(méi)問(wèn)題魄揉,至少到現(xiàn)在2019.10.29或者2020年中估計(jì)都沒(méi)問(wèn)題剪侮,畢竟Android10都出來(lái)。洛退。

image
image
image

1.2 所以針對(duì)官方的配置瓣俯,去除了一些警告參數(shù)外,另外還有利用插件com.tencent.tinker:tinker-android-anno生成MyApplication的話兵怯, annotationProcessor的配置也是需要的!

關(guān)鍵步驟1. start

app/build.gradle

 dependencies {
    ......

    //optional, help to generate the final application
    compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1')
    annotationProcessor 'com.tencent.tinker:tinker-android-anno:1.9.1'
    //tinker's main Android lib
    implementation('com.tencent.tinker:tinker-android-lib:1.9.1')
}

工程下的/build.gradle

buildscript {
    repositories {
        google()
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

關(guān)鍵步驟1.End

1.3 接著看第二步彩匕,這步一看主要是關(guān)于Application的改造。有兩種方式:

1.3.1 可以新定義一個(gè)SampleApplicationLike繼承DefaultApplicationLike 媒区,然后我們的Application繼承從Applicaiton改成TinkerApplication驼仪,并且按照官方說(shuō)明添加默認(rèn)無(wú)參構(gòu)造函數(shù)并添加相關(guān)代碼(代碼的第二個(gè)參數(shù)就是SampleApplicationLike全路徑

1.3.2 另種方式采用注解的方式(之前的配置就是為了這個(gè))生成MyApplication - 這是我采用的方式

If your app has a class that subclasses android.app.Application, then you need to modify that class, and move all its implements to SampleApplicationLike rather than Application:

-public class YourApplication extends Application {
+public class SampleApplicationLike extends DefaultApplicationLike {

Now you should change your Application class, make it a subclass of TinkerApplication. As you can see from its API, it is an abstract class that does not have a default constructor, so you must define a no-arg constructor:

public class SampleApplication extends TinkerApplication {
    public SampleApplication() {
      super(
        //tinkerFlags, which types is supported
        //dex only, library only, all support
        ShareConstants.TINKER_ENABLE_ALL,
        // This is passed as a string so the shell application does not
        // have a binary dependency on your ApplicationLifeCycle class.
        "tinker.sample.android.app.SampleApplicationLike");
    }
}

Use tinker-android-anno to generate your Application is recommended, you just need to add an annotation for your SampleApplicationLike class

@DefaultLifeCycle(
application = "tinker.sample.android.app.SampleApplication",             //application name to generate
flags = ShareConstants.TINKER_ENABLE_ALL)                                //tinkerFlags above
public class SampleApplicationLike extends DefaultApplicationLike

How to install tinker? learn more at the sample SampleApplicationLike.

For proguard, we have already made the proguard config automatic, and tinker will also generate the multiDex keep proguard file for you.

For more tinker configurations, learn more at the sample app/build.gradle.

關(guān)鍵步驟2. start

SampleApplicationLike.java - onBaseContextAttached中,我已經(jīng)將tinker安裝代碼也加上了袜漩⌒靼郑可以先知道,后面我們會(huì)配置的宙攻,不過(guò)由于我們沒(méi)有從服務(wù)器下載奠货,沒(méi)有做很多配置,就單純做一個(gè)安裝即可座掘! - 不要認(rèn)為我們需要tinker平臺(tái)配置創(chuàng)建App才能使用递惋,不是的柔滔,文檔 - Tinker Platform - Android 熱補(bǔ)丁平臺(tái) 只是提供了patch的分發(fā),管理等萍虽。廊遍。我們有自己的服務(wù)器,自己下載加載修復(fù)既可以了贩挣。喉前。。

package com.skl.hotfixtinkertest;

import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.multidex.MultiDex;

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

/**
 * https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/src/main/java/tinker/sample/android/app/SampleApplicationLike.java
 */
@DefaultLifeCycle(
        application = "com.skl.hotfixtinkertest.MyApplication",             //application name to generate
        flags = ShareConstants.TINKER_ENABLE_ALL)                                //tinkerFlags above
public class SampleApplicationLike extends DefaultApplicationLike {

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

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);
        TinkerInstaller.install(this);
    }
}

關(guān)鍵步驟2. end

2. 編譯打包的tinker的build.gradle配置要搞一搞王财,重點(diǎn)是需要我們與當(dāng)前項(xiàng)目整合卵迂。直接參考官方的配置,綜合到我們自己的app/build.gradle就可以了 https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle

位置都對(duì)應(yīng)好绒净,該填的位置都填充上见咒,前后順序都看好;另外簽名記得配置上挂疆,這個(gè)我們上一篇就配置過(guò)了改览,參考下就行。混淆建議先不開(kāi)啟缤言,跑通了宝当,我們?cè)倏椿煜?/p>

關(guān)鍵步驟3. start

完善app/build.gradle

apply plugin: 'com.android.application'
apply plugin: 'com.tencent.tinker.patch'

/*********tink*/
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'")
    }
}

def javaVersion = JavaVersion.VERSION_1_7
/*********tink*/

android {
    compileSdkVersion 28
    buildToolsVersion "29.0.0"
    defaultConfig {
        applicationId "com.skl.hotfixtinkertest"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        /**
         * you can use multiDex and install it in your ApplicationLifeCycle implement
         */
        multiDexEnabled true
        /**
         * buildConfig can change during patch!
         * we can use the newly value when patch
         */
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
//        buildConfigField "String", "MESSAGE", "\"I am the patch 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\""
    }

    /*********tink*/
    //recommend
    dexOptions {
        jumboMode = true
    }

    signingConfigs {
        debug {
            storeFile file('hotfix')
            storePassword "hotfix"
            keyAlias "hotfix"
            keyPassword "hotfix"
        }
        release {
            storeFile file('hotfix')
            storePassword "hotfix"
            keyAlias "hotfix"
            keyPassword "hotfix"
        }
    }
    /*********tink*/

    buildTypes {
        release {
            minifyEnabled false
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }

    /*********tink*/
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

    packagingOptions {
        exclude "/META-INF/**"
    }
    /*********tink*/
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    //optional, help to generate the final application
    compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1')
    annotationProcessor 'com.tencent.tinker:tinker-android-anno:1.9.1'
    //tinker's main Android lib
    implementation('com.tencent.tinker:tinker-android-lib:1.9.1')
}

/*********tink*/

def bakPath = file("${buildDir}/bakApk/")

/**
 * you can use assembleRelease to build you base apk
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * add apk from the build/bakApk
 */
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-debug-old.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0424-15-02-56-R.txt"

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

def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}

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

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

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

    tinkerPatch {
        /**
         * necessary,default 'null'
         * the old apk path, use to diff with the new apk to build
         * add apk from the build/bakApk
         */
        oldApk = getOldApkPath()
        /**
         * optional胆萧,default 'false'
         * there are some cases we may get some warnings
         * if ignoreWarning is true, we would just assert the patch process
         * case 1: minSdkVersion is below 14, but you are using dexMode with raw.
         *         it must be crash when load.
         * case 2: newly added Android Component in AndroidManifest.xml,
         *         it must be crash when load.
         * case 3: loader classes in dex.loader{} are not keep in the main dex,
         *         it must be let tinker not work.
         * case 4: loader classes in dex.loader{} changes,
         *         loader classes is ues to load patch dex. it is useless to change them.
         *         it won't crash, but these changes can't effect. you may ignore it
         * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
         */
        ignoreWarning = false

        /**
         * optional庆揩,default 'true'
         * whether sign the patch file
         * if not, you must do yourself. otherwise it can't check success during the patch loading
         * we will use the sign config with your build type
         */
        useSign = true

        /**
         * optional,default 'true'
         * whether use tinker to build
         */
        tinkerEnable = buildWithTinker()

        /**
         * Warning, applyMapping will affect the normal android build!
         */
        buildConfig {
            /**
             * optional跌穗,default 'null'
             * if we use tinkerPatch to build the patch apk, you'd better to apply the old
             * apk mapping file if minifyEnabled is enable!
             * Warning:
             * you must be careful that it will affect the normal assemble build!
             */
            applyMapping = getApplyMappingPath()
            /**
             * optional订晌,default 'null'
             * It is nice to keep the resource id from R.txt file to reduce java changes
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * necessary,default 'null'
             * because we don't want to check the base apk with md5 in the runtime(it is slow)
             * tinkerId is use to identify the unique base apk when the patch is tried to apply.
             * we can use git rev, svn rev or simply versionCode.
             * we will gen the tinkerId in your manifest automatic
             */
            tinkerId = "1.0"http://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

            /**
             * optional, default 'false'
             * Whether tinker should treat the base apk as the one being protected by app
             * protection tools.
             * If this attribute is true, the generated patch package will contain a
             * dex including all changed classes instead of any dexdiff patch-info files.
             */
            isProtectedApp = false

            /**
             * optional, default 'false'
             * Whether tinker should support component hotplug (add new component dynamically).
             * If this attribute is true, the component added in new apk will be available after
             * patch is successfully loaded. Otherwise an error would be announced when generating patch
             * on compile-time.
             *
             * <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
             */
            supportHotplugComponent = false
        }

        dex {
            /**
             * optional蚌吸,default 'jar'
             * only can be 'raw' or 'jar'. for raw, we would keep its original format
             * for jar, we would repack dexes with zip format.
             * if you want to support below 14, you must use jar
             * or you want to save rom or check quicker, you can use raw mode also
             */
            dexMode = "jar"

            /**
             * necessary锈拨,default '[]'
             * what dexes in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * necessary,default '[]'
             * Warning, it is very very important, loader classes can't change with patch.
             * thus, they will be removed from patch dexes.
             * you must put the following class into main dex.
             * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
             * own tinkerLoader, and the classes you use in them
             *
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /**
             * optional羹唠,default '[]'
             * what library in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * for library in assets, we would just recover them in the patch directory
             * you can get them in TinkerLoadResult with Tinker
             */
            pattern = ["lib/*/*.so"]
        }

        res {
            /**
             * optional奕枢,default '[]'
             * what resource in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * you must include all your resources in apk here,
             * otherwise, they won't repack in the new apk resources.
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * optional,default '[]'
             * the resource file exclude patterns, ignore add, delete or modify resource change
             * it support * or ? pattern.
             * Warning, we can only use for files no relative with resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * default 100kb
             * for modify resource, if it is larger than 'largeModSize'
             * we would like to use bsdiff algorithm to reduce patch file size
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             * optional肉迫,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
             * package meta file gen. path is assets/package_meta.txt in patch file
             * you can use securityCheck.getPackageProperties() in your ownPackageCheck method
             * or TinkerLoadResult.getPackageConfigByName
             * we will get the TINKER_ID from the old apk manifest for you automatic,
             * other config files (such as patchMessage below)is not necessary
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             * just a sample case, you can use such as sdkVersion, brand, channel...
             * you can parse it in the SamplePatchListener.
             * Then you can use patch conditional!
             */
            configField("platform", "all")
            /**
             * patch version via packageConfig
             */
            configField("patchVersion", "1.0")
        }
        //or you can add config filed outside, or get meta value from old apk
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * if you don't use zipArtifact or path, we just use 7za to try
         */
        sevenZip {
            /**
             * optional验辞,default '7za'
             * the 7zip artifact path, it will use the right 7za with your platform
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * optional,default '7za'
             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
             */
//        path = "/usr/local/bin/7za"
        }
    }

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

    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

        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

                        if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
                            def packageAndroidArtifact = variant.packageApplicationProvider.get()
                            if (packageAndroidArtifact != null) {
                                try {
                                    from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
                                } catch (Exception e) {
                                    from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
                                }
                            } else {
                                from variant.outputs.first().mainOutputFile.outputFile
                            }
                        } else {
                            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"
                        from "${buildDir}/intermediates/symbol_list/${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"
                    }

                }
            }
        }
    }
}

task sortPublicTxt() {
    doLast {
        File originalFile = project.file("public.txt")
        File sortedFile = project.file("public_sort.txt")
        List<String> sortedLines = new ArrayList<>()
        originalFile.eachLine {
            sortedLines.add(it)
        }
        Collections.sort(sortedLines)
        sortedFile.delete()
        sortedLines.each {
            sortedFile.append("${it}\n")
        }
    }
}
/*********tink*/

注意:(有些人會(huì)把這塊配置搞到一個(gè)單獨(dú)的gradle管理喊衫,你調(diào)通后可以試試跌造,先調(diào)通什么都好說(shuō))

image

關(guān)鍵步驟3. end

3. 此時(shí)就可以rebuild一下工程,然后就可以發(fā)現(xiàn)有個(gè)MyApplication.java在build目錄下

image

關(guān)鍵步驟4. start - 記得rebuild工程生成MyApplication

4. 配置下manifest的application'name, 這樣運(yùn)行App才會(huì)安裝tinker - 順便把讀寫(xiě)權(quán)限搞上,一會(huì)我們要從sdcard讀取補(bǔ)丁apk包壳贪。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.skl.hotfixtinkertest">

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

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        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>

</manifest>

到這里配置就完事了....就可以去添加補(bǔ)丁修復(fù)測(cè)試的方法了

關(guān)鍵步驟4. end

關(guān)鍵步驟5. start

**5. **代碼測(cè)試添加:

MainActivity.java - 之前記得申請(qǐng)下讀取權(quán)限陵珍,以及定義一下補(bǔ)丁apk的路徑;點(diǎn)擊事件我直接布局添加的...自己搞幾個(gè)點(diǎn)擊事件就行违施。互纯。

package com.skl.hotfixtinkertest;

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

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

import java.io.File;

public class MainActivity extends AppCompatActivity {

    private static final String APATCH_PATH = "/sdcard/patch_signed.apk";

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

        PermissionTool.checkPermission(this);
    }

    /**
     * 修復(fù)方法
     * @param view
     */
    public void fixFun(View view) {
        File file = new File(APATCH_PATH);
        if (file.exists()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplication(), APATCH_PATH);
            Log.i("TAG", "補(bǔ)丁包存在>>>>" + APATCH_PATH);
        } else {
            Log.i("TAG", "補(bǔ)丁包不存在");
        }
    }

    /**
     * 點(diǎn)擊方法
     * @param view
     */
    public void clickFun(View view) {

    }
}

PermissionTool.java

package com.skl.hotfixtinkertest;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v4.app.ActivityCompat;
import android.widget.Toast;

/**
 * Created by hl on 2018/3/15.
 */

/**
 * 權(quán)限管理工具
 */
public class PermissionTool {
    // Storage Permissions
    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_ALL = {
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            //            Manifest.permission.ACCESS_FINE_LOCATION,
            //            Manifest.permission.CALL_PHONE,
            //            Manifest.permission.READ_LOGS,
            //            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            //            Manifest.permission.SET_DEBUG_APP,
            //            Manifest.permission.SYSTEM_ALERT_WINDOW,
            //            Manifest.permission.GET_ACCOUNTS,
            //            Manifest.permission.WRITE_APN_SETTINGS
            Manifest.permission.CAMERA
    };
    private static String[] PERMISSIONS_CAMERA = {
            Manifest.permission.CAMERA
    };

    /**
     * 動(dòng)態(tài)申請(qǐng)權(quán)限(讀寫(xiě)權(quán)限)
     *
     * @param context
     */
    public static void checkPermission(Context context) {
        if (Build.VERSION.SDK_INT >= 23) {
            ///< 檢查權(quán)限(NEED_PERMISSION)是否被授權(quán) PackageManager.PERMISSION_GRANTED表示同意授權(quán)
            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED ||
                    ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                            != PackageManager.PERMISSION_GRANTED) {
                ///< 用戶已經(jīng)拒絕過(guò)一次,再次彈出權(quán)限申請(qǐng)對(duì)話框需要給用戶一個(gè)解釋
                if (ActivityCompat.shouldShowRequestPermissionRationale(
                        (Activity) context,
                        Manifest.permission
                                .WRITE_EXTERNAL_STORAGE)) {
                    Toast.makeText(context, "請(qǐng)開(kāi)通相關(guān)權(quán)限磕蒲,否則有些功能無(wú)法正常使用留潦!", Toast.LENGTH_SHORT).show();
                }
                ///< 申請(qǐng)權(quán)限
                // We don't have permission so prompt the user
                ActivityCompat.requestPermissions(
                        (Activity) context,
                        PERMISSIONS_ALL,
                        REQUEST_EXTERNAL_STORAGE
                );

            } else {
                //Toast.makeText(context, "授權(quán)成功!", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

**6. **然后我們先打包一個(gè)release的apk出來(lái) - 此時(shí)的clickFun方法時(shí)沒(méi)有吐司的辣往。

image

6.1 然后我們安裝apk到模擬器兔院,采用adb install命令(之前安裝過(guò)可以卸載了,然后執(zhí)行如下命令或者用重寫(xiě)安裝的命令也可以)

D:\921\HotFixTinkerTest\app\build\outputs\apk\release>adb install app-release.apk
Success

6.2 同時(shí)我們拷貝該release.apk包(基包)站削,到我們的如下目錄并注意包的名稱:

image

**我的實(shí)際目錄 - **這樣應(yīng)該就能理解看懂了吧...

image

有了解過(guò)的可能大概知道坊萝,Tinker的做法就是針對(duì)新舊apk比較搞了一套算法,然后利用差異化生成補(bǔ)丁文件的许起。

關(guān)鍵步驟5. end

關(guān)鍵步驟6. start

7. 然后修改一下代碼十偶,增加一個(gè)吐司

/**
     * 點(diǎn)擊方法
     * @param view
     */
    public void clickFun(View view) {
        Toast.makeText(this, "這是補(bǔ)丁包喲,嘻嘻...", Toast.LENGTH_SHORT).show();
    }

8. 采用如下方式打出補(bǔ)丁包 - 注意此時(shí)除了補(bǔ)丁包园细,也會(huì)重新生成release包的(所以之前的舊包要保留好用來(lái)做差異化比較)惦积。

image

8.1 生成之后會(huì)有如下補(bǔ)丁文件(還支持7zip,不過(guò)我們先不管這個(gè)珊肃,我們需要的是patch_signed.apk荣刑, 這也是代碼加載修復(fù)的文件文件名):

image

8.2 然后adb push到sdcard

D:\921\HotFixTinkerTest\app\build\outputs\apk\tinkerPatch\release>adb push patch_signed.apk /sdcard
patch_signed.apk: 1 file pushed. 1.5 MB/s (3387 bytes in 0.002s)
image

此時(shí)點(diǎn)擊右側(cè)按鈕(是沒(méi)有吐司的),然后點(diǎn)擊修復(fù)方法的按鈕:

image

則開(kāi)始加載修復(fù)補(bǔ)丁包 - 打補(bǔ)丁成功后的日志:

2019-10-29 15:26:12.198 12177-12177/com.skl.hotfixtinkertest W/Tinker.UpgradePatchRetry: onPatchListenerCheck retry file is not exist, just return
2019-10-29 15:26:12.213 12177-12177/com.skl.hotfixtinkertest I/TAG: 補(bǔ)丁包存在>>>>/sdcard/patch_signed.apk
2019-10-29 15:26:15.619 12177-12241/com.skl.hotfixtinkertest I/Tinker.DefaultTinkerResultService: DefaultTinkerResultService received a result:
    PatchResult: 
    isSuccess:true
    rawPatchFilePath:/sdcard/patch_signed.apk
    costTime:3344
    patchVersion:02724c6e30a5c7664189247c30925759

2019-10-29 15:26:15.620 12177-12241/com.skl.hotfixtinkertest I/Process: Sending signal. PID: 12219 SIG: 9
2019-10-29 15:26:15.620 12177-12241/? W/Tinker.DefaultTinkerResultService: deleteRawPatchFile rawFile path: /sdcard/patch_signed.apk
2019-10-29 15:26:15.620 12177-12241/? I/Tinker.PatchFileUtil: safeDeleteFile, try to delete path: /sdcard/patch_signed.apk
2019-10-29 15:26:15.620 12177-12241/? I/Process: Sending signal. PID: 12177 SIG: 9

之后應(yīng)用會(huì)關(guān)閉伦乔,然后重啟打開(kāi),點(diǎn)擊右側(cè)按鈕則會(huì)吐司:

image

關(guān)鍵步驟6. end

目前針對(duì)當(dāng)前環(huán)境董习,如果按照如上步驟烈和,應(yīng)該問(wèn)題不大。實(shí)際如果遇到如下問(wèn)題:

**q1: **adb多設(shè)備皿淋,拔掉一個(gè)就完事了

adb: error: failed to get feature set: more than one device/emulator - 拔掉一個(gè)設(shè)備不就行了招刹,還各種折騰干嘛!

q2: 取消勾選Instant Run - tinker不支持

Tinker does not support instant run mode, please trigger build by assembleDebug or disable instant run in 'File->Settings...'.

image

終于跑通了窝趣,我擦疯暑。。哑舒。說(shuō)麻煩其實(shí)也好妇拯。只是有時(shí)候我們對(duì)環(huán)境,配置不熟,不理解越锈,導(dǎo)致很多問(wèn)題仗嗦。。另外可能有些參數(shù)甘凭,變量也很難理解稀拐。。如果去看官方demo丹弱,也是可以的德撬,不過(guò)需要一定時(shí)間。 網(wǎng)友的可以參考躲胳,不過(guò)很多文章都是沒(méi)有那么清晰蜓洪,而且大部分都是官方翻譯來(lái)的,所以有些關(guān)鍵點(diǎn)很難理解泛鸟。蝠咆。 所以有句話說(shuō)的好“只有懂原理,熟悉源碼北滥,才能游刃有余的使用”..
q3: 另外如果你的手機(jī)是Android7.0請(qǐng)要考慮FileProvider(Android7.0不支持直接訪問(wèn)sd卡)

q4: 如果是真機(jī)(我之前是模擬器上測(cè)試的)刚操,有可能你需要修改APATCH_PATH = "/sdcard/patch_signed.apk"為如下 - 當(dāng)然最好的方式是用代碼獲取sdcard路徑**:

image

官方文檔其實(shí)還是可以的,就是一開(kāi)始就懵了....

工程地址https://gitee.com/heyclock/doc/tree/master/HotFixTinkerTest

一些鏈接可以供參考再芋,應(yīng)該是綜合參考菊霜,要自己琢磨哈:

https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/src/main/java/tinker/sample/android/app/SampleApplicationLike.java - 這個(gè)設(shè)計(jì)到Tinker的安裝初始化,我們精簡(jiǎn)了济赎,沒(méi)有去搞TinkerManager類(主要是關(guān)于TinkerInstaller的封裝)鉴逞。。司训。

Tinker學(xué)習(xí)之旅(一)--- Demo接入Tinker

https://blog.csdn.net/sw950729/article/details/72876660#t6 - 坑

https://blog.csdn.net/qq1221jyj/article/details/73743612 - 坑

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末构捡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子壳猜,更是在濱河造成了極大的恐慌勾徽,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件统扳,死亡現(xiàn)場(chǎng)離奇詭異喘帚,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)咒钟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)吹由,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人朱嘴,你說(shuō)我怎么就攤上這事倾鲫。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵级乍,是天一觀的道長(zhǎng)舌劳。 經(jīng)常有香客問(wèn)我,道長(zhǎng)玫荣,這世上最難降的妖魔是什么甚淡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮捅厂,結(jié)果婚禮上贯卦,老公的妹妹穿的比我還像新娘。我一直安慰自己焙贷,他們只是感情好撵割,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著辙芍,像睡著了一般啡彬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上故硅,一...
    開(kāi)封第一講書(shū)人閱讀 51,590評(píng)論 1 305
  • 那天庶灿,我揣著相機(jī)與錄音,去河邊找鬼吃衅。 笑死往踢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的徘层。 我是一名探鬼主播峻呕,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼趣效!你這毒婦竟也來(lái)了瘦癌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤跷敬,失蹤者是張志新(化名)和其女友劉穎佩憾,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體干花,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年楞黄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了池凄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鬼廓,死狀恐怖肿仑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤尤慰,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布馏锡,位于F島的核電站,受9級(jí)特大地震影響伟端,放射性物質(zhì)發(fā)生泄漏杯道。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一责蝠、第九天 我趴在偏房一處隱蔽的房頂上張望党巾。 院中可真熱鬧,春花似錦霜医、人聲如沸齿拂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)署海。三九已至,卻和暖如春医男,著一層夾襖步出監(jiān)牢的瞬間砸狞,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工昨登, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留趾代,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓丰辣,卻偏偏與公主長(zhǎng)得像撒强,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子笙什,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355