Tinker 使用

       Tinker时微信推出的热修复框架,优点就是很稳定,可以gradle打包,缺点是这个修复不是实时的,需要重启。这是由于其实现的原理决定的,简单的说他是通过把生成的不定apk,加载进来,通过与基线apk的整合生成新的dex集合,最后生成oat文件。这只是整合成的文件而已,并没有影响ClassLoader的加载既存在的dex类。而重启之后Tinker会通过反射获取BaseDexClassloder对象,然后在获取里面的PathDexList对象中的Excument[]数据,这里面放的都是dex,这时候我们会把整合好的补丁dex插入到数据的前端,当ClassLoder进行加载使用的时候就会取到。也就是说重启是为了让整合的dex补丁插入进去。

   下面来简单说下步骤:

1.加依赖

buildscript {
    
    repositories {
        google()
        jcenter()
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
        classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") {
            changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
            exclude group: 'com.android.tools.build', module: 'gradle'
        }
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
def is_gradle_3() {
    return hasProperty('GRADLE_3') && GRADLE_3.equalsIgnoreCase('TRUE')
}

2.依赖

apply plugin: 'com.android.application'
def javaVersion = JavaVersion.VERSION_1_7

android {
    compileSdkVersion 28


    defaultConfig {
        applicationId "com.example.liukang.mytinkerproject"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "2.0"
        /**
         * you can use multiDex and install it in your ApplicationLifeCycle implement
         */
//        multiDexEnabled true

    }
    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }
    //recommend
    dexOptions {
        jumboMode = true
    }

    signingConfigs {

        release {
            storeFile file("sign/release.keystore")
            storePassword "123456"
            keyAlias "123456"
            keyPassword "123456"
        }

        debug {
            storeFile file("sign/release.keystore")
            storePassword "123456"
            keyAlias "123456"
            keyPassword "123456"
        }
    }

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

        }
    }

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}
def bakPath =  file("${buildDir}/bakApk/")

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'junit:junit:4.12'
    implementation "com.android.support:appcompat-v7:28.0.0+"
    api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }

// Maven local cannot handle transist dependencies.
    implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }

    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") {
        changing = true
    }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }

    implementation "com.android.support:multidex:1.0.1"

    implementation 'com.orhanobut:hawk:2.0.1'
}

/**
 * 使用Tinker的一些常量配置
 */
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release-0317-18-54-47.apk"
    //proguard mapping file to build patch apk混淆文件
    tinkerApplyMappingPath = "${bakPath}/app-release-0317-18-54-47-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed资源路径
    tinkerApplyResourcePath = "${bakPath}/app-release-0317-18-54-47-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/"




}
/**
 * Tinker的全剧配置
 */
if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        /**
         * 第一次生成的基线包
         */
        oldApk = getOldApkPath()
        /**
         * default 'false'
         * 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
         1. minSdkVersion小于14,但是dexMode的值为"raw";
         2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
         3. 定义在dex.loader用于加载补丁的类不在main dex中;
         4. 定义在dex.loader用于加载补丁的类出现修改;
         5. resources.arsc改变,但没有使用applyResourceMapping编译。
         */
        ignoreWarning = false

        /**
         * 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。
         */
        useSign = true

        /**
         * optional,default 'true'
         * 是否打开Tinker功能
         */
        tinkerEnable = buildWithTinker()

        /**
         * 编译相关的配置项
         */
        buildConfig {
            /**
             * optional,default 'null'
             * 在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,
             * 从而减少补丁包的大小。这个只是推荐设置,
             * 不设置applyMapping也不会影响任何的assemble编译。
             */
//            applyMapping = getApplyMappingPath()
            /**
             * optional,default 'null'
             * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配
             * ,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * necessary,default 'null'
             * 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。
             * 这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号
             * 、versionName等等。
             */
            tinkerId = getTinkerIdValue()

            /**
             *     如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。
             *     若打开keepDexApply模式,补丁包将根据基准包的类分布来编译
             */
            keepDexApply = true

            /**
             * optional, default 'false'
             * 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
             */
            isProtectedApp = false

            /**
             * optional, default 'false'
             * 是否支持新增非export的Activity
             * <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
             */
            supportHotplugComponent = false
        }

        dex {
            /**
             * optional,default 'jar'
             *     只能是'raw'或者'jar'。
             对于'raw'模式,我们将会保持输入dex的格式。
             对于'jar'模式,我们将会把输入dex重新压缩封装到jar。
             如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,
             但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
             */
            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 '[]'
             * 需要处理lib路径,
             * 支持*、?通配符,必须使用'/'分割。与dex.pattern一致,
             * 路径是相对安装包的,例如assets/...
             */
            pattern = ["lib/*/*.so"]
        }

        res {
            /**
             * optional,default '[]'
             * 需要处理res路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,
             * 只有满足pattern的资源才会放到合成后的资源包
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * optional,default '[]'
             * 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,
             * 在编译时会忽略该文件的新增、删除与修改。 最极端的情况,
             * ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
             ignoreChange = ["assets/sample_meta.txt"]

             /**
             * default 100kb
             * 如果大于largeModSize,我们将使用bsdiff算法。
             * 这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
             */
            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
                        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")
                        }
                    }
                }
            }
        }
    }

}

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 : android.defaultConfig.versionName
}

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

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}



