android热修复技术tinker,Android热修复方案第一弹——Tinker篇

背景

一款App的正常开发流程应该是这样的:新版本上线-->用户安装-->发现Bug-->紧急修复-->重新发布新版本-->提示用户安装更新,从表面上看这样的开发流程顺理成章,但存在 很多弊端:

1.耗时,代价大,有时候可能是一个很小很细微的一个问题,但你还必须下架并 更新应用版本。

2.用户体验差,安装成本高,一个很小的bug就要导致用户重新下载整个应用安装包来进行覆盖安装,也额外增加了用户的流量开支。

那么问题来了,有没有办法来实现动态的修复,不需要重新下载App,在用户无感知的情况下以较低的成本来修复Bug问题?答案是肯定的,热修复技术做得到。

概述

当前关于热修复的实现方案有很多,比较出名的有阿里的AndFix,美团的Robust,QZone的超级补丁以及微信的Tinker,这篇文章将对Tinker接入使用以及实现原理进行简单的分析,关于Tinker这里就不再赘述,对它不了解的可以点击这里 Tinker,值得注意的是Tinker并不是万能的,也有局限性:

1、Tinker不支持修改AndroidManifest.xml;

2、Tinker不支持新增四大组件;

3、在Android N上,补丁对应用启动时间有轻微的影响;

4、不支持部分三星android-21机型,加载补丁时会主动抛异常;

5、在1.7.6以及之后的版本,tinker不再支持加固的动态更新;

6、对于资源替换,不支持修改remoteView。例如transition动画,notification

icon以及桌面图标。

7、任何热修复技术都无法做到100%的成功修复。

接入

Tinker提供了两种接入方式:Gradle和命令行,在这里以Gradle依赖接入为例。

在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖

buildscript {

dependencies {

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

}

}

在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.

dependencies {

//可选,用于生成application类

provided('com.tencent.tinker:tinker-android-anno:1.7.7')

//tinker的核心库

compile('com.tencent.tinker:tinker-android-lib:1.7.7')

}

//apply tinker插件

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

签名配置

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

}

}

文件目录配置

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-0406-10-59-13.apk"

//proguard mapping file to build patch apk

tinkerApplyMappingPath = "${bakPath}/app-debug-0406-10-59-13-mapping.txt"

//resource R.txt to build patch apk, must input if there is resource changed

tinkerApplyResourcePath = "${bakPath}/app-debug-0406-10-59-13-R.txt"

//only use for build all flavor, if not, just ignore this field

tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0406-10-59-13"

}

具体的参数设置事例可参考tinker sample中的app/build.gradle。

新建一个Application在onCreate()方法中对Tinker进行初始化,不过Tinker自己提供了一套通过反射机制来实现Application,通过代码你会发现它并不是Application的子类,后面会详细介绍。

@SuppressWarnings("unused")

@DefaultLifeCycle(application = ".SampleApplication",

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) {

super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);

}

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)

@Override

public void onBaseContextAttached(Context base) {

super.onBaseContextAttached(base);

}

@Override

public void onCreate() {

super.onCreate();

TinkerInstaller.install(this);

}

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)

public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {

getApplication().registerActivityLifecycleCallbacks(callback);

}

}

“application ”这个标签的name就是Application,必须与AndroidManifest.xml保持一致

android:allowBackup="true"

android:name=".SampleApplication"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme">

...

...

在Activity中模拟热修复加载补丁来解决空指针异常,点击settext按钮为TextView设置“TINKER PATCH”,由于TextView没有进行初始化,因此会出现空指针异常。

public class MainActivity extends AppCompatActivity {

private TextView tv_msg;

private Button btn_loadpatch;

private Button btn_settext;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

init();

}

private void init() {

//在此对TextView不进行初始化直接设置Text会出现空指针的异常

//tv_msg=(TextView)findViewById(R.id.tv_msg);

btn_loadpatch=(Button)findViewById(R.id.btn_loadpatch);

btn_settext=(Button)findViewById(R.id.btn_settext);

btn_settext.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

//此处会报空指针异常

tv_msg.setText("TINKER PATCH");

}

});

//加载补丁

btn_loadpatch.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),

Environment.getExternalStorageDirectory().getAbsolutePath() +

"/patch_unsigned.apk");

}

});

}

}

通过Gradle编译后,就会在build/bakApk下生成本地打包的apk(Debug不会生成mapping文件)

40863377091d

bakApk

因为TextView没有进行初始化,接下来修改Activity代码,对TextView进行初始化,解决空指针异常。

public class MainActivity extends AppCompatActivity {

private TextView tv_msg;

private Button btn_loadpatch;

private Button btn_settext;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

init();

}

