配色: 字号:
热修复框架Tinker的从0到集成之路
2017-01-16 | 阅:  转:  |  分享 
  
热修复框架Tinker的从0到集成之路



2015年10月,QQ空间提出了热修复方案,一时间热修复风靡Android界,阿里的AndFix,360的插件形式,开源的NUWA等,都分别提出了自己的热修复实现方案,2016年微信推出了自己的热修复框架Tinker,从原理上来说,Tinker的实现方式和QQ空间的一脉相承,随着时间的推移,Tinker逐渐的成熟并推出了一键集成的SDK,感觉应该比较是靠谱了,所以就动手集成一下吧。集成比较方便,甚至推出了方便集成的SDK版本,不过需要money,所以我们还是从github上入手吧。



推荐大家看一下Tinker的Wiki,一些细节配置上比较清晰,本篇会忽略这些细节的配置



大致原理



为什么说大致原理呢,因为我也不知道他的原理,不过是在使用中猜的,如果错了,别怪我~~~。



利用MulitDex打出多个dex文件。

主dex中没有逻辑,只是Tinker的所有逻辑,不能更新。

其余dex文件保存我们编写的逻辑,主要用于更新方便。

保存上次打包的记录,便于打补丁包。

根据上一次的记录,一一对比,找出区别之后整合,加签,打包,生成补丁文件。

将补丁文件放到对应位置,验签,加载到内存。(QQ控件热修复原理加载到内存)

具体原理可以看我之前的热修复文章或者百度一下,本篇只说集成。

Gradle配置



工程的根目录的gradle.properties文件中添加Tinker的版本号





org.gradle.jvmargs=-Xmx2048m-XX:MaxPermSize=512m-XX:+HeapDumpOnOutOfMemoryError-Dfile.encoding=UTF-8



TINKER_VERSION=1.7.6



工程根目录下的build.gradle中添加Tinker的编译插件



dependencies{

//Tinker编译的插件

classpath''com.tencent.tinker:tinker-patch-gradle-plugin:1.7.6''





classpath''com.android.tools.build:gradle:2.1.0''

}



app目录下的build.gradle中添加程序运行时的jar包



applyplugin:''com.android.application''



android{

compileSdkVersion23

buildToolsVersion"23.0.2"



defaultConfig{

applicationId"com.alex.tinkerdemo"

minSdkVersion15

targetSdkVersion21

versionCode1

versionName"1.0"





//tinker基本配置

multiDexEnabledtrue

buildConfigField"String","MESSAGE","\"Iamthebaseapk\""

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

buildConfigField"String","PLATFORM","\"all\""



}



//Tinker推荐设置

dexOptions{

jumboMode=true

}



//签名信息的配置

signingConfigs{

release{

try{

storeFilefile("./keystore/key.jks")

storePassword"123456"

keyAlias"1111"

keyPassword"1234567"

}catch(ex){

thrownewInvalidUserDataException(ex.toString())

}

}

}



//编译类型的配置

buildTypes{

release{

minifyEnabledtrue

signingConfigsigningConfigs.release

proguardFilesgetDefaultProguardFile(''proguard-android.txt''),''proguard-rules.pro''

}

debug{

debuggabletrue

minifyEnabledfalse

signingConfigsigningConfigs.debug

}

}

}



dependencies{

compilefileTree(dir:''libs'',include:[''.jar''])

testCompile''junit:junit:4.12''

compile''com.android.support:appcompat-v7:24.2.1''



//多dex打包的类库

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

//Tinker基本类库

compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"){changing=true}

//编译时生成Application

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



}



在build.gradle中,关键是依赖的添加,分别三个依赖,第一个是MultiDex分包使用的依赖,第二个是基本功能的类库,第三个是重点。



Tinker不推荐我们自己实现Application,而是由Tinker自己生成,那么我们通常开发时都会自己定义一个Application类,进行一些App的初始化操作,怎么解决呢?Tinker推荐了另一种实现的方式,后面再说,而第三个依赖便是方便实现自定义Application所推荐的类库。



如上,就是基本的环境配置,当然还有Tinker的配置,这里我们直接采用Tinker提供的基本配置,具体配置的细节可以看wiki。



在app的build.gradle最后,添加如下配置



//=======================Tinker配置=======================================



