1. 程式人生 > >微信Tinker熱更新詳細使用

微信Tinker熱更新詳細使用

先看一下效果圖

這裡寫圖片描述

Tinker已知問題

由於原理與系統限制,Tinker有以下已知問題:

  • Tinker不支援修改AndroidManifest.xml,Tinker不支援新增四大元件;
  • 由於Google Play的開發者條款限制,不建議在GP渠道動態更新程式碼;
  • 在Android N上,補丁對應用啟動時間有輕微的影響;
  • 不支援部分三星android-21機型,載入補丁時會主動丟擲”TinkerRuntimeException:checkDexInstall failed”;
  • 由於各個廠商的加固實現並不一致,在1.7.6以及之後的版本,tinker不再支援加固的動態更新;
  • 對於資源替換,不支援修改remoteView。例如transition動畫,notification icon以及桌面圖示。

1.首先在專案的build中,整合tinker外掛 ,如下所示(目前最新版是1.7.6)

先看結構圖,只有幾個類而已:

這裡寫圖片描述

專案中的build整合

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.6')
        // NOTE: Do not
place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }
  1. 再將app的build中的關聯屬性新增進去,這些屬性都是經過測試過的,都有註釋顯示,如果自己需要其他屬性,可以自己去github上檢視並整合,文章末尾會送上地址,ps:官方的整合特別麻煩,有時候一整天都有可能搞不定,根據自己的需求和情況來新增,末尾會送上demo
apply plugin: 'com.android.application'

def javaVersion = JavaVersion.VERSION_1_7
android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

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


    defaultConfig {
        applicationId "com.tinker.demo.tinkerdemo"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        buildConfigField "String", "MESSAGE", "\"I am the base apk\""

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

      signingConfigs {
        release {
            try {
                storeFile file("./keystore/release.keystore")
                storePassword "testres"
                keyAlias "testres"
                keyPassword "testres"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }

        debug {
            storeFile file("./keystore/debug.keystore")
        }
    }

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

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


}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile "com.android.support:appcompat-v7:23.1.1"
    testCompile 'junit:junit:4.12'

    compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compile "com.android.support:multidex:1.0.1"
}