private void init() {

//在此对TextView进行初始化,修复空指针异常

tv_msg=(TextView)findViewById(R.id.tv_msg);

btn_loadpatch=(Button)findViewById(R.id.btn_loadpatch);

btn_settext=(Button)findViewById(R.id.btn_settext);

btn_settext.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

tv_msg.setText("TINKER PATCH");

}

});

btn_loadpatch.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),

Environment.getExternalStorageDirectory().getAbsolutePath() +

"/patch_unsigned.apk");

}

});

}

}

可以通过gradlew命令来生成差分包,在此之前需要在app/build.gradle中设置相比较的两个app,其中app-debug-0406-10-33-27.apk就是需要类比的apk。

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-0406-10-33-27.apk"

//proguard mapping file to build patch apk

tinkerApplyMappingPath = "${bakPath}/app-debug-0406-10-33-27-mapping.txt"

//resource R.txt to build patch apk, must input if there is resource changed

tinkerApplyResourcePath = "${bakPath}/app-debug-0406-10-33-27-R.txt"

//only use for build all flavor, if not, just ignore this field

tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0406-10-33-27"

}

./gradlew tinkerPatchRelease //Release包

./gradlew tinkerPatchDebug //Debug包

差分包存放在build/outputs/tinkerPatch目录下,patch_unsigned.apk为没有签名的补丁包,patch_signed.apk为已签名的补丁包,patch_signed_7zip.apk为签名后并使用7zip压缩的补丁包,也是Tinker推荐的一种使用方式,这里没有进行签名打包,所以选择使用patch_unsigned.apk差分包,并把该补丁包放在手机的sdcard中。

40863377091d

差分包

然后先点击“btn_loadpatch”按钮,去加载补丁,然后再点击“settext”按钮,可以看到空指针异常已经修复。

运行效果图:

40863377091d

运行效果图

运行原理

Tinker对两个App进行对比,找出差分包,即为patch.dex,然将patch.dex与应用的classes.dex合并整体替换掉旧的dex文件。

一、Application生成

Application的生成采用了java的注解方式,在编译时生成,在com.tencent.tinker.anno下面定义了一个注解方式。

从注解格式中可以看出:

1、描述的是一个类的实现

2、注解会被编译器丢弃,但它会保留源文件

3、该类是被继承的

4、定义体内的参数类型为:String,String,int boolean

@Target({ElementType.TYPE})

@Retention(RetentionPolicy.SOURCE)

@Inherited

public @interface DefaultLifeCycle {

String application();

String loaderClass() default "com.tencent.tinker.loader.TinkerLoader";

int flags();

boolean loadVerifyFlag() default false;

}

在com.tencent.tinker.anno包里面存放有一个TinkerApplication.tmpl的Application的模板:

%TINKER_FLAGS%对应flags

%APPLICATION_LIFE_CYCLE%,为ApplicationLike的全路径

%TINKER_LOADER_CLASS%,loaderClass属性

%TINKER_LOAD_VERIFY_FLAG%对应loadVerifyFlag

public class %APPLICATION% extends TinkerApplication {

public %APPLICATION%() {

super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);

}

}

自定义注解的实现,需要继承AbstractProcessor类,com.tencent.tinker.anno包下的AnnotationProcessor类继承该类并有具体的实现,在processDefaultLifeCycle方法中会循环遍历被DefaultLifeCycle标识的对象,获取注解中声明的数值,然后读取模板,填充数值,最终生成一个继承于TinkerApplication的Application实例

private void processDefaultLifeCycle(Set extends Element> elements) {

Iterator var2 = elements.iterator();

while(var2.hasNext()) {

Element e = (Element)var2.next();

DefaultLifeCycle ca = (DefaultLifeCycle)e.getAnnotation(DefaultLifeCycle.class);

String lifeCycleClassName = ((TypeElement)e).getQualifiedName().toString();

String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf(46));

lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf(46) + 1);

String applicationClassName = ca.application();

if(applicationClassName.startsWith(".")) {

applicationClassName = lifeCyclePackageName + applicationClassName;

}

String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf(46));

applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf(46) + 1);

String loaderClassName = ca.loaderClass();

if(loaderClassName.startsWith(".")) {

loaderClassName = lifeCyclePackageName + loaderClassName;

}

System.out.println("*");

InputStream is = AnnotationProcessor.class.getResourceAsStream("/TinkerAnnoApplication.tmpl");

Scanner scanner = new Scanner(is);

String template = scanner.useDelimiter("\\A").next();

String fileContent = template.replaceAll("%PACKAGE%", applicationPackageName).replaceAll("%APPLICATION%", applicationClassName).replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName).replaceAll("%TINKER_FLAGS%", "" + ca.flags()).replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName).replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());

