Android App 线上热修复方案

热修复一词恐怕最早应用在微软。为了巩固其windows系统和office的市场占有率,微软开发并维护了一套线上修复方案,用于修复漏洞及特定问题(LDR),避免延续到发版解决(GDR),详见HotFix维基词条。

天猫android面临同样的问题,尤其对于双十一来讲。提早发出去的包,如果出现客户端的问题,实在是干着急,覆水难收。因此线上修复方案迫在眉睫。那么跟随这篇文章,我们来梳理一下热修复方案的技术细节以及阿里沉淀的相关技术。

一句话概述:基于Xposed中的思想,通过修改c层的Method实例描述,来实现更改与之对应的java方法的行为,从而达到修复的目的。

先来了解一下Xposed:诞生于XDA论坛,类似一个应用平台,不同的是其提供诸多系统级的应用。可实现许多神奇的功能。Xposed需要以越狱为前提,像是android中的cydia。

Xposed可以修改任何程序的任何java方法(需root),github上提供了XposedInstaller,是一个android app。提供很多framework层,应用层级的程序。开发者可以为其开发一些系统或应用方面的插件,自定义android系统,它甚至可以做动态权限管理(XposedMods)。

Xposed是在什么时机启动,需要什么条件,来做了如此敏感的动作?

android系统启动与应用启动

zygote进程是android手机系统启动后,常驻的一个名为‘受精卵’的进程,

  • zygote的启动实现脚本在/init.rc文件中
  • 启动过程中执行的二进制文件在/system/bin/app_process

任何应用程序启动时,会从zygote进程fork出一个新的进程。并装载一些必要的class,invoke一些初始化方法。这其中包括像:

  • ActivityThread
  • ServiceThread
  • ApplicationPackageManager
  • ...

等应用启动中必要的类,触发必要的方法,比如:handleBindApplication,将此进程与对应的应用绑定的初始化方法;同时,会将zygote进程中的dalvik虚拟机实例复制一份,因此每个应用程序进程都有自己的dalvik虚拟机实例;会将已有Java运行时加载到进程中;会注册一些android核心类的jni方法到虚拟机中,支撑从c到java的启动过程。

Xposed做了手脚

Xposed在这个过程改写了app_process(源码在Xposed : a modified app_process binary),替换/system/bin/app_process这个二进制文件。然后做了两个事:

  1. 通过Xposed的hook技术,在上述过程中,对上面提到的那些加载的类的方法hook。
  2. 加载XposedBridge.jar

这时hook必要的方法是为了方便开发者为它开发插件,加载XposedBridge.jar是为动态hook提供了基础。在这个时候加载它意味着,所有的程序在启动时,都可以加载这个jar(因为上面提到的fork过程)。结合hook技术,从而达到了控制所有程序的所有方法。

为获得/system/bin/目录的读写权限,因而需要以root为前提。

那么Xposed是怎么hook java方法的呢?要从XposedBridge看起,重点在XposedBridge.hookmethod(原方法的Member对象,含有新方法的XC_MethodHook对象);,这里会调到

  
  
  
  
  1. private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);

这个native的方法,通过这个方法,可以让所hook的方法,转向native层的一个c方法。如何做到?

When a transmit from java to native occurs, dvm sets up a native stack. In dvmCallJNIMethod(), dvmPlatformInvoke is used to call the native method(signature in Method.insns).

在jni这个中间世界里,类型数据由jni表来沟通java和c的世界;方法由c++指针结合DVM*系(如dvmSlotToMethod,dvmDecodeIndirectRef等方法)的api方法,操作虚拟机,从而实现java方法与c方法的世界。

那么hook的过程是这样:首先通过dexclassload来load所要hook的方法,分析类后,进c层,见代码XposedBridge_hookMethodNative方法,拿到要hook的Method类,然后通过dvmslotTomethod方法获取Method*指针,

  
  
  
  
  1. Method* method = dvmSlotToMethod(declaredClass, slot);

declaredClass就是所hook方法所在的类,对应的jobject。slot是Method类中,描述此java对象在vm中的索引;那么通过这个方法,我们就获取了c层的Method指针,通过

  
  
  
  
  1. SET_METHOD_FLAG(method, ACC_NATIVE);

将该方法标记为一个native方法,然后通过

  
  
  
  
  1. method->nativeFunc = &hookedMethodCallback;

Point "method->nativeFunc" at the JNI bridge, and overload "method->insns" to point at the actual function.

引用自Jni源码注释。定向c层方法到hookedMethodCallback,这样当被hook的java方法执行时,就会调到c层的hookedMethodCallback方法。

After name resolved via dvmResolveNativeMethod, meth->nativeFunc is assigned with new MethodCallBridge dvmCallJNIMethod. So, only resolve once for one native method.

通过meth->nativeFunc重定向MethodCallBridge到hookedMethodCallback这个方法上,控制这个c++指针是无视java的private的。

另外,在method结构体中有

  
  
  
  
  1. method->insns = (const u2*) hookInfo;

用insns指向替换成为的方法,以便hookedMethodCallback可以获取真正期望执行的java方法。

