探究支付寶android客戶端的動態加載

在早期的支付寶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本身的pathClassloader
  • BootstrapClassloader:繼承自PathClassloader,該類中包含一個map集合,保存着一個一個的BundleClassloader,同時包含一個HostClasloader,該類就是自定義的pathClassloader,通過反射將原來的mClassloader替換成該類。
  • OriginClassLoader:繼承自classloader,也就是上圖中new的c,ClassLoader.class的parent成了該對象。重寫了findClass方法,調用了對android原生的類和APK中的類加載做了分發處理,APK中的類調用BootstrapClassloaderloadClass方法返回。

關於資源

使用反射創建一個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程序員的學習成本相當高,後來支付寶就換成了該方法。

優點:

  1. 使用同一個Context,還是同一個應用,相當靈活
  2. 能解決打包時候method ID not in [0, 0xffff]: 65536的問題
  3. 使用反射少

另外,該方法的缺點:

  1. 用到的組件必須在manifest.xml中聲明,我們並沒有突破manifest的驗證
  2. 使用了反射私有API,儘管反射使用的不多。
  3. 資源文件處理,如果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]

 

聲明

  歡迎轉載,但請保留文章原始出處:) 

    博客園:http://www.cnblogs.com/

    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異常,一般不會發生。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章