|
1. 為什么使用代碼插樁 首先我們來回顧第一章中的Android軟件架構(gòu)圖,這個圖中框架層的代碼完全是由Java語言編寫的,對于這兩層的代碼,在沒有源代碼的情況下我們可以 采取代碼插樁的方式來注入我們的代碼。但是對于下面幾層的代碼幾乎都是以機器碼的形式存在,機器碼也是可以修改的,但是修改難度和修改smali代碼的難 度不可同日而語。我們這個系列的文章不介紹如何修改這些機器碼,大家有興趣的可以參考網(wǎng)上的相關(guān)資料。MIUI是基于源碼開發(fā)的,為了提升整個效率,我們 會修改下面幾層的代碼,比如說我們修改了dalvik虛擬機,skia繪圖庫等。幸好這些修改不多而且有些是為了提升性能的,不影響MIUI的整體功能。 MIUI的絕大部分修改都是對框架層和核心應用層,這樣保證了我們在原廠ROM的基礎(chǔ)上修改這兩個層的代碼達到移植MIUI的目的。 大家看到這里可能有一個疑問,我們直接替換原廠ROM框架層和核心應用層這兩層的代碼不就得了。不行,因為各個層次之間是有管理的,框架層和下層代碼的一些調(diào)用接口是各個廠家自己擴展的,簡單的整個替換MIUI框架層和核心應用層的代碼無法工作。 2. 方法概述 這一章介紹MIUI框架層的移植,其實主要是修改system/framework目錄下的三個文件:framework.jar, android.policy.jar和services.jar。這3個文件是Android系統(tǒng)的核心:framework.jar提供了應用層調(diào)用 的各種API的實現(xiàn),android.policy.jar提供了鎖屏的實現(xiàn)以及手機窗口管理策略的實現(xiàn)。services.jar是一些核心服務(wù) Java層的實現(xiàn),比如ActivityManagerService, PackageManagerService等,這些服務(wù)大都運行在system_server進程中。 我們目前2.3的代碼是基于google發(fā)布的android 2.3.7源代碼開發(fā)的,大家下載附件中的壓縮包打開后的目錄結(jié)構(gòu)為: 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發(fā)布的android2.3.7源代碼編譯而來的, 而miui目錄中的這三個文件則是我們在android2.3.7源代碼基礎(chǔ)上修改后的代碼編譯而來的。這樣我們可以先反編譯這些文件,找出反編譯后的差 別之處,然后將這些差別之處應用到原廠ROM的這三個文件中。聽起來是不是和Linux下的patch過程很相似,是的,確實相似,只不過通常的 patch是基于源代碼的,然后解決一些沖突。而我們是基于smali代碼,然后解決一些沖突。(解決沖突現(xiàn)在可能不太明白,沒關(guān)系,下面會有例子)。 3. 移植資源 在上一節(jié)中miui目錄下的framework-res目錄和framework-miui-res.apk這兩個是和移植資源相關(guān)的。 framework-res目錄下是我們對系統(tǒng)資源所做的修改即/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,也會發(fā)現(xiàn)一些其它的xxx-res.apk。為了針對這種情況,miui的資源ID都是以0x03開頭的, 一般的原廠ROM是2個資源包,framework-res.apk的資源ID是以0x01開頭的,另外一個資源包以0x02開頭。但是我們發(fā)現(xiàn)國行的 defy比較變態(tài),這個目錄下有3個資源包,因此針對defy我們得特殊處理。所以如果你所移植的機型這個目錄下不止兩個資源包的話,需要和我們聯(lián)系。未 來我們會考慮MIUI的資源ID都以0x06開頭,我們相信應該沒有哪個原廠ROM變態(tài)到有5個資源包。 4. 修改smali 這一章我們重點介紹如何修改原廠ROM的smali將MIUI的修改應用到上面去。我們不會將所有的修改都會在文中列出,挑選幾個有代表性的講解,剩下的 大家可以自己去做。我們以i9100為例講述如何修改。在第二篇準備工作中,我們給出了i9100國行ROM的下載鏈接,并且討論了如何在這個ROM的基 礎(chǔ)上做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 執(zhí)行完畢后,會產(chǎn)生framework.jar.out目錄。 接下來使用附件中的腳本rmline.sh運行如下命令: ./rmline.sh framework.jar.out rmline.sh是用以把smali所有以.line開頭的行去掉,這樣我們?nèi)菀妆容^smali代碼上的差別。但是對于所移植的機型,請先復制一份為去 掉.line的framework.jar.out版本,因為這些對調(diào)試很重要,我們能通過adb logcat報告的錯誤信息中去定位在哪一行。 接下來用大家說熟悉的文件比較工具來比較差異,Linux下推薦meld, Windows下推薦Beyond Compare。 用meld比較miui和原生android的區(qū)別,大家可以看到有很多新增的Miui開頭的類,和一個新增的miui目錄,這些新增的文件和目錄我們直 接拷到i9100的framework.jar.out中對應的目錄中即可(使用有.line的版本)。為什么我們不把這些新增的類組織在一個單獨的 jar包中呢(請大家思考一下這個問題)。 不比較那些相同的和新加的,我們只比較修改過的文件,在附件中有一個change-list文件,其中列出了我們修改過的文件,你會發(fā)現(xiàn)和這個比較結(jié)果有 一點不符,如果你比較那些文件,會發(fā)現(xiàn)只是一些微小的差異(比如說nop這種空指令),這是由于apktool反編譯導致的。我們無需關(guān)心,我們只需要比 較那些我們修改過的文件。 下面我們就開始修改smali文件了,我將這些修改分成3種情況,選擇有代表性的3個文件加以介紹,這3種情況難度依次增加。 4.2 直接替換 以ActivityThread.smali為例,比較發(fā)現(xiàn)miui改了其中一個方法getTopLevelResources,而i9100和原生 android的實現(xiàn)完全一樣,這種情形是最簡單也是最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代碼的一些規(guī)律: 所有的局部變量用v開頭,方法的頂部.locals 8表示這個方法使用8個局部變量。所有的參數(shù)用p開頭,局部變量和參數(shù)都是從0開始編號。對于非靜態(tài)方法來說,p0就是對象本身的引用,即this指針。 這里miui新增了一個靜態(tài)方法調(diào)用,對于這種順序執(zhí)行的一段代碼,我們稱之為線性代碼。這個例子比較簡單,只新增了一個靜態(tài)方法調(diào)用。線性代碼的特點是 只有一個入口和一個出口,在編譯器的術(shù)語這叫做基本塊。對于這種新增的代碼,我們找出它的上下文,即修改的代碼前后的操作。然后在i9100的該方法的 smali代碼中找到相應的位置,把這個修改應用到i9100中去。這種修改也相對簡單,插入代碼的相應位置比較好定位。 4.4 條件判斷 這種情況指的是miui插入的代碼并不是一個線性代碼,而是有條件判斷的。我們以Resources.smali為例,miui修改了其中的loadDrawable方法,修改后的結(jié)果如下: .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對這個方法的改動,發(fā)現(xiàn)改動非常大。這種情況怎么辦呢,這種情況下的關(guān)鍵是找到所插入代碼的入口點和出口點(即這段代碼是從哪執(zhí)行而來的,執(zhí)行完畢后又往哪去開始執(zhí)行代碼)。 首先,我們發(fā)現(xiàn)插入代碼的前面是一個標號:cond_6,這說明程序中應該有一個跳轉(zhuǎn)語句跳轉(zhuǎn)到這個標號:cond_6。而且這種程序應該也可以 從:cond_6上面的語句順序執(zhí)行而來(即它可能有兩個入口點),我們分別去找這兩個入口點的代碼。首先我們?nèi)フ夷膫€語句使用了: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 可以發(fā)現(xiàn)這段代碼是在判斷v9這個字符串是否以".xml"結(jié)尾,如果不是的話,跳轉(zhuǎn)到:cond_6。好,我們?nèi)9100中找到對應的代碼邏輯。對于 這個例子,我們完全可以以".xml"作為一個關(guān)鍵字去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 之后,我們看看這條代碼后面的代碼,發(fā)現(xiàn)和我們插入的代碼后面的代碼基本類似,這下可以確定miui新插入的代碼應該放在:cond_b之后了。 再來看看出口點,miui插入的代碼有兩個出口點(是一個條件判斷), if-nez v6, :cond_1 如果v6為空往下執(zhí)行,如果不為空,則跳轉(zhuǎn)到: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中發(fā)現(xiàn)了一段類似的代碼: :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 內(nèi)部類 在這一節(jié)的最后我們來介紹一下內(nèi)部類。對于Java文件中的每一個內(nèi)部類,都會產(chǎn)生一個單獨的smali文件,比如ActivityThread$1.smali,這些文件的命名規(guī)范是如果是匿名類,外部類+$+數(shù)字。否則的話是外部類+$+內(nèi)部類的名字。 當在內(nèi)部類中調(diào)用外部類的私有方法時,編譯器會自動合成一個靜態(tài)函數(shù)。比如下面這個類: public class Hello { public class A { void func() { setup(); } } private void setup() { } } 我們在內(nèi)部類A的func方法中調(diào)用了外部類的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方法,假如當我們在一個較復雜的內(nèi)部類中加入了一個對外部類私有方法的調(diào)用,雖然只是導致新合成了一 個方法,但是這些合成的方法名可能都會有變化,這樣的結(jié)果就是smali文件差異較大,這個時候需要仔細分析,找到調(diào)用的私有方法。然后給合成的方法選取 一個未被使用的名字。 5. 建議 最后想對修改smali代碼給出一些建議: (1) 細心,仔細的定位插入代碼在相應機型代碼中的插入位置。 (2) 要注意局部變量序號的改變。 (3) 不要一次修改完所有的文件再用apktool重新編譯,如果插入代碼有錯誤,會無法編譯。但是apktool的編譯出錯信息是天書,你無從知道是哪個文件改錯了。 (4) 出現(xiàn)錯誤不要緊,檢查adb logcat的錯誤信息,找出錯誤發(fā)生的原因。 修改smali代碼沒那么難,多實踐一定會掌握相應的技巧。 |
|
|