Android-Tinker热修复接入实践问题记录(自己认为的较完善的理解)

上一篇我们实践了美团的方法级别的热修复MonkeyLei:Android-美团Robust热修复接入实践问记录 ,之后项目用这个就足够了。至于17,18年一些人提到的其他的已经几年没维护的方法级别热修复框架,就不建议用了。 如果有更复杂的需求,可以试试腾讯的Tencent/tinker, Bugly集成的应该也是这个啦。

Let's go....

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

image

1.1 这一步没问题,不过我的环境是 - 其他的环境没试过哈,不过按照最新的集成方式应没问题,至少到现在2019.10.29或者2020年中估计都没问题,毕竟Android10都出来。。

image
image
image

1.2 所以针对官方的配置,去除了一些警告参数外,另外还有利用插件com.tencent.tinker:tinker-android-anno生成MyApplication的话, annotationProcessor的配置也是需要的!

关键步骤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
    }
}

关键步骤1.End

1.3 接着看第二步,这步一看主要是关于Application的改造。有两种方式:

1.3.1 可以新定义一个SampleApplicationLike继承DefaultApplicationLike ,然后我们的Application继承从Applicaiton改成TinkerApplication,并且按照官方说明添加默认无参构造函数并添加相关代码(代码的第二个参数就是SampleApplicationLike全路径

1.3.2 另种方式采用注解的方式(之前的配置就是为了这个)生成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.

关键步骤2. start

SampleApplicationLike.java - onBaseContextAttached中,我已经将tinker安装代码也加上了。可以先知道,后面我们会配置的,不过由于我们没有从服务器下载,没有做很多配置,就单纯做一个安装即可! - 不要认为我们需要tinker平台配置创建App才能使用,不是的,文档 - Tinker Platform - Android 热补丁平台 只是提供了patch的分发,管理等。。我们有自己的服务器,自己下载加载修复既可以了。。。

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);
    }
}

关键步骤2. end

2. 编译打包的tinker的build.gradle配置要搞一搞,重点是需要我们与当前项目整合。直接参考官方的配置,综合到我们自己的app/build.gradle就可以了 https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle

位置都对应好,该填的位置都填充上,前后顺序都看好;另外签名记得配置上,这个我们上一篇就配置过了,参考下就行。混淆建议先不开启,跑通了,我们再看混淆!

关键步骤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"//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.
             *
             * Notice that currently this feature is incubating and only support NON-EXPORTED Activity
             */
            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 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 sortedLines = new ArrayList<>()
        originalFile.eachLine {
            sortedLines.add(it)
        }
        Collections.sort(sortedLines)
        sortedFile.delete()
        sortedLines.each {
            sortedFile.append("${it}\n")
        }
    }
}
/*********tink*/

注意:(有些人会把这块配置搞到一个单独的gradle管理,你调通后可以试试,先调通什么都好说)

image

关键步骤3. end

3. 此时就可以rebuild一下工程,然后就可以发现有个MyApplication.java在build目录下

image

关键步骤4. start - 记得rebuild工程生成MyApplication

4. 配置下manifest的application'name, 这样运行App才会安装tinker - 顺便把读写权限搞上,一会我们要从sdcard读取补丁apk包。




    
    

    
        
            
                

                
            
        
    


到这里配置就完事了....就可以去添加补丁修复测试的方法了

关键步骤4. end

关键步骤5. start

**5. **代码测试添加:

MainActivity.java - 之前记得申请下读取权限,以及定义一下补丁apk的路径;点击事件我直接布局添加的...自己搞几个点击事件就行。。

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);
    }

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

    /**
     * 点击方法
     * @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.
 */