def gitSha() {
    try {
        // String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        String gitRev = "1008611"
        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 bakPath = file("${buildDir}/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-0113-14-01-29.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-0113-14-01-29-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") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

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

    tinkerPatch {
        /**
         * 預設為null
         * 將舊的apk和新的apk建立關聯
         * 從build / bakApk新增apk
         */
        oldApk = getOldApkPath()
        /**
         * 可選,預設'false'
         *有些情況下我們可能會收到一些警告
         *如果ignoreWarning為true,我們只是斷言補丁過程
         * case 1:minSdkVersion低於14,但是你使用dexMode與raw。
         * case 2:在AndroidManifest.xml中新新增Android元件,
         * case 3:裝載器類在dex.loader {}不保留在主要的dex,
         * 它必須讓tinker不工作。
         * case 4:在dex.loader {}中的loader類改變,
         * 載入器類是載入補丁dex。改變它們是沒有用的。
         * 它不會崩潰,但這些更改不會影響。你可以忽略它
         * case 5:resources.arsc已經改變,但是我們不使用applyResourceMapping來構建
         */
        ignoreWarning = false

        /**
         *可選,預設為“true”
         * 是否簽名補丁檔案
         * 如果沒有,你必須自己做。否則在補丁載入過程中無法檢查成功
         * 我們將使用sign配置與您的構建型別
         */
        useSign = true

        /**
         可選,預設為“true”
         是否使用tinker構建
         */
        tinkerEnable = buildWithTinker()

        /**
         * 警告,applyMapping會影響正常的android build!
         */
        buildConfig {
            /**
             *可選,預設為'null'
             * 如果我們使用tinkerPatch構建補丁apk,你最好應用舊的
             * apk對映檔案如果minifyEnabled是啟用!
             * 警告:你必須小心,它會影響正常的組裝構建!
             */
            applyMapping = getApplyMappingPath()
            /**
             *可選,預設為'null'
             * 很高興保持資源ID從R.txt檔案,以減少java更改
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             *必需,預設'null'
             * 因為我們不想檢查基地apk與md5在執行時(它是慢)
             * tinkerId用於在試圖應用補丁時標識唯一的基本apk。
             * 我們可以使用git rev,svn rev或者簡單的versionCode。
             * 我們將在您的清單中自動生成tinkerId
             */
            tinkerId = getTinkerIdValue()

            /**
             *如果keepDexApply為true,則表示dex指向舊apk的類。
             * 開啟這可以減少dex diff檔案大小。
             */
            keepDexApply = false
        }

        dex {
            /**
             *可選,預設'jar'
             * 只能是'raw'或'jar'。對於原始,我們將保持其原始格式
             * 對於jar,我們將使用zip格式重新包裝dexes。
             * 如果你想支援下面14,你必須使用jar
             * 或者你想儲存rom或檢查更快,你也可以使用原始模式
             */
            dexMode = "jar"

            /**
             *必需,預設'[]'
             * apk中的dexes應該處理tinkerPatch
             * 它支援*或?模式。
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             *必需,預設'[]'
             * 警告,這是非常非常重要的,載入類不能隨補丁改變。
             * 因此,它們將從補丁程式中刪除。
             * 你必須把下面的類放到主要的dex。
             * 簡單地說,你應該新增自己的應用程式{@code tinker.sample.android.SampleApplication}
             * 自己的tinkerLoader,和你使用的類
             *
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /**
             可選,預設'[]'
             apk中的圖書館應該處理tinkerPatch
             它支援*或?模式。
             對於資源庫,我們只是在補丁目錄中恢復它們
             你可以得到他們在TinkerLoadResult與Tinker
             */
            pattern = ["lib/armeabi/*.so"]
        }

        res {
            /**
             *可選,預設'[]'
             * apk中的什麼資源應該處理tinkerPatch
             * 它支援*或?模式。
             * 你必須包括你在這裡的所有資源,
             * 否則,他們不會重新包裝在新的apk資源。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             *可選,預設'[]'
             *資原始檔排除模式,忽略新增,刪除或修改資源更改
             * *它支援*或?模式。
             * *警告,我們只能使用檔案沒有relative與resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             *預設100kb
             * *對於修改資源,如果它大於'largeModSize'
             * *我們想使用bsdiff演算法來減少補丁檔案的大小
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             *可選,預設'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
             * 包元檔案gen。路徑是修補程式檔案中的assets / package_meta.txt
             * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
             * 或TinkerLoadResult.getPackageConfigByName
             * 我們將從舊的apk清單為您自動獲取TINKER_ID,
             * 其他配置檔案(如下面的patchMessage)不是必需的
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             *只是一個例子,你可以使用如sdkVersion,品牌,渠道...
             * 你可以在SamplePatchListener中解析它。
             * 然後你可以使用補丁條件!
             */
            configField("platform", "all")
            /**
             * 補丁版本通過packageConfig
             */
            configField("patchVersion", "1.0")
        }
        //或者您可以新增外部的配置檔案,或從舊apk獲取元值
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * 如果你不使用zipArtifact或者path,我們只是使用7za來試試
         */
        sevenZip {
            /**
             * 可選,預設'7za'
             * 7zip工件路徑,它將使用正確的7za與您的平臺
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * 可選,預設'7za'
             * 你可以自己指定7za路徑,它將覆蓋zipArtifact值
             */
//        path = "/usr/local/bin/7za"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each {flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("MMdd-HH-mm-ss")

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

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.outputFile
                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    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"
                    }

                }
            }
        }
    }
}

3.在清單檔案中整合application和服務 ,name的application必須是.AMSKY,如果你新增不進去,或者是紅色的話,請先build一下,如果你已經有了自己的application,後面我會說怎麼來整合,Service中做的操作是在你載入成功熱更新外掛後,會提示你更新成功,並且這裡做了鎖屏操作就會載入熱更新外掛,繼續往下看。

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

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:name=".AMSKY"
        android:theme="@style/AppTheme">

        <service
            android:name=".service.SampleResultService"
            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>

4.到這裡就已經基本整合的差不多了,剩下的就是程式碼裡面的整合,首先是application,這裡主要說如果是自已已經存在的application的時候改怎麼操作 ,這個applicaiton可以說就是自己的一個application,只不過寫法,要這樣去寫,可以在onCreate中做自己的一些操作,只不過清單檔案中,要寫AMSKY

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.tinker.demo.tinkerdemo.AMSKY",
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

