談動態代理在Android外掛中的一些用法
主APP為外掛提供了一系列介面,我們需要考慮以下幾個問題:
一、許可權控制,檢查呼叫者許可權
如果介面都封裝到service中,則可以在Manifest檔案中對暴露的service增加signature的保護級別
使用Binder的靜態方法getCallingPid或者getCallingUid來驗證IPC呼叫者的身份,在獲得呼叫者uid以後,可進一步使用PackageManager.getPackagesForUid(int uid)來獲得呼叫者的包名,然後使用PackageManager.getPackageInfo(String Packagename, int flag)檢查是否具有相應的許可權(使用PackageManager.GET_PERMISSIONS flag)
在Service的OnBind方法中呼叫Context.checkCallingPermission(String permission)或者checkCallingPermissionOrSelf (String permission) 方法,驗證IPC呼叫者是否擁有指定的許可權,同樣適用於Messenger;
使用Context.enforceCallingPermission(String permission, String message),如果呼叫者不具備許可權,自動丟擲SecurityException
二、介面異常的統一捕獲,防禦各種崩潰,並上報崩潰日誌
三、介面有效性檢查,典型的為防禦NoSuchMethodError
現有的對介面的相容性檢查是採用api level的方式,外掛中定義一個minLevel,主app中提供某個level的api,主app會根據兩個level來選擇具體載入哪個外掛。與此類似的是Android SDK,每個APP都會提供minSdkVersion,如果在低版本手機上呼叫高版本系統api就會報找不到類或者函式,結果是崩潰。可以對這種情況進行防禦,解決的辦法就是對所有呼叫進行有效性檢查,檢查介面的實現類和函式是否存在,如果不存在就返回失敗。
四、介面的呼叫上報,後臺能檢視介面呼叫情況,包括頻次,系統環境,所帶的資料等
五、介面的轉向,比如對於某個介面的呼叫,我們將其替換成執行另一個動作,這些對於介面實現本身來說是透明的
考慮到這些問題都與具體介面無關,所以我們可以統一進行處理,而最有效的辦法就是利用動態代理攔截掉所有的介面呼叫,然後在統一的入口去執行這些檢查。
接下來用程式碼來舉例說明,先給出當前介面的形式,如下:
public abstract class AbsBluetoothManager {
static AbsBluetoothManager instance;
public static AbsBluetoothManager getInstance() {
return instance;
}
abstract void connect(String mac);
}
這個AbsBluetoothManager是開放給外掛的類,是個抽象類,具體的實現在主APP中,如下:
public class BluetoothManager extends AbsBluetoothManager {
public static void init() {
instance = new BluetoothManager();
}
@Override
void connect(String mac) {
// TODO Auto-generated method stub
}
}
這樣對外掛來說只能看到一個instance,具體實現對外掛是不可見的。
現在為了攔截所有的介面呼叫,我們需要做一點改動,將抽象類中的函式分離出介面IBluetoothManager:
public abstract class AbsBluetoothManager implements IBluetoothManager {
static IBluetoothManager instance;
public static IBluetoothManager getInstance() {
return instance;
}
}
public interface IBluetoothManager {
void connect(String mac);
}
在主APP的實現類中用動態代理給instance包一層:
public class BluetoothManager extends AbsBluetoothManager {
public static void init() {
if (instance == null) {
BluetoothManager manager = new BluetoothManager();
instance = (IBluetoothManager) Proxy.newProxyInstance(
BluetoothManager.class.getClassLoader(),
new Class<?>[] {IBluetoothManager.class},
new ProxyHandler(manager));
}
}
private static class ProxyHandler implements InvocationHandler {
private Object subject;
ProxyHandler(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object object, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
Object result = null;
// 檢查呼叫許可權
checkPermission();
try {
result = method.invoke(subject, args);
// 介面呼叫上報
addCallRecord();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
};
@Override
public void connect(String mac) {
// TODO Auto-generated method stub
System.out.println(mac);
}
}
這樣每次調到介面中的函式時,都會被攔截一遍,在裡面我們可以做一些檢查,或者乾脆改變函式的執行方向。