defgitSha(){

try{

//StringgitRev=''gitrev-parse--shortHEAD''.execute(null,project.rootDir).text.trim()

StringgitRev="1008611"

if(gitRev==null){

thrownewGradleException("can''tgetgitrev,youshouldaddgittosystempathorjustinputtestvalue,suchas''testTinkerId''")

}

returngitRev

}catch(Exceptione){

thrownewGradleException("can''tgetgitrev,youshouldaddgittosystempathorjustinputtestvalue,suchas''testTinkerId''")

}

}



//保存打包oldapk的地址,便于生成补丁包时的对比

defbakPath=file("${buildDir}/bakApk/")



ext{

//是否打开tinker的功能。

tinkerEnabled=true



//oldapk地址

tinkerOldApkPath="${bakPath}/app-release-16-13.apk"

//oldapk混淆文件地址

tinkerApplyMappingPath="${bakPath}/app-release-16-13-mapping.txt"

//oldapkR文件地址

tinkerApplyResourcePath="${bakPath}/app-release-16-13-R.txt"



//多渠道打包相关

tinkerBuildFlavorDirectory="${bakPath}/app-1018-17-32-47"

}





defgetOldApkPath(){

returnhasProperty("OLD_APK")?OLD_APK:ext.tinkerOldApkPath

}



defgetApplyMappingPath(){

returnhasProperty("APPLY_MAPPING")?APPLY_MAPPING:ext.tinkerApplyMappingPath

}



defgetApplyResourceMappingPath(){

returnhasProperty("APPLY_RESOURCE")?APPLY_RESOURCE:ext.tinkerApplyResourcePath

}



defgetTinkerIdValue(){

returnhasProperty("TINKER_ID")?TINKER_ID:gitSha()

}



defbuildWithTinker(){

returnhasProperty("TINKER_ENABLE")?TINKER_ENABLE:ext.tinkerEnabled

}



defgetTinkerBuildFlavorDirectory(){

returnext.tinkerBuildFlavorDirectory

}



if(buildWithTinker()){

applyplugin:''com.tencent.tinker.patch''



tinkerPatch{

/

necessary,default''null''

theoldapkpath,usetodiffwiththenewapktobuild

addapkfromthebuild/bakApk

/

oldApk=getOldApkPath()

/

optional,default''false''

therearesomecaseswemaygetsomewarnings

ifignoreWarningistrue,wewouldjustassertthepatchprocess

case1:minSdkVersionisbelow14,butyouareusingdexModewithraw.

itmustbecrashwhenload.

case2:newlyaddedAndroidComponentinAndroidManifest.xml,

itmustbecrashwhenload.

case3:loaderclassesindex.loader{}arenotkeepinthemaindex,

itmustbelettinkernotwork.

case4:loaderclassesindex.loader{}changes,

loaderclassesisuestoloadpatchdex.itisuselesstochangethem.

itwon''tcrash,butthesechangescan''teffect.youmayignoreit

case5:resources.arschaschanged,butwedon''tuseapplyResourceMappingtobuild

/

ignoreWarning=false



/

保证签名的唯一性

/

useSign=true



/

optional,default''true''

whetherusetinkertobuild

/

tinkerEnable=buildWithTinker()



/

编译相关配置

/

buildConfig{



/

新的apk使用旧的Map文件,减少补丁包大小

/

applyMapping=getApplyMappingPath()

/

同上所述,相同的R文件,减少补丁包大小

/

applyResourceMapping=getApplyResourceMappingPath()



/

补丁的id标识,补丁包的tinkerId和apk的tinkerId相同才能加载补丁

/

tinkerId=getTinkerIdValue()



/

打开keepDexApply模式,补丁包将根据基准包的类分布来编译。

/

keepDexApply=false

}



dex{

/

''raw''模式,将会保持输入dex的格式。

''jar''模式,我们将会把输入dex重新压缩封装到jar

/

dexMode="jar"



/

需要处理dex路径

/

pattern=["classes.dex",

"assets/secondary-dex-?.jar"]

/

放在main.dex中的类,这些类不会被加载

/

loader=[

//usesample,letBaseBuildInfounchangeablewithtinker

"tinker.sample.android.app.BaseBuildInfo"

]

}



lib{

/

需要处理的lib路径

/

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

}



res{

/

需要处理的资源路径

/

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



/

忽视改变的文件,即这些文件的改变不会被打到补丁包中

/

ignoreChange=["assets/sample_meta.txt"]



/

对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb

/

largeModSize=100

}



packageConfig{

/

配置到清单文件的一些字段,没啥用

/

configField("patchMessage","tinkerissampletouse")

/

配置到清单文件的一些字段,没啥用

/

configField("platform","all")

/

配置到清单文件的一些字段,没啥用

/

configField("patchVersion","1.0")

}

//oryoucanaddconfigfiledoutside,orgetmetavaluefromoldapk

//project.tinkerPatch.packageConfig.configField("test1",project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))

//project.tinkerPatch.packageConfig.configField("test2","sample")



/

ifyoudon''tusezipArtifactorpath,wejustuse7zatotry

/

sevenZip{

/

zip路径配置项,执行前提是useSign为true,推荐配置

/

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



}

}



Listflavors=newArrayList<>();

project.android.productFlavors.each{flavor->

flavors.add(flavor.name)

}

booleanhasFlavors=flavors.size()>0

/

bakapkandmapping

/

android.applicationVariants.all{variant->

/

tasktype,youwanttobak

/

deftaskName=variant.name

//defdate=newDate().format("MMdd-HH-mm-ss")

defdate=newDate().format("mm-ss")



tasks.all{

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



it.doLast{

copy{

deffileNamePrefix="${project.name}-${variant.baseName}"

defnewFileNamePrefix=hasFlavors?"${fileNamePrefix}":"${fileNamePrefix}-${date}"



defdestPath=hasFlavors?file("${bakPath}/${project.name}-${date}/${variant.flavorName}"):bakPath

fromvariant.outputs.outputFile

intodestPath

rename{StringfileName->

fileName.replace("${fileNamePrefix}.apk","${newFileNamePrefix}.apk")

}



from"${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"

intodestPath

rename{StringfileName->

fileName.replace("mapping.txt","${newFileNamePrefix}-mapping.txt")

}



from"${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"

intodestPath

rename{StringfileName->

fileName.replace("R.txt","${newFileNamePrefix}-R.txt")

}

}

}

}

}

}