try {

JavaFileObject x = this.processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName, new Element[0]);

this.processingEnv.getMessager().printMessage(Kind.NOTE, "Creating " + x.toUri());

Writer writer = x.openWriter();

try {

PrintWriter pw = new PrintWriter(writer);

pw.print(fileContent);

pw.flush();

} finally {

writer.close();

}

} catch (IOException var21) {

this.processingEnv.getMessager().printMessage(Kind.ERROR, var21.toString());

}

}

}

二、执行流程

在TinkerApplication的onBaseContextAttached()方法调用loadTinker()方法

private void loadTinker() {

//disable tinker, not need to install

if (tinkerFlags == TINKER_DISABLE) {

return;

}

tinkerResultIntent = new Intent();

try {

//reflect tinker loader, because loaderClass may be define by user!

Class> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());

Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class, int.class, boolean.class);

Constructor> constructor = tinkerLoadClass.getConstructor();

tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this, tinkerFlags, tinkerLoadVerifyFlag);

} catch (Throwable e) {

//has exception, put exception error code

ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);

tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);

}

}

在loadTinker中通过反射的方式调用TinkerLoader中的tryLoad方法

@Override

public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {

Intent resultIntent = new Intent();

long begin = SystemClock.elapsedRealtime();

tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);

long cost = SystemClock.elapsedRealtime() - begin;

ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);

return resultIntent;

}

在tryLoadPatchFilesInternal()方法中加载本地补丁,进行dex文件对比判断并添加到dexList中

if (isEnabledForDex) {

//tinker/patch.info/patch-641e634c/dex

boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);

if (!dexCheck) {

//file not found, do not load patch

Log.w(TAG, "tryLoadPatchFiles:dex check fail");

return;

}

}

//now we can load patch jar

if (isEnabledForDex) {

boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);

if (!loadTinkerJars) {

Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");

return;

}

}

//now we can load patch resource

if (isEnabledForResource) {

boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);

if (!loadTinkerResources) {

Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");

return;

}

}

然后在核心类SystemClassLoaderAdde中的installDexes进行修复,Android版本的不同,采用的方法也不同,在installDexes对Android的版本进行判断执行相应的操作,然后对Element[]数组进行组合,保存到pathList

private static final class V23 {

private static void install(ClassLoader loader, List additionalClassPathEntries,

File optimizedDirectory)

throws IllegalArgumentException, IllegalAccessException,

NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {

/* The patched class loader is expected to be a descendant of

* dalvik.system.BaseDexClassLoader. We modify its

* dalvik.system.DexPathList pathList field to append additional DEX

* file entries.

*/

Field pathListField = ShareReflectUtil.findField(loader, "pathList");

Object dexPathList = pathListField.get(loader);

ArrayList suppressedExceptions = new ArrayList();

ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,

new ArrayList(additionalClassPathEntries), optimizedDirectory,

suppressedExceptions));

if (suppressedExceptions.size() > 0) {

for (IOException e : suppressedExceptions) {

Log.w(TAG, "Exception in makePathElement", e);

throw e;

}

}

}

Tinker开启TinkerPatchService来执行合并操作,TinkerPatchService继承于IntentService,只用关注onHandleIntent()方法,在该方法调用UpgradePatch.tryPatch(),最终在DexDiffPatchInternal类中extractDexDiffInternals方法进行合并

@Override

protected void onHandleIntent(Intent intent) {

final Context context = getApplicationContext();

Tinker tinker = Tinker.with(context);

tinker.getPatchReporter().onPatchServiceStart(intent);

if (intent == null) {

TinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");

return;

}

String path = getPatchPathExtra(intent);

if (path == null) {

TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");

return;

}

File patchFile = new File(path);

long begin = SystemClock.elapsedRealtime();

boolean result;

long cost;

Throwable e = null;

increasingPriority();

PatchResult patchResult = new PatchResult();

try {

if (upgradePatchProcessor == null) {

throw new TinkerRuntimeException("upgradePatchProcessor is null.");

}

result = upgradePatchProcessor.tryPatch(context, path, patchResult);

} catch (Throwable throwable) {

e = throwable;

result = false;

tinker.getPatchReporter().onPatchException(patchFile, e);

}

cost = SystemClock.elapsedRealtime() - begin;

tinker.getPatchReporter().

onPatchResult(patchFile, result, cost);

patchResult.isSuccess = result;

patchResult.rawPatchFilePath = path;

patchResult.costTime = cost;

patchResult.e = e;

AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

}

关于Tinker的 合并算法可以参考 Tinker Dexdiff算法解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值