一次集成使用Tinker熱更新的體驗(yàn)

對(duì)于熱修復(fù)也學(xué)習(xí)了2天撩满,基本了解它是怎么操作與實(shí)現(xiàn)的蜒程,自己在做的時(shí)候也遇到了一些坑,看到好的文莊總是忍不住要收藏伺帘,要學(xué)習(xí)昭躺,下面這篇文章內(nèi)容寫的很好,借此分享非大家曼追!
轉(zhuǎn)載地址:
http://blog.csdn.net/qq_34306963/article/details/76926930

官方文檔:https://github.com/Tencent/tinker
下面我們按照官方文檔和這個(gè)里面的tinker-sample-android來進(jìn)行集成Tinker窍仰。
注意說明,
官方demo的使用https://github.com/Tencent/tinker/wiki/Tinker-接入指南里面的使用步驟詳解

熱加載

簡(jiǎn)單的說 如果Android要加載一個(gè)類 就會(huì)調(diào)用ClassLoader的findClass方法 在dex中查找這個(gè)類 找到后加載到內(nèi)存
而我們要做的就是在findClass的時(shí)候讓類加載找到的是我們修復(fù)過后的類礼殊,而不是未修復(fù)的類驹吮。

這里寫圖片描述

至于是如何做到的暫時(shí)還沒有深究,個(gè)人理解大概是通過反射替換了ClassLoader晶伦,通過自定義的ClassLoader去加載更新之后的類文件從而完成替換

直接上手

關(guān)于熱更新介紹和機(jī)制碟狞,網(wǎng)上很多介紹,這里簡(jiǎn)單記錄一下小白使用Tinker熱更新的經(jīng)歷

跑通demo婚陪,稍后再說導(dǎo)入

https://github.com/Tencent/tinker

  1. 將tinker-sample-android 導(dǎo)入
    這里要注意的是


    20170808173726600.png

需要將app 下的gradle 中 gitRev 字段自己賦值族沃,因?yàn)樵跊]有配置git的情況下,無法獲取泌参,這個(gè)值作為Tinker 的Id脆淹,我好像沒感覺這個(gè)玩意有啥用,應(yīng)該是唯一的即可沽一,然后這個(gè)項(xiàng)目就可以根據(jù)編譯打包啦(即帶有bug需要修復(fù)的線上版本)

  1. 修改Bug

比如給隨便一個(gè)按鈕點(diǎn)擊事件中多打印一個(gè)Toast

  1. 打差異包

首先在Gradle中配置帶有bug的apk路徑盖溺,以及資源文件txt路徑,以便讓Tinker新老apk進(jìn)行比較铣缠,生成差異包


這里寫圖片描述

在AS自帶命令行中輸入 gradlew tinkerPatchDebug脑沿,如果其中報(bào)錯(cuò)
Warning:ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times
com.tencent.tinker.build.util.TinkerPatchException: ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times

FAILURE: Build failed with an exception.

  • What went wrong:
    Could not dispatch a message to the daemon.

  • Try:
    Run with –stacktrace option to get the stack trace. Run with –info or –debug option to get more log output.

,可在Gradle中嘗試打開警告忽略

20170808175604998.png

4.將生成的Patch包導(dǎo)入代碼中的加載路徑卿拴,默認(rèn)項(xiàng)目中MainActivity中寫死了,在存儲(chǔ)卡根目錄
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + “/patch_signed_7zip.apk”);

patch默認(rèn)生成在


20170808180945296.png

5.最后一步醉鳖,調(diào)用

 TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");

即可,然后將程序關(guān)閉哮内,重新打開盗棵,就執(zhí)行的是新的代碼啦~

導(dǎo)入到自己的工程

推薦使用Gradle導(dǎo)入

在app下添加

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

  compile('com.tencent.tinker:tinker-android-anno:1.7.11')
    //tinker的核心庫(kù)
    compile('com.tencent.tinker:tinker-android-lib:1.7.11')
    compile "com.android.support:multidex:1.0.1"