3.创建代理的MyTinkerApplication

package com.example.liukang.mytinkerproject;

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

import com.orhanobut.hawk.Hawk;
import com.tencent.tinker.anno.DefaultLifeCycle;
import com.tencent.tinker.entry.ApplicationLike;
import com.tencent.tinker.entry.DefaultApplicationLike;
import com.tencent.tinker.loader.shareutil.ShareConstants;

@SuppressWarnings("unused")
@DefaultLifeCycle(application = ".MyTinkerApplication" ,
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)//都是官方要求这么写的
public class SampleApplicationLike extends ApplicationLike {

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

    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);

        TinkerManager.installedTinker(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Hawk.init(getApplication()).build();
    }
}

4.TinkerManager对Tinker进行管理,这里面可以将重写的如DefaultResultService等一些自定义处理的东西,在这里面通过Tinker 进行初始化

package com.example.liukang.mytinkerproject;

import android.content.Context;

import com.tencent.tinker.entry.ApplicationLike;
import com.tencent.tinker.lib.patch.UpgradePatch;
import com.tencent.tinker.lib.service.DefaultTinkerResultService;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.tinker.TinkerInstaller;

public class TinkerManager {

    private static boolean isInstalled = false;//是否已经初始化标志位
    private static ApplicationLike mApplicationLike;

    /**
     * 完成Tinker初始化
     *
     * @param applicationLike
     */
    public static void installedTinker(ApplicationLike applicationLike) {
        mApplicationLike = applicationLike;
        if (isInstalled) {
            return;
        }
        Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build();
        Tinker.create(tinker);
        tinker.install(applicationLike.getTinkerResultIntent(),MyTinkerService.class, new UpgradePatch());
        isInstalled = true;
    }

    /**
     * 完成patch文件的加载
     *
     * @param path 补丁文件路径
     */
    public static void loadPatch(String path) {
        if (Tinker.isTinkerInstalled()) {//是否已经安装过
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
        }
    }

    /**
     * 利用Tinker代理Application 获取应用全局的上下文
     * @return 全局的上下文
     */
    private static Context getApplicationContext() {
        if (mApplicationLike != null)
            return mApplicationLike.getApplication().getApplicationContext();
        return null;
    }
}

5.清单配置 需要修改application  的name

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.liukang.mytinkerproject">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application
        android:name=".MyTinkerApplication"
        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">
        <service android:name=".MyTinkerService"
            android:exported="false"/>
        <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>

6.生成基线

--》--》对应替换7.修改一下代码之后,在生成补丁包

===》这样便得到了

8.使用实例:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Tinker.MainActivity";
    private static final String FILE_END = ".apk";//文件后缀
    private String FILEDIR;//文件路径
    Activity activity;
    TextView showInfo;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        activity=this;
        createDir();

        Button loadPatchButton = findViewById(R.id.loadPatch);

        loadPatchButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new PermissionHelper(activity).requestPermissions("请同意读取权限"
                        , new PermissionHelper.PermissionListener() {
                            @Override
                            public void doAfterGrand(String... permission) {
                                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), getPatchName());
                            }

                            @Override
                            public void doAfterDenied(String... permission) {
                                Toast.makeText(activity, "没有权限", Toast.LENGTH_LONG).show();
                            }
                        }, Manifest.permission.READ_EXTERNAL_STORAGE
                        , Manifest.permission.WRITE_EXTERNAL_STORAGE);
            }
        });




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

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

        Button killSelfButton = (Button) 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());
            }
        });

        showInfo=findViewById(R.id.showInfo);
        showInfo.setText("哈哈哈哈哈!");

    }



    @Override
    protected void onResume() {
        Log.e(TAG, "i am on onResume");
//        Log.e(TAG, "i am on patch onResume");

        super.onResume();
        Utils.setBackground(false);

    }

    @Override
    protected void onPause() {
        super.onPause();
        Utils.setBackground(true);
    }


    public void createDir() {
        FILEDIR = Environment.getExternalStorageDirectory().getPath() + "/tpatch/";
        //创建路径对应的文件夹
        File file = new File(FILEDIR);
        if (!file.exists())
            file.mkdir();
    }


    public String getPatchName() {
        String s = FILEDIR.concat("tinker").concat(FILE_END);
        Log.e("TAG", s);
        return s;
    }
}

猜你喜欢

转载自blog.csdn.net/lk2021991/article/details/88624273