1. 程式人生 > >全面認識Android手機(MIUI ROM適配之旅第四天——移植MIUI Framework)

全面認識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釋出的android 2.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的資源包,所有的miui app都會用到它。這個資源包也需要放在/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包:
apktool d framework.jar
執行完畢後,會產生framework.jar.out目錄。

接下來使用附件中的指令碼rmline.sh執行如下命令:
./rmline.sh framework.jar.out
rmline.sh是用以把smali所有以.line開頭的行去掉,這樣我們容易比較smali程式碼上的差別。但是對於所移植的機型,請先複製一份為去掉.line的framework.jar.out版本,因為這些對除錯很重要,我們能通過adb logcat報告的錯誤資訊中去定位在哪一行。

接下來用大家說熟悉的檔案比較工具來比較差異,Linux下推薦meld, Windows下推薦Beyond Compare。

用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修改的程式碼:
.method final applyConfigurationToResourcesLocked(Landroid/content/res/Configuration;)Z

invoke-virtual {v5, p1}, Landroid/content/res/Configuration;->updateFrom(Landroid/content/res/Configuration;)I
move-result v0
.local v0, 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-object v1
.local v1, dm:Landroid/util/DisplayMetrics;

在上面將miui增加的程式碼用紅色標出,在講述之前,先解釋一下smali程式碼的一些規律:
所有的區域性變數用v開頭,方法的頂部.locals 8表示這個方法使用8個區域性變數。所有的引數用p開頭,區域性變數和引數都是從0開始編號。對於非靜態方法來說,p0就是物件本身的引用,即this指標。

這裡miui新增了一個靜態方法呼叫,對於這種順序執行的一段程式碼,我們稱之為線性程式碼。這個例子比較簡單,只新增了一個靜態方法呼叫。線性程式碼的特點是隻有一個入口和一個出口,在編譯器的術語這叫做基本塊。對於這種新增的程式碼,我們找出它的上下文,即修改的程式碼前後的操作。然後在i9100的該方法的smali程式碼中找到相應的位置,把這個修改應用到i9100中去。這種修改也相對簡單,插入程式碼的相應位置比較好定位。

4.4 條件判斷

這種情況指的是miui插入的程式碼並不是一個線性程式碼,而是有條件判斷的。我們以Resources.smali為例,miui修改了其中的loadDrawable方法,修改後的結果如下:

.method loadDrawable(Landroid/util/TypedValue;I)Landroid/graphics/drawable/Drawable;
.end local v8 #e:Ljava/lang/Exception;
.end local v13 #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-object v6
if-nez v6, :cond_1

:try_start_1
move-object/from16 v0, p0

紅色程式碼是miui插入的程式碼,我們再看一下i9100相對於原生android對這個方法的改動,發現改動非常大。這種情況怎麼辦呢,這種情況下的關鍵是找到所插入程式碼的入口點和出口點(即這段程式碼是從哪執行而來的,執行完畢後又往哪去開始執行程式碼)。

首先,我們發現插入程式碼的前面是一個標號:cond_6,這說明程式中應該有一個跳轉語句跳轉到這個標號:cond_6。而且這種程式應該也可以從:cond_6上面的語句順序執行而來(即它可能有兩個入口點),我們分別去找這兩個入口點的程式碼。首先我們去找哪個語句使用了:cond_6,找到如下程式碼:
const-string v15, ".xml"
invoke-virtual {v9, v15}, Ljava/lang/String;->endsWith(Ljava/lang/String;)Z
move-result v15
if-eqz v15, :cond_6

可以發現這段程式碼是在判斷v9這個字串是否以".xml"結尾,如果不是的話,跳轉到:cond_6。好,我們去i9100中找到對應的程式碼邏輯。對於這個例子,我們完全可以以".xml"作為一個關鍵字去i9100的loadDrawable方法中搜索一下,定位到如下程式碼:
const-string v17, ".xml"
move-object v0, v10
move-object/from16 v1, v17
invoke-virtual {v0, v1}, Ljava/lang/String;->endsWith(Ljava/lang/String;)Z
move-result v17
if-eqz v17, :cond_b
這段程式碼的邏輯和我們在miui中找到的程式碼一樣,看來,我們應該把miui插入的程式碼插入到:cond_b之後。找到i9100程式碼中的:cond_b之後,我們看看這條程式碼後面的程式碼,發現和我們插入的程式碼後面的程式碼基本類似,這下可以確定miui新插入的程式碼應該放在:cond_b之後了。

再來看看出口點,miui插入的程式碼有兩個出口點(是一個條件判斷),
if-nez v6, :cond_1
如果v6為空往下執行,如果不為空,則跳轉到:cond_1,好,我們來看看:cond_1的程式碼是在幹嘛?:cond_1的程式碼如下:
:cond_1
:goto_1
if-eqz v6, :cond_2
move-object/from16 v0, p1
iget v0, v0, Landroid/util/TypedValue;->changingConfigurations:I

我們在i9100中發現了一段類似的程式碼:
:cond_1
:goto_1
if-eqz v7, :cond_2
move-object/from16 v0, p1
iget v0, 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-object v7
if-nez v7, :cond_1

4.5 內部類

在這一節的最後我們來介紹一下內部類。對於Java檔案中的每一個內部類,都會產生一個單獨的smali檔案,比如ActivityThread$1.smali,這些檔案的命名規範是如果是匿名類,外部類+$+數字。否則的話是外部類+$+內部類的名字。

當在內部類中呼叫外部類的私有方法時,編譯器會自動合成一個靜態函式。比如下面這個類:
public class Hello {
public class A {
void func() {
setup();
}
}
private void setup() {
}
}
我們在內部類A的func方法中呼叫了外部類的setup方法,最終編譯的smali程式碼為:
Hello$A.smali檔案程式碼片段:
# virtual methods
.method func()V
.locals 1

.prologue
.line 5
iget-object v0, p0, LHello$A;->this$0:LHello;

#calls: LHello;->setup()V
invoke-static {v0}, LHello;->access$000(LHello;)V

.line 6
return-void
.end method

Hello.smali程式碼片段:
.method static synthetic access$000(LHello;)V
.locals 0
.parameter

.prologue
.line 1
invoke-direct {p0}, LHello;->setup()V

return-void
.end method
可以看到,編譯器自動合成了一個access$000方法,假如當我們在一個較複雜的內部類中加入了一個對外部類私有方法的呼叫,雖然只是導致新合成了一個方法,但是這些合成的方法名可能都會有變化,這樣的結果就是smali檔案差異較大,這個時候需要仔細分析,找到呼叫的私有方法。然後給合成的方法選取一個未被使用的名字。

5. 建議
最後想對修改smali程式碼給出一些建議:
(1) 細心,仔細的定位插入程式碼在相應機型程式碼中的插入位置。
(2) 要注意區域性變數序號的改變。
(3) 不要一次修改完所有的檔案再用apktool重新編譯,如果插入程式碼有錯誤,會無法編譯。但是apktool的編譯出錯資訊是天書,你無從知道是哪個檔案改錯了。
(4) 出現錯誤不要緊,檢查adb logcat的錯誤資訊,找出錯誤發生的原因。

修改smali程式碼沒那麼難,多實踐一定會掌握相應的技巧。

hook_smail.apk