现在所有被hook的方法,都指向了hookedMethodCallbackc方法中,然后在此方法中实现调用替换成为的java方法。

回顾Xposed,以root为必要条件,在app_process加载XposedBidge.jar,从而实现有hook所有应用的所有方法的能力;而后续动态hook应用内的方法,其实只是load了从zypote进程复制出来的运行时的这个XposedBidge.jar,然后hook而已。因此,若在一个应用范围内的hook,root不是必须的,只是单纯的加载hook的实现方法,即可修改本应用的方法。

基于其思路,阿里沉淀出两套热修复解决方案,并已开源

  1. dexposed
  2. AndFix

两技术方案均已开源,实现细节可自行前往,学习了解。这里概述一下两者与Xposed的关系,以及两者的区别。

dexposed紧继承自Xposed,拥有强大的:「原方法前hook」「方法替换」「原方法后hook」三种方式。相互组合,可依据你的hook思想,解决和规避几乎所有的意外情形。它更像是一个hook工具,效果跟使用者思路关系紧密,是地地道道的hook方案。在阿里有着良好的实践成果。

AndFix提炼精华,轻便、简洁(详见源码)的完成了热修复方案。完美的支持了Android 2.3到6.0系统,以及x86框架,机型覆盖率广。提供根据两个apk生成patch的工具,因而使用者只需正向编程,通过工具生成patch文件,下发给客户端即可,编程效率高。是完善的、高可用的热修复方案。

正向编程:意思是说像你发新版那样去解决问题,而不是用hook思想去解决问题

关于AndFix的apkpatch工具

它根据两个apk差别来生成apatch文件,不要将其完全理解成是一个差异文件。它是对比两个apk中的smali文件,找到不同的方法,增加方法annotation(供客户端修复逻辑识别并修复),保留此方法所在类的smali描述,修改类名、将其再打成dex,并与META-INF下的签名、证书、包含有patch信息的PATCH.MF一并打成一个压缩文件,文件格式命为apatch。

对smali类方法修改的内容如下:

  
  
  
  
  1. .class public Lorg/renlonglee.andfixsample.classWillBeingFixed_CF;
  2. .super Landroid/app/Activity;
  3. .source "classWillBeingFixed.java"
  4. .method private methodWillBeingReplaced()V
  5. .locals 0
  6. .annotation runtime Lcom/alipay/euler/andfix/annotation/MethodReplace;
  7. clazz = "org.renlonglee.andfixsample.classWillBeingFixed"
  8. method = "methodWillBeingReplaced"
  9. .end annotation
  10. .prologue
  11. .line 13
  12. return-void
  13. .end method

请留意这些修改:

  1. 类名由classWillBeingFixed被修改成为classWillBeingFixed_CF
  2. 被替换的方法methodWillBeingReplaced,被添加了一个MethodReplace注解。注解描述中说明了原类及原方法。

由此,你应该了解到,对于客户端,这些信息已经足够。那么客户端的处理为:解析apatch->解析dex->加载类->识别含有MethodReplace注解的方法->根据原方法签名已经新方法smali描述进行hook并替换。

实现细节

AndFix在实现上相对简洁,例如在c层核心hook代码中:

  
  
  
  
  1. Method* meth = (Method*) env->FromReflectedMethod(src);
  2. Method* target = (Method*) env->FromReflectedMethod(dest);

来获取Method实例指针,免去获取slot,再从classLoader根据偏移量获取的方式。

以上为开源内容,当你想应用到自己的项目中,还需要做更多:

  • patch包加密、校验
  • app的安全校验
  • 版本控制
  • 加载的安全性
  • ...

本篇介绍的方案是从「修改对应java类的c层对象」来实现的热修复,业界内也不乏通过「修改BaseDexClassLoader中的pathList,来动态加载dex」方式实现热修复。后者纯java实现,但需要hack类的优化流程,将打CLASS_ISPREVERIFIED标签的类,去除此标签,以解决类与类引用不在一个dex中的异常问题。这会放弃dex optimize对启动运行速度的优化。原则上,这对于方法数没有大到需要multidex的应用,损失更明显。而前者不触犯原有的优化流程,只点杀需要hook的方法,更为纯粹、有效。

另外,dexposed和AndFix各自对方案做了诸多不同思路的实现,感兴趣的可对比Xposed继续探索。再贴一下各自c层实现方法替换的传送门:

  • Xposed
  • dexposed
  • AndFix

在业界中,热修复方案,棘手且重要。有时它可以避免发新版,避免无谓时间的浪费,稳住迭代节奏;有时它可以救项目于水深火热之中;有时它也可以带给用户展现上的惊喜。其技术较深且要抵抗更深技术的侵入。阿里将其开源,旨在更多人加入到这个「路漫漫其修远兮」的终端热修复解决方案中,欢迎大家参与开源,提宝贵issue。从目前的android端技术发展来看,原则上这个hook角度是可以永存的。当然还需要更多人的参与与维护,冲破各种未来的限制。另外,热修复只是此技术的一种表现形式,它的更多作用与潜藏的力量,我们共同挖掘。

PS: pc上查看请到这里.

你可能感兴趣的:(android,hotfix,热修复)