project.afterEvaluate{

//sampleuseforbuildallflavorforonetime

if(hasFlavors){

task(tinkerPatchAllFlavorRelease){

group=''tinker''

deforiginOldPath=getTinkerBuildFlavorDirectory()

for(Stringflavor:flavors){

deftinkerTask=tasks.getByName("tinkerPatch${flavor.capitalize()}Release")

dependsOntinkerTask

defpreAssembleTask=tasks.getByName("process${flavor.capitalize()}Releasewww.tt951.comManifest")

preAssembleTask.doFirst{

StringflavorName=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''

deforiginOldPath=getTinkerBuildFlavorDirectory()

for(Stringflavor:flavors){

deftinkerTask=tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")

dependsOntinkerTask

defpreAssembleTask=tasks.getByName("process${flavor.capitalize()}DebugManifest")

preAssembleTask.doFirst{

StringflavorName=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"

}



}

}

}

}

}





在一些基本的配置上都有注释,方便大家理解。



Tinker的编译配置就到此为止,下面便是代码的配置。

代码编写



因为需要的代码不少,这里不在一一讲解,只标明主要的逻辑代码。具体代码可以去github上下载。



首先看一下整个类的结构



app:这个我也不知道是干什么的,我感觉没什么用,自己可是删掉试试。



crash:崩溃保护。



Log:修复过程中的日志打印。



reporter:补丁过程中的一些流程的回调。



service:修复成功以及耗时等回调



util:工具类,关键是TinkerManager。



Application编写



Tinker不推荐编写自定义Application,我们在自定义Application的逻辑,不在继承Application,而继承由Tinker提供的DefaultApplicationLike类,提供好编写的模板如下:



@SuppressWarnings("unused")

@DefaultLifeCycle(application="com.alex.tinkerdemo.TinkerApp",//自定义生成

flags=ShareConstants.TINKER_ENABLE_ALL,

loadVerifyFlag=false)

publicclassAppextendsDefaultApplicationLike{





publicstaticAppsApp;





publicApp(Applicationapplication,inttinkerFlags,booleantinkerLoadVerifyFlag,

longapplicationStartElapsedTime,longapplicationStartMillisTime,IntenttinkerResultIntent,

Resources[]resources,ClassLoader[]classLoader,AssetManager[]assetwww.baiyuewang.netManager){

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

}









@Override

publicvoidonCreate(){

super.onCreate();

sApp=this;



/JPushInterface.setDebugMode(true);

JPushInterface.init(getApplication());



//友盟统计日志加密

AnalyticsConfig.enableEncrypt(true);/





}



@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)

@Override

publicvoidonBaseContextAttached(Contextbase){

super.onBaseContextAttached(base);



//其原理是分包架构,所以在加载初要加载其余的分包

MultiDex.install(base);



//Tinker管理类,保存当前对象

TinkerManager.setTinkerApplicationLike(this);

//崩溃保护

TinkerManager.initFastCrashProtect();

//是否重试

TinkerManager.setUpgradeRetryEnable(true);



//Log实现,打印加载补丁的信息

TinkerInstaller.setLogIml(newMyLogImp());



//运行Tinker,通过Tinker添加一些基本配置

TinkerManager.installTinker(this);





}



@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)

