背景
一款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保持一致
...
...
在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文件)
因为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中。
然后先点击“btn_loadpatch”按钮,去加载补丁,然后再点击“settext”按钮,可以看到空指针异常已经修复。
运行效果图:
运行原理
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算法解析