全面认识Android手机(MIUI ROM适配之旅第四天——移植MIUI Framework)
1. 为什么使用代码插桩
首先我们来回顾第一章中的Android软件架构图,这个图中框架层的代码完全是由Java语言编写的,对于这两层的代码,在没有源代码的情况下我们可以采取代码插桩的方式来注入我们的代码。但是对于下面几层的代码几乎都是以机器码的形式存在,机器码也是可以修改的,但是修改难度和修改smali代码的难度不可同日而语。我们这个系列的文章不介绍如何修改这些机器码,大家有兴趣的可以参考网上的相关资料。MIUI是基于源码开发的,为了提升整个效率,我们会修改下面几层的代码,比如说我们修改了dalvik虚拟机,skia绘图库等。幸好这些修改不多而且有些是为了提升性能的,不影响MIUI的整体功能。MIUI的绝大部分修改都是对框架层和核心应用层,这样保证了我们在原厂ROM的基础上修改这两个层的代码达到移植MIUI的目的。
大家看到这里可能有一个疑问,我们直接替换原厂ROM框架层和核心应用层这两层的代码不就得了。不行,因为各个层次之间是有管理的,框架层和下层代码的一些调用接口是各个厂家自己扩展的,简单的整个替换MIUI框架层和核心应用层的代码无法工作。
2.方法概述
这一章介绍MIUI框架层的移植,其实主要是修改system/framework目录下的三个文件:framework.jar,android.policy.jar和services.jar。这3个文件是Android系统的核心:framework.jar提供了应用层调用的各种API的实现,android.policy.jar提供了锁屏的实现以及手机窗口管理策略的实现。services.jar是一些核心服务Java层的实现,比如ActivityManagerService,PackageManagerService等,这些服务大都运行在system_server进程中。
我们目前2.3的代码是基于google发布的android2.3.7源代码开发的,大家下载附件中的压缩包打开后的目录结构为:
porting-miui/
|-----------------android
|------------framework.jar
|------------services.jar
|------------android.policy.jar
|------------------miui
|----------framework.jar
|----------services.jar
|----------android.policy.jar
|-----------framework-res/
|-----------framework-miui-res.apk
其中android目录中的这三个文件是从google发布的android2.3.7源代码编译而来的,
而miui目录中的这三个文件则是我们在android2.3.7源代码基础上修改后的代码编译而来的。这样我们可以先反编译这些文件,找出反编译后的差别之处,然后将这些差别之处应用到原厂ROM的这三个文件中。听起来是不是和Linux下的patch过程很相似,是的,确实相似,只不过通常的patch是基于源代码的,然后解决一些冲突。而我们是基于smali代码,然后解决一些冲突。(解决冲突现在可能不太明白,没关系,下面会有例子)。
3.移植资源
在上一节中miui目录下的framework-res目录和framework-miui-res.apk这两个是和移植资源相关的。framework-res目录下是我们对系统资源所做的修改即/system/framework/framework-res.apk的修改,大家可以反编译framework-res.apk,将这些修改合到framework-res中,然后再编译回去,这个比较简单,不多做介绍。
framework-miui-res.apk是miui的资源包,所有的miuiapp都会用到它。这个资源包也需要放在/system/framework/目录中,在原厂ROM中,大家一般在/system/framework目录下除了framework-res.apk,也会发现一些其它的xxx-res.apk。为了针对这种情况,miui的资源ID都是以0x03开头的,一般的原厂ROM是2个资源包,framework-res.apk的资源ID是以0x01开头的,另外一个资源包以0x02开头。但是我们发现国行的defy比较变态,这个目录下有3个资源包,因此针对defy我们得特殊处理。所以如果你所移植的机型这个目录下不止两个资源包的话,需要和我们联系。未来我们会考虑MIUI的资源ID都以0x06开头,我们相信应该没有哪个原厂ROM变态到有5个资源包。
4.修改smali
这一章我们重点介绍如何修改原厂ROM的smali将MIUI的修改应用到上面去。我们不会将所有的修改都会在文中列出,挑选几个有代表性的讲解,剩下的大家可以自己去做。我们以i9100为例讲述如何修改。在第二篇准备工作中,我们给出了i9100国行ROM的下载链接,并且讨论了如何在这个ROM的基础上做deodex。为了方便起见,我们把i9100原厂ROM做个deodex后的framework.jar,services.jar和android.policy.jar也放在了附件中。
4.1比较差异
这里的比较差异包含两个部分:比较miui和原生android的差异,比较i9100和原生android的差异。
以framework.jar为例,首先可以建3个目录反汇编这3个jar包:
apktooldframework.jar
执行完毕后,会产生framework.jar.out目录。
接下来使用附件中的脚本rmline.sh运行如下命令:
./rmline.shframework.jar.out
rmline.sh是用以把smali所有以.line开头的行去掉,这样我们容易比较smali代码上的差别。但是对于所移植的机型,请先复制一份为去掉.line的framework.jar.out版本,因为这些对调试很重要,我们能通过adblogcat报告的错误信息中去定位在哪一行。
接下来用大家说熟悉的文件比较工具来比较差异,Linux下推荐meld,Windows下推荐BeyondCompare。
用meld比较miui和原生android的区别,大家可以看到有很多新增的Miui开头的类,和一个新增的miui目录,这些新增的文件和目录我们直接拷到i9100的framework.jar.out中对应的目录中即可(使用有.line的版本)。为什么我们不把这些新增的类组织在一个单独的jar包中呢(请大家思考一下这个问题)。
不比较那些相同的和新加的,我们只比较修改过的文件,在附件中有一个change-list文件,其中列出了我们修改过的文件,你会发现和这个比较结果有一点不符,如果你比较那些文件,会发现只是一些微小的差异(比如说nop这种空指令),这是由于apktool反编译导致的。我们无需关心,我们只需要比较那些我们修改过的文件。
下面我们就开始修改smali文件了,我将这些修改分成3种情况,选择有代表性的3个文件加以介绍,这3种情况难度依次增加。
4.2直接替换
以ActivityThread.smali为例,比较发现miui改了其中一个方法getTopLevelResources,而i9100和原生android的实现完全一样,这种情形是最简单也是最happy的,我们改的地方要适配的机型原厂ROM完全没有修改,直接替换就可以了。
4.3线性代码
还是以ActivityThread.smali为例,对于这个文件,miui一共改了两个方法,一个是上面介绍的,另一个是applyConfigurationToResourcesLocked。通过比较得知,miui修改了这个方法,i9100也修改了这个方法。怎么办呢,我们先分析一下miui修改的代码:
.methodfinalapplyConfigurationToResourcesLocked(Landroid/content/res/Configuration;)Z
invoke-virtual{v5,p1},Landroid/content/res/Configuration;->updateFrom(Landroid/content/res/Configuration;)I
move-resultv0
.localv0,changes:I
invoke-static{v0},Landroid/app/MiuiThemeHelper;->handleExtraConfigurationChanges(I)V
invoke-virtual{p0,v7},Landroid/app/ActivityThread;->getDisplayMetricsLocked(Z)Landroid/util/DisplayMetrics;
move-result-objectv1
.localv1,dm:Landroid/util/DisplayMetrics;
在上面将miui增加的代码用红色标出,在讲述之前,先解释一下smali代码的一些规律:
所有的局部变量用v开头,方法的顶部.locals8表示这个方法使用8个局部变量。所有的参数用p开头,局部变量和参数都是从0开始编号。对于非静态方法来说,p0就是对象本身的引用,即this指针。
这里miui新增了一个静态方法调用,对于这种顺序执行的一段代码,我们称之为线性代码。这个例子比较简单,只新增了一个静态方法调用。线性代码的特点是只有一个入口和一个出口,在编译器的术语这叫做基本块。对于这种新增的代码,我们找出它的上下文,即修改的代码前后的操作。然后在i9100的该方法的smali代码中找到相应的位置,把这个修改应用到i9100中去。这种修改也相对简单,插入代码的相应位置比较好定位。
4.4条件判断
这种情况指的是miui插入的代码并不是一个线性代码,而是有条件判断的。我们以Resources.smali为例,miui修改了其中的loadDrawable方法,修改后的结果如下:
.methodloadDrawable(Landroid/util/TypedValue;I)Landroid/graphics/drawable/Drawable;
.endlocalv8#e:Ljava/lang/Exception;
.endlocalv13#rnf:Landroid/content/res/Resources$NotFoundException;
:cond_6
invoke-virtual/range{p0..p2},
Landroid/content/res/Resources;->loadOverlayDrawable(Landroid/util/TypedValue;I)Landroid/graphics/drawable/Drawable;
move-result-objectv6
if-nezv6,:cond_1
:try_start_1
move-object/from16v0,p0
红色代码是miui插入的代码,我们再看一下i9100相对于原生android对这个方法的改动,发现改动非常大。这种情况怎么办呢,这种情况下的关键是找到所插入代码的入口点和出口点(即这段代码是从哪执行而来的,执行完毕后又往哪去开始执行代码)。
首先,我们发现插入代码的前面是一个标号:cond_6,这说明程序中应该有一个跳转语句跳转到这个标号:cond_6。而且这种程序应该也可以从:cond_6上面的语句顺序执行而来(即它可能有两个入口点),我们分别去找这两个入口点的代码。首先我们去找哪个语句使用了:cond_6,找到如下代码:
const-stringv15,".xml"
invoke-virtual{v9,v15},Ljava/lang/String;->endsWith(Ljava/lang/String;)Z
move-resultv15
if-eqzv15,:cond_6
可以发现这段代码是在判断v9这个字符串是否以".xml"结尾,如果不是的话,跳转到:cond_6。好,我们去i9100中找到对应的代码逻辑。对于这个例子,我们完全可以以".xml"作为一个关键字去i9100的loadDrawable方法中搜索一下,定位到如下代码:
const-stringv17,".xml"
move-objectv0,v10
move-object/from16v1,v17
invoke-virtual{v0,v1},Ljava/lang/String;->endsWith(Ljava/lang/String;)Z
move-resultv17
if-eqzv17,:cond_b
这段代码的逻辑和我们在miui中找到的代码一样,看来,我们应该把miui插入的代码插入到:cond_b之后。找到i9100代码中的:cond_b之后,我们看看这条代码后面的代码,发现和我们插入的代码后面的代码基本类似,这下可以确定miui新插入的代码应该放在:cond_b之后了。
再来看看出口点,miui插入的代码有两个出口点(是一个条件判断),
if-nezv6,:cond_1
如果v6为空往下执行,如果不为空,则跳转到:cond_1,好,我们来看看:cond_1的代码是在干嘛?:cond_1的代码如下:
:cond_1
:goto_1
if-eqzv6,:cond_2
move-object/from16v0,p1
igetv0,v0,Landroid/util/TypedValue;->changingConfigurations:I
我们在i9100中发现了一段类似的代码:
:cond_1
:goto_1
if-eqzv7,:cond_2
move-object/from16v0,p1
igetv0,v0,Landroid/util/TypedValue;->changingConfigurations:I
只不过是v6变成了v7,说明这段代码检测v7的值,因此我们需要将我们插入的代码改为:
invoke-virtual/range{p0..p2},
Landroid/content/res/Resources;->loadOverlayDrawable(Landroid/util/TypedValue;I)Landroid/graphics/drawable/Drawable;
move-result-objectv7
if-nezv7,:cond_1
4.5内部类
在这一节的最后我们来介绍一下内部类。对于Java文件中的每一个内部类,都会产生一个单独的smali文件,比如ActivityThread$1.smali,这些文件的命名规范是如果是匿名类,外部类+$+数字。否则的话是外部类+$+内部类的名字。
当在内部类中调用外部类的私有方法时,编译器会自动合成一个静态函数。比如下面这个类:
publicclassHello{
publicclassA{
voidfunc(){
setup();
}
}
privatevoidsetup(){
}
}
我们在内部类A的func方法中调用了外部类的setup方法,最终编译的smali代码为:
Hello$A.smali文件代码片段:
#virtualmethods
.methodfunc()V
.locals1
.prologue
.line5
iget-objectv0,p0,LHello$A;->this$0:LHello;
#calls:LHello;->setup()V
invoke-static{v0},LHello;->access$000(LHello;)V
.line6
return-void
.endmethod
Hello.smali代码片段:
.methodstaticsyntheticaccess$000(LHello;)V
.locals0
.parameter
.prologue
.line1
invoke-direct{p0},LHello;->setup()V
return-void
.endmethod
可以看到,编译器自动合成了一个access$000方法,假如当我们在一个较复杂的内部类中加入了一个对外部类私有方法的调用,虽然只是导致新合成了一个方法,但是这些合成的方法名可能都会有变化,这样的结果就是smali文件差异较大,这个时候需要仔细分析,找到调用的私有方法。然后给合成的方法选取一个未被使用的名字。
5.建议
最后想对修改smali代码给出一些建议:
(1)细心,仔细的定位插入代码在相应机型代码中的插入位置。
(2)要注意局部变量序号的改变。
(3)不要一次修改完所有的文件再用apktool重新编译,如果插入代码有错误,会无法编译。但是apktool的编译出错信息是天书,你无从知道是哪个文件改错了。
(4)出现错误不要紧,检查adblogcat的错误信息,找出错误发生的原因。
修改smali代码没那么难,多实践一定会掌握相应的技巧。