在早期的支付寶android客戶端中,也有插件化的功能。大概的做法就是,自定義所有的UI控件,再通過XML文件,仿安卓原生XML的佈局文件來搭建佈局,再通過自定義的表達式解析器,利用JAVA的反射特性來給具體的控件添加不同的功能。這樣也達到了插件化。
之前寫過一篇文章,說的是支付寶的插件化。其實這篇文章很老了,現在的支付寶早已不是這種做法。最近幾天忙裏偷閒,反編譯了一下支付寶的插件化。
在下資歷不高,簡單分享一下,大牛看到也不要噴我,在下也是在探索學習中,歡迎交流!
工具:
工欲善其事,必先利其器。因爲平時拆包少,對某些好工具也瞭解不多,基本用了手工的方法來處理的。大家可以用什麼APK改之理之類的工具。
apktool
:這個大家都知道,反編譯利器,我下的是apktool_2.0.0b9版本dex2jar
:不是必須,但看smali代碼太累,用這個工具好受一些jd-gui
:不解釋Replace Studio
:文本搜索工具,可以搜索某文件夾下的文件是否有某文本,我一直用這個,不知道大家有沒有其它好工具推薦。notepad++
:如果你用記事本也可以android環境
:這個必須,你看完它的代碼了,你起碼得自己寫的試試吧
簡單拆包分析
先copy一份apk出來,改後綴名爲zip,直接解壓,先瞧瞧裏面的內容,發現在/lib/armeabi/下的so文件相當的多,有蹊蹺!
出於習慣,立馬就拿notepad++打開了,結果發現,在文本的最前邊是PK
開頭的兩個字符,哈哈,這絕對的是一個zip文件,我們都知道apk其實就是一個zip,而且,真正的so文件應該是以ELF
開頭的。隨便找一個打開,發現了APK的結構:
該上工具了,一步到位。對支付寶主APK進行拆包,使用apktool d xxx.apk
命令,直接拆成smali,再使用dex2jar
命令拆成jar包,再保存成java文件。
打開AndroidManifest.xml
文件,找到application
com.alipay.mobile.quinox.LauncherApplication
用jd-gui打開jar包,找到該類,查看onCreate方法,找到這段代碼
代碼中反射的mPackageInfo
其實就是有名的LoadedApk
類。
使用Replace Studio
在生成的java文件目錄下搜索pathClassloader
我發現在com.alipay.mobile.quinox.classloader下有兩個類繼承自該類。
通過對smali代碼的注入log日誌的跟蹤,JAVA文件和smali文件相互對照(因爲不是所有的class都能反編譯回來),我大概整理了一些邏輯與類的結構。
安卓動態加載原理
支付寶把一個一個插件稱爲bundle,在application
的onCreate方法中,反射mPackageInfo
中的mClassloader
字段,該屬性是一個pathClassloade,將其替換成自己的PathClassloader(這段代碼在dex2jar後的代碼中看不到,我是直接讀的smali代碼)。
在自定義的PathClassloader中處理,如果是自身dex中的類,則用原pathClassloader加載,如果是bundle,則用bundle的dexfile.loadClass
來加載
BundleClassloader
:繼承自classloader,用於加載具體的某個插件,包含一個DexFile文件引用,重寫了loadClass方法,通過調用dexFile.loadClass(“className” , classLoader);HostClasloader
:繼承自PathClassloader,包含一個系統的pathClassloader,也就是加載apk本身的pathClassloaderBootstrapClassloader
:繼承自PathClassloader
,該類中包含一個map集合,保存着一個一個的BundleClassloader
,同時包含一個HostClasloader,該類就是自定義的pathClassloader,通過反射將原來的mClassloader替換成該類。OriginClassLoader
:繼承自classloader,也就是上圖中new的c,ClassLoader.class的parent
成了該對象。重寫了findClass方法,調用了對android原生的類和APK中的類加載做了分發處理,APK中的類調用BootstrapClassloader
的loadClass
方法返回。
關於資源
使用反射創建一個AssetManager對象, 使用getDeclaredMethod
後調用addAssetPath
方法,先用getApplicationInfo().sourceDir
做參數調用該方法,再用bundle的路徑調用該方法,這樣就能整合到一起。
1 2 3 4 5 6 7 8 |
//先反射創建一個AssetManager對象 //反射得到addAssetPath //調用addAssetPath並把當前APK的路徑傳進去,也就是sourceDir //調用addAssetPath並把子包的APK路徑傳進去 //使用下面的代碼創建出Resources //用反射替換掉 mPackageInfo 的mResources字段 Resources rs = getResources(); new Resources(assetManager , rs.getDisplayMetrics(), rs.getConfiguration()) |
使用反射替換掉 mPackageInfo
的mResources
字段
代碼
我只寫了一下動態加載activity的代碼,具體的資源我沒有加載,小夥伴們可以自己試試。
activity我沒有添加,大家可以添加到主工程下,也可以添加的被加載的工程下,不過一定要記得在AndroidManifest.xml
裏註冊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class MyApplication extends Application { private Field field; @Override public void onCreate() { super.onCreate(); Context context = getBaseContext(); Field localField1; try { localField1 = context.getClass().getDeclaredField("mPackageInfo"); localField1.setAccessible(true); Object mPackageInfo = localField1.get(context); field = mPackageInfo.getClass().getDeclaredField("mClassLoader"); field.setAccessible(true); Object mClassLoader = field.get(mPackageInfo); ClassLoader loader = new MyPathClassLoader(this, this.getApplicationInfo().sourceDir, (PathClassLoader) mClassLoader); field.set(mPackageInfo, loader); } catch (Exception e) { } } } |
下面是我的Classloader 比較粗糙
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class MyPathClassLoader extends PathClassLoader { private ClassLoader mClassLoader; private Context context; public MyPathClassLoader(Context context,String dexPath, PathClassLoader mClassLoader) { super(dexPath, mClassLoader); this.mClassLoader = mClassLoader; this.context = context; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = new File("/data/data/com.example.test/lib/libtest.so"); Class clazz = null; try { clazz = mClassLoader.loadClass(name); } catch (Exception e) { } if (clazz != null) { return clazz; } try { DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), context .getDir("dex", 0).getAbsolutePath() + "/libtest.so", 0); return dexFile.loadClass(name, ClassLoader.getSystemClassLoader()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return super.findClass(name); } } |
其它
支付寶之前的做法利用自定義表達式解析器,對JAVA程序員的學習成本相當高,後來支付寶就換成了該方法。
優點:
- 使用同一個Context,還是同一個應用,相當靈活
- 能解決打包時候method ID not in [0, 0xffff]: 65536的問題
- 使用反射少
另外,該方法的缺點:
- 用到的組件必須在manifest.xml中聲明,我們並沒有突破manifest的驗證
- 使用了反射私有API,儘管反射使用的不多。
- 資源文件處理,如果bundle中的id和主工程下的id衝突了就悲劇了。支付寶自己修改了aapt的源碼,把資源
0x7f010001
前面的7f改了。所有應用的生成id都是7f打頭的,該方法不修改aapt辦不到,會給你自動改回來。一般我們也可以通過public.xml下指定id
說明:
同是阿里系的淘寶網有一套框架叫atlas,該框架是一套重量級框架,完全突破了manifest的封鎖,不同的bundle使用的不同的context。
後記
其實支付寶也突破了manifest文件,採用的是代理的模式,註冊一個CommonActivity,在各生命週期的方法中調用targetActivity的方法。再利用反射將CommonActivity中的變量賦值到插件中targetActivity中(用遍歷就能滿足),此方法有個缺陷就是,在插件中的activity中,要慎用this關鍵字,必要用的時候,得用其它方法取CommonActivity對象。
Android中文API(140) —— DexFile
前言
本章內容dalvik.system.DexFile章節,版本爲Android 4.0 r1,翻譯來自:"阿年",歡迎訪問他的博客:"http://blog.csdn.net/mtding",再次感謝"阿年" !期待你一起參與翻譯Android的相關資料,聯繫我[email protected]。
聲明
歡迎轉載,但請保留文章原始出處:)
Android中文翻譯組:http://androidbox.sinaapp.com/
DexFile
譯者署名:阿年
譯者鏈接:http://blog.csdn.net/mtding
版本:Android 4.0 r1
結構
繼承關係
public final class DexFile extends Object
java.lang.Object
dalvik.system.DexFile
類概述
操作DEX文件。這個類原理上和ZipFile相似。主要在類裝載器裏被使用。
注意,我們不直接打開和讀取DEX文件。它們被虛擬機以只讀方式映射到內存了。
構造函數
public DexFile (File file)
通過指定的File對象打開DEX文件。指定的文件通常是一個ZIP/JAR文件,裏面包含一個”classes.dex”。虛擬機將在目錄/data/dalvik-cache下生成對應的文件名字並打開它,如果系統權限允許的話會首先創建或更新它。不要傳目錄/data/dalvik-cache下的文件名給它,因爲這個文件被認爲處於初始狀態(DEX被優化之前)。
參數
File 引用實際DEX文件的File對象
異常
IOException 發生I/O異常,例如文件不存在或者沒有權限訪問。
public DexFile (String fileName)
打開指定文件名的DEX文件。指定的文件通常是一個ZIP/JAR文件,裏面包含一個”classes.dex”。虛擬機將在目錄/data/dalvik-cache下生成對應的文件名字並打開它,如果系統權限允許的話會首先創建或更新它。不要傳目錄/data/dalvik-cache下的文件名給它,因爲這個文件被認爲處於初始狀態(DEX被優化之前)。
參數
fileName DEX文件名。
異常
IOException 發生I/O異常,例如文件不存在或者沒有權限訪問。
公共方法
public void close ()
關閉DEX文件。
有可能無法釋放任何資源。如果來自DEX文件的類還存活着的話,DEX文件不能被取消映射。
異常
IOException 在關閉文件的過程中可能發生I/O異常,一般不會發生。
public Enumeration<String> entries ()
枚舉DEX文件裏面的類名。
返回值
DEX文件所包含類名的枚舉,類名的類型是一般內部格式(像java/lang/String)。
public String getName ()
獲取(已打開)DEX文件名。
返回值
文件名
public static boolean isDexOptNeeded (String fileName)
如果虛擬機認爲apk/jar文件已經過期返回true,並且應該再次通過”dexopt”傳遞。(譯者注:dexopt是apk優化工具)
參數
fileName 被檢查apk/jar文件的絕對路徑名。
返回值
如果應該調用dexopt處理文件返回true;否則false。
異常
FileNotFoundException 文件不可讀、不是一個文件或者文件不存在。
IOException fileName不是有效的apk/jar文件,或者在解析文件時出現問題。
NullPointerException fileName是空的。
StaleDexCacheError 優化過的DEX文件已過期且位於只讀分區。
public Class loadClass (String name, ClassLoader loader)
裝載一個類。返回成功裝載的類,失敗返回空。
如果在類裝載器之外調用它,往往不會得到你想要的結果,這時請使用forName(String)。
該方法不會在找不到類的時候拋出ClassNotFoundException異常,因爲每次在我們看到的第一個DEX文件裏找不到類就粗暴地拋出異常是不合理的。
參數
name 類名,應該是一個"java/lang/String"
loader 試圖裝載類的類裝載器(大多數情況下就是該方法的調用者)
返回值
類名對應的對象,裝載失敗時返回空。
public static DexFile loadDex (String sourcePathName, String outputPathName, int flags)
打開一個DEX文件,並提供一個文件來保存優化過的DEX數據。如果優化過的格式已存在並且是最新的,就直接使用它。如果不是,虛擬機將試圖重新創建一個。該方法主要用於應用希望在通常的應用安裝機制之外下載和執行DEX文件。不能在應用裏直接調用該方法,而應該通過一個類裝載器例如dalvik.system.DexClassLoader.
參數
sourcePathName 包含”classes.dex”的Jar或者APK文件。(將來可能會擴展支持"raw DEX"。)
outputPathName 保存優化過的DEX數據的文件。
flags 打開可選功能(目前什麼也沒定義)
返回值
一個新的,或者先前已經打開的DexFile。
異常
IOException 無法打開輸入或輸出文件。
受保護方法
protected void finalize ()
類結束時調用。確保DEX文件被關閉。
異常
IOException 關閉文件時發生I/O異常,一般不會發生。