/**
 * 权限管理工具
 */
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
    };

    /**
     * 动态申请权限(读写权限)
     *
     * @param context
     */
    public static void checkPermission(Context context) {
        if (Build.VERSION.SDK_INT >= 23) {
            ///< 检查权限(NEED_PERMISSION)是否被授权 PackageManager.PERMISSION_GRANTED表示同意授权
            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED ||
                    ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                            != PackageManager.PERMISSION_GRANTED) {
                ///< 用户已经拒绝过一次,再次弹出权限申请对话框需要给用户一个解释
                if (ActivityCompat.shouldShowRequestPermissionRationale(
                        (Activity) context,
                        Manifest.permission
                                .WRITE_EXTERNAL_STORAGE)) {
                    Toast.makeText(context, "请开通相关权限,否则有些功能无法正常使用!", Toast.LENGTH_SHORT).show();
                }
                ///< 申请权限
                // We don't have permission so prompt the user
                ActivityCompat.requestPermissions(
                        (Activity) context,
                        PERMISSIONS_ALL,
                        REQUEST_EXTERNAL_STORAGE
                );

            } else {
                //Toast.makeText(context, "授权成功!", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

**6. **然后我们先打包一个release的apk出来 - 此时的clickFun方法时没有吐司的。

image

6.1 然后我们安装apk到模拟器,采用adb install命令(之前安装过可以卸载了,然后执行如下命令或者用重写安装的命令也可以)

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

6.2 同时我们拷贝该release.apk包(基包),到我们的如下目录并注意包的名称:

image

**我的实际目录 - **这样应该就能理解看懂了吧...

image

有了解过的可能大概知道,Tinker的做法就是针对新旧apk比较搞了一套算法,然后利用差异化生成补丁文件的。

关键步骤5. end

关键步骤6. start

7. 然后修改一下代码,增加一个吐司

/**
     * 点击方法
     * @param view
     */
    public void clickFun(View view) {
        Toast.makeText(this, "这是补丁包哟,嘻嘻...", Toast.LENGTH_SHORT).show();
    }

8. 采用如下方式打出补丁包 - 注意此时除了补丁包,也会重新生成release包的(所以之前的旧包要保留好用来做差异化比较)。

image

8.1 生成之后会有如下补丁文件(还支持7zip,不过我们先不管这个,我们需要的是patch_signed.apk, 这也是代码加载修复的文件文件名):

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

此时点击右侧按钮(是没有吐司的),然后点击修复方法的按钮:

image

则开始加载修复补丁包 - 打补丁成功后的日志:

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: 补丁包存在>>>>/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

之后应用会关闭,然后重启打开,点击右侧按钮则会吐司:

image

关键步骤6. end

目前针对当前环境,如果按照如上步骤,应该问题不大。实际如果遇到如下问题:

**q1: **adb多设备,拔掉一个就完事了

adb: error: failed to get feature set: more than one device/emulator - 拔掉一个设备不就行了,还各种折腾干嘛!

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

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

image

终于跑通了,我擦。。。说麻烦其实也好。只是有时候我们对环境,配置不熟,不理解,导致很多问题。。另外可能有些参数,变量也很难理解。。如果去看官方demo,也是可以的,不过需要一定时间。 网友的可以参考,不过很多文章都是没有那么清晰,而且大部分都是官方翻译来的,所以有些关键点很难理解。。 所以有句话说的好“只有懂原理,熟悉源码,才能游刃有余的使用”..
q3: 另外如果你的手机是Android7.0请要考虑FileProvider(Android7.0不支持直接访问sd卡)

q4: 如果是真机(我之前是模拟器上测试的),有可能你需要修改APATCH_PATH = "/sdcard/patch_signed.apk"为如下 - 当然最好的方式是用代码获取sdcard路径**:

image

官方文档其实还是可以的,就是一开始就懵了....

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

一些链接可以供参考,应该是综合参考,要自己琢磨哈:

https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/src/main/java/tinker/sample/android/app/SampleApplicationLike.java - 这个设计到Tinker的安装初始化,我们精简了,没有去搞TinkerManager类(主要是关于TinkerInstaller的封装)。。。

Tinker学习之旅(一)--- Demo接入Tinker

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

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

你可能感兴趣的:(Android-Tinker热修复接入实践问题记录(自己认为的较完善的理解))