初探Android熱修復——tinker接入

前言

衆所周知,Android不僅系統版本衆多,機型衆多,而且各個應用市場都各有各的政策和審覈速度。每次發佈一個版本對於開發的同學來說都是一種煎熬。而且很多時候我們也會想到:

  • 修復的 bug 需要等待下個版本發佈窗口才能發佈?
  • 已經 ready 的需求排隊上線,需要等待其他 Feature Team 合入代碼?
  • 老版本升級速度慢?頻繁上線版本提醒用戶升級,影響用戶體驗?
他山之石,可以攻玉

針對這些問題,Android熱更新出現了。


接入步驟(Tinker)

一、新建項目,添加gradle依賴在項目的 build.gradle 中,添加tinker-patch-gradle-plugin的依賴
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')

        // 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
}
二、在app的gradle文件,我們需要添加tinker的庫依賴以及apply tinker的gradle插件
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:25.2.0'
    testCompile 'junit:junit:4.12'

    //可選,用於生成application類
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker的核心庫
    compile('com.tencent.tinker:tinker-android-lib:1.7.7')

    compile "com.android.support:multidex:1.0.1"
}

三、順便加一下簽名的配置,放在項目目錄。建議在沒有任何配置的時候進行簽名。



在構建項目的時候可能會遇到下面這個問題(至少我遇到了):



四、對tinker 進行配置,整個build.gradle文件如下所示。
apply plugin: 'com.android.application'
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    defaultConfig {
        applicationId "com.chenzhi.tinkerdemo"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    signingConfigs {
        release {
            try {
                storeFile file("tinkerdemo.jks")
                storePassword "tinker"
                keyAlias "tinker"
                keyPassword "tinker"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }
    }

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

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:25.2.0'
    testCompile 'junit:junit:4.12'

    //可選,用於生成application類
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker的核心庫
    compile('com.tencent.tinker:tinker-android-lib:1.7.7')

    compile "com.android.support:multidex:1.0.1"
}

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

ext {
    tinkerEnabled = true
    tinkerOldApkPath = "${bakPath}/app-debug-0331-11-34-51.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0331-11-34-51-R.txt"
}

def getOldApkPath() {
    return ext.tinkerOldApkPath
}
def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
    return  ext.tinkerApplyResourcePath
}

if (ext.tinkerEnabled) {
    tinkerPatch {
        oldApk = getOldApkPath()
        ignoreWarning = false
        useSign = true

//        packageConfig {
//
//            configField("TINKER_ID", "2.0")
//        }
        buildConfig{
            tinkerId = "1.0"
            applyMapping = getApplyMappingPath()
            applyResourceMapping = getApplyResourceMappingPath()
        }

        lib {

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

        res {

            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            ignoreChange = ["assetsmple_meta.txt"]

            largeModSize = 100
        }

        sevenZip {

            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }

        dex {

            dexMode = "jar"

            pattern = ["classes*.dex",
                       "assetscondary-dex-?.jar"]

            loader = ["com.tencent.tinker.loader.*",
                      "com.tencent.tinker.*",
                      "com.chenzhi.tinkerdemo.MyTinkerApplication"
            ]
        }


    }
}

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 date = new Date().format("MMdd-HH-mm-ss")
                    from "${buildDir}/outputs/apk/${project.getName()}-${taskName}.apk"
                    into bakPath
                    rename { String fileName ->
                        fileName.replace("${project.getName()}-${taskName}.apk", "${project.getName()}-${taskName}-${date}.apk")
                    }

                    from "${buildDir}/outputs/mapping/${taskName}/mapping.txt"
                    into bakPath
                    rename { String fileName ->
                        fileName.replace("mapping.txt", "${project.getName()}-${taskName}-${date}-mapping.txt")
                    }

                    from "${buildDir}/intermediates/symbols/${taskName}/R.txt"
                    into bakPath
                    rename { String fileName ->
                        fileName.replace("R.txt", "${project.getName()}-${taskName}-${date}-R.txt")
                    }
                }
            }
        }
    }
}

五、添加你項目的 Application ,使之繼承DefaultApplicationLike。(配置application需要仔細)

@SuppressWarnings("unused")
//這裏的application是manifest裏面的 不需要實際寫出類
@DefaultLifeCycle(application = "com.chenzhi.tinkerdemo.MyTinkerApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
//更改爲繼承DefaultApplicationLike
public class MyApplication extends DefaultApplicationLike {

    public MyApplication(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
     */
    //以前application寫在oncreate的東西搬到這裏來初始化
    @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);

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerInstaller.install(this);
    }

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

六、千萬千萬千萬別忘記配置清單文件AndroidManifest,這裏會報紅,Build一下就好了,不好也沒關係,但是不能有紅色下劃線。

這裏MyTinkerApplication指向的是MyApplication裏面聲明的


關於application還有一個地方需要注意:


別忘了加權限,因爲這個案例是本地環境,我直接將補丁放到了手機sdcard裏面,所以需要添加一個讀寫權限

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

七、至此,tinker環境已經搭建完成了,現在來進行測試吧。

在activity裏新建一下代碼:

public class MainActivity extends AppCompatActivity {

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

        TextView mTvBug = (TextView) findViewById(R.id.tv_bug);
        
        mTvBug.setText("現在是報錯狀態了");

        Toast.makeText(this, "現在是報錯狀態了", Toast.LENGTH_SHORT).show();

        //進行補丁的操作,暫時用本地代替
        TinkerInstaller.onReceiveUpgradePatch(this,
                Environment.getExternalStorageDirectory().getAbsolutePath()+"/ysb/patch_signed_7zip.apk");
    }
}

八、先運行一個 應用出錯的程序在手機上作爲上線出bug的版本,運行圖中的assembleDebug ,會在圖一中生成debug文件。根據出錯的apk的日期,去設置build.gredle 中 出錯包的信息。

image1


             image2

九、修改activity中的代碼,當做是你修改好的補丁包:

public class MainActivity extends AppCompatActivity {

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

        TextView mTvBug = (TextView) findViewById(R.id.tv_bug);

        mTvBug.setText("tinker已經修復好了");

        Toast.makeText(this, "tinker已經修復好了", Toast.LENGTH_SHORT).show();

        //進行補丁的操作,暫時用本地代替
        TinkerInstaller.onReceiveUpgradePatch(this,
                Environment.getExternalStorageDirectory().getAbsolutePath()+"/ysb/patch_signed_7zip.apk");
    }
}

十、運行tinkerPatchDebug,開始生成補丁:

最後,我通過USB將修改後的補丁包放到手機sdcard中(因爲我的手機沒有root,不可以通過命令adb push來操作)。
最終效果,手機錄不了GIF,只能是靜態圖了。



大功告成了!


最後附上Demo地址:https://github.com/westlifeChen/TinkerDemo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章