public SampleApplicationLike(Application application, int tinkerFlags, boolean        tinkerLoadVerifyFlag,long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent,Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {

    super(application,tinkerFlags,tinkerLoadVerifyFlag,applicationStartElapsedTime,applicationStartMillisTime, tinkerResultIntent, resources, classLoader, assetManager);

    }

    /**
     * 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);
        //MultiDex必須在Tinker初始化之前
        MultiDex.install(base);
        //這裡就是初始化Tinker
        TinkerInstaller.install(this,new DefaultLoadReporter(getApplication()),new DefaultPatchReporter(getApplication()),
        new DefaultPatchListener(getApplication()),SampleResultService.class,new UpgradePatch());
        Tinker tinker = Tinker.with(getApplication());
        //這個只是一個Toast提示
        Toast.makeText(
        getApplication(),"沒鳥用,就是Toast提示而已", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //這裡可以做自己的操作
    }

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

}

5.這裡就是在MainActivity中來載入熱更新檔案,在點選載入的時候,就直接鎖屏載入(不要刪除service),當然退出app,下次進來也是可以載入的嗎,這裡載入補丁外掛的話,路徑可以自己設定,我是放在根目錄的debug資料夾當中的,並且我的補丁外掛名字叫patch,可以自行更改。

public class MainActivity extends AppCompatActivity {

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

    /**
     * 載入熱補丁外掛
     * @param v
     */
    public void loadPatch(View v) {
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), "/sdcard/debug/patch.apk");
    }

    /**
     * 殺死應用載入補丁
     * @param v
     */
    public void killApp(View v) {
        ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
        android.os.Process.killProcess(android.os.Process.myPid());
    }

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

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

6.Service檔案

public class SampleResultService extends DefaultTinkerResultService {
    private static final String TAG = "Tinker.SampleResultService";


    @Override
    public void onPatchResult(final PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "SampleResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "SampleResultService receive result: %s", result.toString());

        //first, we want to kill the recover process
        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (result.isSuccess) {
                    Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();
                }
            }
        });
        // is success and newPatch, it is nice to delete the raw file, and restart at once
        // for old patch, you can't delete the patch file
        if (result.isSuccess) {
            File rawFile = new File(result.rawPatchFilePath);
            if (rawFile.exists()) {
                TinkerLog.i(TAG, "save delete raw patch file");
                SharePatchFileUtil.safeDeleteFile(rawFile);
            }
            //not like TinkerResultService, I want to restart just when I am at background!
            //if you have not install tinker this moment, you can use TinkerApplicationHelper api
            if (checkIfNeedKill(result)) {
                if (Utils.isBackground()) {
                    TinkerLog.i(TAG, "it is in background, just restart process");
                    restartProcess();
                } else {
                    //we can wait process at background, such as onAppBackground
                    //or we can restart when the screen off
                    TinkerLog.i(TAG, "tinker wait screen to restart process");
                    new ScreenState(getApplicationContext(), new ScreenState.IOnScreenOff() {
                        @Override
                        public void onScreenOff() {
                            restartProcess();
                        }
                    });
                }
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }
        }
    }

    /**
     * you can restart your process through service or broadcast
     */
    private void restartProcess() {
        TinkerLog.i(TAG, "app is background now, i can kill quietly");
        //you can send service or broadcast intent to restart your process
        android.os.Process.killProcess(android.os.Process.myPid());
    }

    static class ScreenState {
        interface IOnScreenOff {
            void onScreenOff();
        }

        ScreenState(Context context, final IOnScreenOff onScreenOffInterface) {
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_SCREEN_OFF);
            context.registerReceiver(new BroadcastReceiver() {

                @Override
                public void onReceive(Context context, Intent in) {
                    String action = in == null ? "" : in.getAction();
                    TinkerLog.i(TAG, "ScreenReceiver action [%s] ", action);
                    if (Intent.ACTION_SCREEN_OFF.equals(action)) {

                        context.unregisterReceiver(this);

                        if (onScreenOffInterface != null) {
                            onScreenOffInterface.onScreenOff();
                        }
                    }
                }
            }, filter);
        }
    }

}

7.Utils檔案

public class Utils {

    /**
     * the error code define by myself
     * should after {@code ShareConstants.ERROR_PATCH_INSERVICE
     */
    public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL      = -5;
    public static final int ERROR_PATCH_ROM_SPACE               = -6;
    public static final int ERROR_PATCH_MEMORY_LIMIT            = -7;
    public static final int ERROR_PATCH_ALREADY_APPLY           = -8;
    public static final int ERROR_PATCH_CRASH_LIMIT             = -9;
    public static final int ERROR_PATCH_RETRY_COUNT_LIMIT       = -10;
    public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -11;

