在Android開發中免不了使用PackageManager獲取當前應用的一些信息。java
Class for retrieving various kinds of information related to the application packages that are currently installed on the device. You can find this class through Context#getPackageManager.git
從官方文檔上能肯定PackageManager通常都是經過Context的getPackageManager
方法得到的,實際上咱們日常開發中也只有這個途徑。github
顯然,若是插件框架什麼都不作,插件沒有安裝到系統中,PackageManager是不可能能夠查詢到插件的任何信息的。因此,插件框架要處理這個問題。bash
想要讓插件代碼對插件框架無感知,沒必要在插件代碼中寫形如Shadow.getPluginContext().getPluginPackageManager()
之類的代碼,常見的方案就是Override插件的Context的getPackageManager
方法,返回一個PackageManager的子類。而後在子類中Override各類方法,返回插件的信息。app
好比插件中寫這樣的代碼:框架
context.getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA)
複製代碼
在這樣的代碼中getPackageName()
要麼是一個沒有在系統中安裝的PackageName,要麼就是宿主的PackageName。因此PackageManager的子類在OverridegetApplicationInfo
方法時通常要判斷PackageName,而後返回對應的插件的ApplicationInfo。ide
這種實現看起來沒有任何問題,可是實際上線後會發現各類各樣的Crash。緣由在於Android官方系統和OEM系統都會向PackageManager這個抽象類增長抽象的hide方法。好比:post
public abstract class PackageManager {
/**
* 省略註釋
* @hide
*/
public abstract Drawable getUserBadgeForDensity(UserHandle user, int density);
}
複製代碼
getUserBadgeForDensity方法就是能夠在Android官方系統源碼中看到的hide方法。這樣的hide的abstract方法,在繼承子類時是不須要覆蓋就能編譯經過的。可是在運行時系統也會拿到插件的Context,get出咱們的PackageManager子類,而後調用這個hide方法。當系統調用時,就會出現AbstractMethodError而Crash。解決方法也很是簡單,咱們只須要在PackageManager子類中覆蓋這個方法就好了。ui
public Drawable getUserBadgeForDensity(UserHandle user, int density){
return null;
}
複製代碼
也不用寫@Override註解。可是這個hide方法的實現就不能保證是正確的了,這是這種方案的第一個問題。this
第二個問題就是,不光Android官方系統有這些hide方法,OEM系統也有。好比Oppo手機上會有isClosedSuperFirewall()
方法。這樣的話,就須要插件框架不停地兼容各類OEM系統。
上面兩個問題只是表面的,最關鍵的問題是這種實現方法實際上違背了不使用任何非公開API的原則。咱們須要去兼容非公開API實際上也是在使用非公開API。
首先咱們分析了一下,插件代碼有可能使用這些hide方法嗎?不可能,業務代碼就不該該使用非公開API,因此這些hide方法插件本身不會用,只有系統會去使用。系統會須要從這些hide方法中獲取到任何插件的私有信息嗎?也不可能,插件的私有信息就沒有安裝到系統,讓系統知道了既沒用也沒有好處。
那麼要保持hide方法的實現不變,咱們就不能返回PackageManager的子類,不能繼承PackageManager。
可是咱們還須要改變PackageManager的一些公開方法的實現,好比getApplicationInfo
方法。那麼繼承不能用了,還有什麼能修改一個類的方法實現呢?天然是字節碼編輯技術。可是咱們能不能修改系統的PackageManager子類實現呢?確定不能,系統的PackageManager子類是一個私有類,叫什麼名字咱們都不能肯定,就算知道叫什麼了也沒用,系統類是不會打包在插件中的,是存在於系統的BootClassLoader中的。咱們改不到人家的字節碼。
可是咱們還有一個辦法,就是修改插件代碼中對PackageManager的調用代碼,這些調用代碼不是系統代碼而是插件本身的代碼。好比:
public void test() {
PackageManager pm = context.getPackageManager();
ApplicationInfo info = pm.getApplicationInfo("packageName", GET_META_DATA);
}
複製代碼
這裏面對pm對象調用getApplicationInfo
方法的字節碼就是屬於插件代碼的。
因此咱們有機會將這行調用代碼進行修改,若是咱們修改爲這樣:
public void test() {
PackageManager pm = context.getPackageManager();
ApplicationInfo info = staticMethod.getApplicationInfo(pm, "packageName", GET_META_DATA);
}
private static ApplicationInfo staticMethod(PackageManager pm, String packageName, int flags) {
...
...
}
複製代碼
咱們是否是就能夠在staticMethod
方法的實現中任意處理此次調用的全部參數,而後決定返回一個什麼值了?因爲咱們只修改咱們關心的調用,好比getApplicationInfo
方法。因此也就不用像繼承PackageManager同樣,要對每個抽象方法都要實現一遍。
上面說的方法想要實現,須要在字節碼編輯上可以作到非靜態調用改成靜態調用。原本在Javassist中是沒有這種高級API的,可是我研究了一下JVM字節碼的規則,發現了一點有趣的知識。
類A的非靜態方法add和類S的靜態方法add在被調用時,各有5個指令的字節碼。注意,類S的add方法比類A的add方法多了一個類型是A的參數,可是它們被調用是的字節碼只有一點點區別。區別就在於第4條指令,invoke指令的類型和參數。
其他的4條指令,前3條是先將被調用方法的參數壓棧,第一條指令對於非靜態方法的調用來講,就是被調用對象自己。而對於靜態方法調用來講,從第一條指令開始就是調用參數了。因此,對非靜態方法的調用指令改爲靜態調用後,本來被調用對象的壓棧正好就成了靜態方法的第一個參數。最後一條指令是return返回值指令。
實際字節碼編輯只須要修改2個字節,見Shadow的源碼:com.tencent.shadow.core.transform_kit.CodeConverterExtension#redirectMethodCallToStaticMethodCall
。
因此這是一個很是很是通用的AOP手段,能夠將修改任意一個非靜態調用的行爲。由於靜態方法能夠拿到本來的被調用對象自己和本來調用的所有參數。因此如此通用的方法,咱們在開發Shadow時也貢獻回了Javassist:github.com/jboss-javas… 。目前Javassist的最新版本已經包含了這個方法,你們能夠直接使用了。
前面staticMethod
方法在Shadow的實際代碼位於com.tencent.shadow.core.transform.specific.PackageManagerTransform#setupPackageManagerTransform
。能夠看到實際上這個方法對於getApplicationInfo
來講,生成的static方法叫getApplicationInfo_shadow
。
在Shadow裏插件的PackageName都是和宿主同樣的,緣由見 juejin.im/post/5d1357… 。因此,在多插件的場景下,static方法收到都是同樣的PackageName,那static方法的實現就不能區分要返回哪一個插件的ApplicationInfo了。因此,咱們選擇在Loader加載插件時,將插件的ClassLoader做爲key,創建一個ClassLoader反查插件partKey的Map。這樣實現static方法時就能夠實現成:
public static ApplictionInfo getApplicationInfo_shadow(PackageManager pm, String packageName, int flags) {
Classloader classloader = this.getClass().getClassLoader();
return PackageManagerInvokeRedirect.getApplicationInfo(classloader, packageName, flags);
}
複製代碼
將這個static調用再次委託給Runtime層的類PackageManagerInvokeRedirect
,這樣這個static調用的實現能夠用源碼比較方便的撰寫。當前調用getApplicationInfo
方法的類所在的CLassLoader做爲參數傳給它,它就能夠知道這是哪一個插件了。同時注意到,咱們就不須要本來被調用的PackageManager了,可是咱們不能在第一時間放棄這個參數,由於前面講的字節碼編輯的細節,咱們經過字節碼編輯轉調的靜態方法的第一個參數必須是原來被調用的對象。
Shadow中不是真的沒有繼承PackageManager實現子類,是有一個PluginPackageManager
的,這個類中完成了返回插件信息,同時持有了宿主的PackageManager,能夠在查詢非插件信息時用宿主的PackageManager返回。
因此在PluginPackageManager
的邏輯中,凡是PackageName等於宿主的,咱們假設這個代碼想要的就是插件的信息。對於插件真的想要查詢宿主的信息的場景,只能讓插件代碼拿到宿主的Context後直接拿宿主的PackageManager獲取。在Shadow中,插件的Context的baseContext都是宿主的Context,因此能夠經過baseContext得到到宿主的Context。
寫到這裏,也至關於從新Review了一下這塊的實現,發現PluginPackageManager
確實也不須要真的繼承PackageManager。由於這個PackageManager,不管是插件仍是系統都是拿不到的。