publicvoidregisterActivityLifecycleCallbacks(Application.ActivityLifecycleCallbackscallback){

//生命周期,默认配置

getApplication().registerActivityLifecycleCallbacks(callback);

}







publicstaticAppgetApp(){

returnsApp;

}





}



在类上标有注解@DefaultLifeCycle,该注解是为了定义在编译是自动生成的Application类的信息,清单文件中的Application注册需要填此处定义的名字。



onCreate()方法和Applciation的onCreate()类似,在这里做一些初始化操作,例如友盟的初始化等等,不在过多的讲解。



其中一个区别便是上下文对象不能够在使用该对象,而是使用app.getApplication()方法获取上下文对象。



onBaseContextAttached()和Applcation的方法功能类似,在这里我们做一些基本操作。



加载分包:MultiDex.install(base);

初始化Tinker对象,设置一些信息。

关于Tinker初始化信息,主要用到TinkerManager对象,注释很详细,看注释即可。关键方法便是TinkerManager.installTinker(this);,他进行初始化的最终操作。看一下这个方法





/

youcanspecifyallclassyouwant.

sometimes,youcanonlyinstalltinkerinsomeprocessyouwant!



@paramappLike

/

publicstaticvoidinstallTinker(ApplicationLikeappLike){

if(isInstalled){

TinkerLog.w(TAG,"installtinker,buthasinstalled,ignore");

return;

}

//Tinker在加载补丁时的一些回调,我们实现对应方法获取回调

LoadReporterloadReporter=newSampleLoadReporter(appLike.getApplication());

//Tinker在修复或者升级补丁时的一些回调

PatchReporterpatchReporter=newSamplePatchReporter(appLike.getApplication());

//用来过滤Tinker收到的补丁包的修复、升级请求

PatchListenerpatchListener=newSamplePatchListener(appLike.getApplication());

//补丁包的核心处理类

AbstractPatchupgradePatchProcessor=newUpgradePatch();



TinkerInstaller.install(appLike,

loadReporter,patchReporter,patchListener,

SampleResultService.class,upgradePatchProcessor);



isInstalled=true;

}



注释很清晰,自己理解吧~~。



加载补丁包



MainActivity中的代码



publicclassMainActivityextendsAppCompatActivity{



@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);



//加载补丁包

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),"/sdcard/mdtinker.apk");

}



/

=====================Tinker默认配置========================

/



protectedvoidonResume(){

super.onResume();

Utils.setBackground(false);



}



@Override

protectedvoidonPause(){

super.onPause();

Utils.setBackground(true);

}

}



关键性便是加载补丁包,这里为了省事直接加载,如果内存理没有补丁,直接会返回了,程序不会崩溃。



开始操作



Debug的不在演示,直接按照Release的方式演示。



正常打出签名包

先按照正常流程打包签名APK,此时将签名文件安装到手机中。







会在上图目录下多出一些文件,这些文件保存好,因为生成补丁的时候需要这个东西。



代码随便改出一些不同



修改生成补丁的配置文件



随便修改一些文件之后,打开app下的build.gradle文件,修改如下文件配置为我们的文件地址



ext{

//是否打开tinker的功能。

tinkerEnabled=true



//oldapk地址

tinkerOldApkPath="${bakPath}/app-release-27-43.apk"

//oldapk混淆文件地址

tinkerApplyMappingPath="${bakPath}/app-release-27-43-mapping.txt"

//oldapkR文件地址

tinkerApplyResourcePath="${bakPath}/app-release-27-43-R.txt"



//多渠道打包相关

tinkerBuildFlavorDirectory="${bakPath}/app-1018-17-32-47"

}



献花(0)
+1
(本文系thedust79首藏)