    public static final String PLATFORM = "platform";

    public static final int MIN_MEMORY_HEAP_SIZE = 45;

    private static boolean background = false;

    public static boolean isGooglePlay() {
        return false;
    }

    public static boolean isBackground() {
        return background;
    }

    public static void setBackground(boolean back) {
        background = back;
    }

    public static int checkForPatchRecover(long roomSize, int maxMemory) {
        if (Utils.isGooglePlay()) {
            return Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL;
        }
        if (maxMemory < MIN_MEMORY_HEAP_SIZE) {
            return Utils.ERROR_PATCH_MEMORY_LIMIT;
        }
        //or you can mention user to clean their rom space!
        if (!checkRomSpaceEnough(roomSize)) {
            return Utils.ERROR_PATCH_ROM_SPACE;
        }

        return ShareConstants.ERROR_PATCH_OK;
    }

    public static boolean isXposedExists(Throwable thr) {
        StackTraceElement[] stackTraces = thr.getStackTrace();
        for (StackTraceElement stackTrace : stackTraces) {
            final String clazzName = stackTrace.getClassName();
            if (clazzName != null && clazzName.contains("de.robv.android.xposed.XposedBridge")) {
                return true;
            }
        }
        return false;
    }

    @Deprecated
    public static boolean checkRomSpaceEnough(long limitSize) {
        long allSize;
        long availableSize = 0;
        try {
            File data = Environment.getDataDirectory();
            StatFs sf = new StatFs(data.getPath());
            availableSize = (long) sf.getAvailableBlocks() * (long) sf.getBlockSize();
            allSize = (long) sf.getBlockCount() * (long) sf.getBlockSize();
        } catch (Exception e) {
            allSize = 0;
        }

        if (allSize != 0 && availableSize > limitSize) {
            return true;
        }
        return false;
    }

    public static String getExceptionCauseString(final Throwable ex) {
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final PrintStream ps = new PrintStream(bos);

        try {
            // print directly
            Throwable t = ex;
            while (t.getCause() != null) {
                t = t.getCause();
            }
            t.printStackTrace(ps);
            return toVisualString(bos.toString());
        } finally {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static String toVisualString(String src) {
        boolean cutFlg = false;

        if (null == src) {
            return null;
        }

        char[] chr = src.toCharArray();
        if (null == chr) {
            return null;
        }

        int i = 0;
        for (; i < chr.length; i++) {
            if (chr[i] > 127) {
                chr[i] = 0;
                cutFlg = true;
                break;
            }
        }

        if (cutFlg) {
            return new String(chr, 0, i);
        } else {
            return src;
        }
    }
}

到這裡就已經整合完畢,下面來說下使用的方法

  1. 這是有bug的版本,我們測試就使用assembleDebug來測試 ,注意沒點選assembleDebug之前,build資料夾裡面是沒有bakApk資料夾的
    這裡寫圖片描述這裡寫圖片描述

2.點選assembleDebug之後會出現bakApk這個資料夾,裡面就有apk檔案,如果失敗,記得clean一下,然後build一下

這裡寫圖片描述

3.接下來在build資料夾裡面,更改ext中的屬性,將bakApk中生成的apk檔案和R檔案複製到ext這裡,如果你打的release包有mapping的話同樣複製到這裡,我們這裡是debug測試,所以沒有mapping檔案

這裡寫圖片描述

4.下面就修改我們需要更新,或者更改的bug,我這裡是新增一張圖片,並且更改標題顯示

這是有bug的版本,我還沒新增圖片,更改標題

這裡寫圖片描述

這裡我添加了一張aa的圖片,並且更改了標題

這裡寫圖片描述

5.接下來我們執行tinker下面的tinkerPatchDebug,來生成補丁包,這個補丁包在outputs下面

這裡寫圖片描述

點選完成後,就會生成tinkerPatch資料夾

這裡寫圖片描述

將tinkerPatch資料夾下面的patch_signed_7zip.apk檔案,粘貼出來,改成你的MainActivity中載入的檔名字,我這裡叫patch,然後點選載入

沒載入之前

這裡寫圖片描述

載入之後,鎖頻,解鎖 ,補丁已經加載出來了,並且資料夾中的補丁已經不在了,因為它和老apk合併了

這裡寫圖片描述

注意

簽名檔案的話 在build的signingConfigs中設定,以及左側的kestore資料夾中設定 ,如下圖
這裡寫圖片描述
這裡寫圖片描述