project.gradle

 classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.11')

附上我的完整gradle文件,注意牍蜂,一定要配置簽名文件

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

def gitSha() {
    try {
//        String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        String gitRev = "fushuang666"
        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

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-0808-17-04-26.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-0808-17-04-26.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 {
        /**
         * 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 = true

        /**
         * 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 = 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
        }

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

                }
            }
        }
    }
}

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.0"

    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }

    //recommend
    dexOptions {
        jumboMode = true
    }

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

        debug {
            storeFile file("./keystore/key.jks")
            storePassword "123456"
            keyAlias "key"
            keyPassword "123456"
        }
    }

    defaultConfig {
        applicationId "com.example.admin.tinker"
        minSdkVersion 19
        targetSdkVersion 26
        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\""

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

}

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:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    //可選,用于生成application類
    compile('com.tencent.tinker:tinker-android-anno:1.7.11')
    //tinker的核心庫(kù)
    compile('com.tencent.tinker:tinker-android-lib:1.7.11')
    compile "com.android.support:multidex:1.0.1"

}

將demo中的類導(dǎo)入


這里寫圖片描述

修改Manifast文件


這里寫圖片描述

好啦合呐,你的工程可以和Demo工程一樣愉快的使用了暮的。具體加載Patch清除Patch 看Demo就可以了

githubDemo地址:https://github.com/fushuangdage/Tinker

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市淌实,隨后出現(xiàn)的幾起案子冻辩,更是在濱河造成了極大的恐慌,老刑警劉巖拆祈,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恨闪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡放坏,警方通過查閱死者的電腦和手機(jī)凛剥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轻姿,“玉大人犁珠,你說我怎么就攤上這事』チ粒” “怎么了犁享?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)豹休。 經(jīng)常有香客問我炊昆,道長(zhǎng),這世上最難降的妖魔是什么威根? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任凤巨,我火速辦了婚禮,結(jié)果婚禮上洛搀,老公的妹妹穿的比我還像新娘敢茁。我一直安慰自己,他們只是感情好留美,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布彰檬。 她就那樣靜靜地躺著,像睡著了一般谎砾。 火紅的嫁衣襯著肌膚如雪逢倍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天景图,我揣著相機(jī)與錄音较雕,去河邊找鬼。 笑死挚币,一個(gè)胖子當(dāng)著我的面吹牛亮蒋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忘晤,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宛蚓,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了设塔?” 一聲冷哼從身側(cè)響起凄吏,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闰蛔,沒想到半個(gè)月后痕钢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡序六,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年任连,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片例诀。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡随抠,死狀恐怖裁着,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拱她,我是刑警寧澤二驰,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站秉沼,受9級(jí)特大地震影響桶雀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唬复,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一矗积、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧敞咧,春花似錦棘捣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至丰包,卻和暖如春禁熏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背邑彪。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工瞧毙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寄症。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓宙彪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親有巧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子释漆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Tinker使用 前言 寫在前面的話,在上家公司一直在主導(dǎo)組件框架的開發(fā)篮迎,所以對(duì)Android領(lǐng)域組件化男图,熱更新的...
    徐正峰閱讀 1,888評(píng)論 6 6
  • 官方文檔:https://github.com/Tencent/tinker 下面我們按照官方文檔和這個(gè)里面的ti...
    fushuang閱讀 3,104評(píng)論 0 8
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,146評(píng)論 25 707
  • 22天逊笆,能帶來多少變化? 我從未想過自己在22天里收獲如此多岂傲∧疡桑回想初衷,那時(shí)剛知道了零秒思考,就興致沖沖地寫了起來...
    喵皇后閱讀 281評(píng)論 1 2
  • 我們打小就認(rèn)識(shí)乃戈,我比他大一歲褂痰,只是我認(rèn)識(shí)他他不記得我,后來到夏令營(yíng)的一個(gè)組當(dāng)時(shí)加了好友但是沒聊過天偏化,過了一段時(shí)間...
    季以陽閱讀 107評(píng)論 0 0