編輯:關於Android編程
在上一篇中有一個細節沒有提到,那就是getResourcesForApplication和AssetManager的區別。
getResourcesForApplication(String packageName),很顯然需要傳入一個包名,換言之,這個插件必須已經被安裝在系統內,然後才能通過包名來獲取資源。你可能會想,不安裝照樣可以獲取包名啊。的確,通過pm.getPackageArchiveInfo()
可以獲取安裝包信息。但是,這些包都是沒有在PMS中注冊的。如果仍然這樣獲取,會提示如下錯誤。
android.content.pm.PackageManager$NameNotFoundException: com.maplejaw.hotplugin
現在我們就從源碼角度來分析getResourcesForApplication。源碼在ApplicationPackageManager中,如下:
@Override
public Resources getResourcesForApplication(String appPackageName)
throws NameNotFoundException {
return getResourcesForApplication(
getApplicationInfo(appPackageName, sDefaultFlags));
}
可以看出內部調用了重載方法。getApplicationInfo返回的是ApplicationInfo對象。
@Override
public Resources getResourcesForApplication(@NonNull ApplicationInfo app){
//...
//省略了部分源碼
final Resources r = mContext.mMainThread.getTopLevelResources(
sameUid ? app.sourceDir : app.publicSourceDir,
sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
null, mContext.mPackageInfo);
if (r != null) {
return r;
}
}
最終走的仍舊是ActivityThread的getTopLevelResources,ActivityThread裡面的相關源碼我就不分析了,跟上一篇是一樣的也是調用ResourcesManager中的getTopLevelResources,這裡不做贅述。
現在我們主要來看看getApplicationInfo裡面做了什麼?
@Override
public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException {
ApplicationInfo ai = mPM.getApplicationInfo(packageName, flags, mContext.getUserId());
//...
//省略了部分源碼
throw new NameNotFoundException(packageName);
}
mPM的初始化源碼如下,可以看出是一個PMS(PackageManagerService)對象。
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b);
return sPackageManager;
}
繼續深究,找出PMS中相關源碼。
@Override
public ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) {
if (!sUserManager.exists(userId)) return null;
enforceCrossUserPermission(Binder.getCallingUid(), userId, false, "get application info");
// writer
synchronized (mPackages) {
PackageParser.Package p = mPackages.get(packageName);
if (DEBUG_PACKAGE_INFO) Log.v(
TAG, "getApplicationInfo " + packageName
+ ": " + p);
if (p != null) {
PackageSetting ps = mSettings.mPackages.get(packageName);
if (ps == null) return null;
// Note: isEnabledLP() does not apply here - always return info
return PackageParser.generateApplicationInfo(
p, flags, ps.readUserState(userId), userId);
}
if ("android".equals(packageName)||"system".equals(packageName)) {
return mAndroidApplication;
}
if ((flags & PackageManager.GET_UNINSTALLED_PACKAGES) != 0) {
return generateApplicationInfoFromSettingsLPw(packageName, flags, userId);
}
}
return null;
}
可以看出會去mPackages中找,然而根本就找不到,因為根本就沒有安裝。
AssetManager這裡就不做贅述了,上一篇已經簡單看過,可以直接指定目錄。換言之,也就更加靈活。
從上面可以看出,AssetManager比getResourcesForApplication要靈活很多,使用場景也更廣。
看完了前面的部分,我們知道可以通過DexClassLoader來加載類,通過AssetManager可以來加載資源。可是現在問題來了,怎麼運行一個未安裝APK中的Activity?Activity不僅有類有資源,最最重要的是,它有生命!。
我們先來看看按照之前的寫法會發生什麼狀況吧,首先在插件的PluginClass中加入啟動Activity的代碼如下:
public void startPluginActivity(Context context, Class cls) {
Intent intent=new Intent(context,cls);
context.startActivity(intent);
}
public void startPluginActivity(Context context) {
Intent intent=new Intent(context,PluginActivity.class);
context.startActivity(intent);
}
然後修改核心測試代碼,分別測試兩種形式。
private void useDexClassLoader(String path){
loadResources(path);
File codeDir=getDir("dex", Context.MODE_PRIVATE);
//創建類加載器,把dex加載到虛擬機中
ClassLoader classLoader = new DexClassLoader(path,codeDir.getAbsolutePath() ,null,
this.getClass().getClassLoader());
//獲得包管理器
PackageManager pm = getPackageManager();
PackageInfo packageInfo=pm.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
String packageName=packageInfo.packageName;
try {
Class clazz = classLoader.loadClass(packageName+".PluginClass");
Comm obj = (Comm) clazz.newInstance();
obj.startPluginActivity(this,classLoader.loadClass(packageName+".PluginActivity"));
// obj.startPluginActivity(this);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
果然沒有想象中那麼輕松,直接報錯提示。
android.content.ActivityNotFoundException: Unable to find explicit activity class {com.maplejaw.hotfix/com.maplejaw.hotplugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?
提示找不到Activity,是否在AndroidManifest.xml中聲明?說的也是,並沒有在宿主APK中進行聲明啊,插件APK的清單是沒有效果的。於是懷著滿滿的自信在AndroidManifest.xml中加入聲明。
筆者心想,這回應該可以了吧,再次運行測試。WTF!又報錯。
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.maplejaw.hotfix/com.maplejaw.hotplugin.PluginActivity}: java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginActivity" on path: DexPathList[[zip file "/data/app/com.maplejaw.hotfix-2/base.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]
從打印信息可以看出提示沒有找到該類。這就奇怪了,明明可以找到PluginClass類,為什麼提示找不到PluginActivity這個類呢?簡直沒有道理啊。
為了進行對比,筆者故意修改核心測試代碼去加載一個不存在的PluginClass2類,看看有什麼提示。
java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginClass2" on path: DexPathList[[zip file "/storage/emulated/0/2.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]
同樣提示找不到該類。
但是!!!注意看DexPathList
這裡,它們指向的dex目錄居然不一樣。換言之,它們兩個的ClassLoader不是同一個。
我們先不想其他問題,暫時不去研究startActivity的源碼(下篇探索動態代理會進行研究)。我們先來想一個解決思路,有沒有一種方法可以將dex目錄指向到插件APK的dex?
要更改dex目錄指向談何容易啊,更何況還要同時兼顧兩個dex目錄。幸虧ClassLoader遵循著雙親委托原則,讓這一切變得不是特別困難。
還記得我們在第一篇DexClassLoader中提到過,一個BaseClassLoader對應一個DexPathList
嗎?
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
那麼我們把啟動Activity的那個ClassLoader替換成我們的,不就間接的改變了dex目錄指向嗎?你可能會擔心,替換成我們的ClassLoader,那宿主APK中的類還找得到嗎?由於雙親委托原則,會首先從父ClassLoader中去找,只要我們的父ClassLoader是默認的系統ClassLoader即可。
所以,我們現在的任務是要把ClassLoader替換掉,翻了翻源碼,發現ClassLoader對象在LoadedApk中
而ActivityThread中有著相關引用。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160530/20160530092133119.png" title="\" />
於是做了如下反射替換。
private void replaceClassLoader(ClassLoader dLoader,String resPath){
try{
String packageName = this.getPackageName();
ClassLoader loader=ClassLoader.getSystemClassLoader();
Class loadApkCls =loader.loadClass("android.app.LoadedApk");
Class activityThreadCls =loader.loadClass("android.app.ActivityThread");
//獲取ActivityThread對象
Method currentActivityThreadMethod=activityThreadCls.getMethod("currentActivityThread");
Object currentActivityThread= currentActivityThreadMethod.invoke(null);
//反射獲取mPackages中的LoadedApk
Field filed=activityThreadCls.getDeclaredField("mPackages");
filed.setAccessible(true);
Map mPackages= (Map) filed.get(currentActivityThread);
WeakReference wr = (WeakReference) mPackages.get(packageName);
//反射修改LoadedApk中的mClassLoader
Field classLoaderFiled=loadApkCls.getDeclaredField("mClassLoader");
classLoaderFiled.setAccessible(true);
classLoaderFiled.set(wr.get(),dLoader);
}catch(Exception e){
e.printStackTrace();
}
}
插件Activity的代碼如下:
public class PluginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plugin);
Log.i("JG", "包名:"+getPackageName());
Log.w("JG", "代碼路徑:"+getPackageCodePath());
Log.e("JG", "資源路徑:"+getPackageResourcePath());
}
@Override
protected void onStart() {
super.onStart();
Log.d("JG","onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.d("JG","onResume");
}
@Override
protected void onPause() {
super.onPause();
Log.d("JG","onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.d("JG","onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d("JG","onDestroy");
}
運行測試通過,Activity是能啟動了,生命周期完全正常,但是發現資源卻完全加載不了,一片白(也有加載到宿主界面的,那是因為資源id剛好和插件的重復)!控制台打印信息如下:
可以看出代碼路徑和資源路徑全部指向了宿主APK,即使使用loadResources也完全沒有效果,因為一個Activity一個Context,我們的loadResources只對那個Activity的Context有效果。迫不得已,又去翻看了源碼,最後在上面的反射基礎中加入如下反射修改LoadedApk中的mResDir代碼。
//反射修改LoadedApk中的資源目錄
Field filed2=loadApkCls.getDeclaredField("mResDir");
filed2.setAccessible(true);
filed2.set(wr.get(),resPath);
測試,啟動成功,加載出插件的界面。查看控制台,發現成功修改資源目錄,生命周期完全正常。
但是呢,這種方法是有弊端的,因為反射導致它徹底改變了資源目錄,假如你要回到宿主Activity還要重新切換目錄才行。不由得想,要是資源也有雙親委托該有多好啊。
這種方式類似於熱修復方案。將插件的dexElements插入到系統的dexElements中,這樣我們啟動Activity時就不會提示找不到該類。在第一篇中,我們簡單看過DexPathList源碼,現在再來回顧下。
首先,一個ClassLoader一個DexPathList。
然後,一個DexPathList中含有一個dexElements數組
最後,加載類時從dexElements數組中遍歷。
好了,思路很清晰,通過反射,將插件的dexElements與宿主的合並,並賦值給宿主的dexPathList。
實現方案如下:
private void combinePathList(ClassLoader loader){
//獲取系統的classloader
PathClassLoader pathLoader = (PathClassLoader) getClassLoader();
try {
//反射dexpathlist
Field pathListFiled = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
pathListFiled.setAccessible(true);
//反射dexElements
Field dexElementsFiled=Class.forName("dalvik.system.DexPathList").getDeclaredField("dexElements");
dexElementsFiled.setAccessible(true);
//獲取系統的pathList
Object pathList1= pathListFiled.get(pathLoader);
//獲取系統的dexElements
Object dexElements1=dexElementsFiled.get(pathList1);
//獲取插件的pathlist
Object pathList2= pathListFiled.get(loader);
//獲取插件的dexElements
Object dexElements2=dexElementsFiled.get(pathList2);
//合並dexElements
Object combineDexElements=combineArray(dexElements1,dexElements2);
//設置給系統的dexpathlist
dexElementsFiled.set(pathList1,combineDexElements);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//合並兩個數組,返回一個新數組
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
測試通過,成功啟動Activity。
但是,同樣需要在清單文件注冊,同樣加載不了資源。仍然需要去反射替換掉LoadedApk中的資源目錄。
源碼下載地址:https://github.com/maplejaw/HotPluginDemo
關於上面啟動免安裝Activity的方案,可以看出存在很明顯的缺陷,首先,需要在清單文件提前注冊,此外資源反射修改也很蛋疼。如果不想用反射,我們可以提前將資源內置於宿主中,或者純用JAVA代碼來寫。
但是,這兩種方案總歸很麻煩。有沒有更好的方案呢?沒錯,就是動態代理!
下一篇准備探索動態代理啟動Activity。
首先我們先來看一下效果分析我們來看這個進度條應該分為3個小部分1.中間的圓2.外邊的圓環3.中間的文字分開畫這3部分就是需要我們自己畫出來的,因此我們需要3根畫筆//設置
Android中的Toast是很常見的一個消息提示框,但是默認的消息提示框就是一行純文本,所以我們可以為它設置一些其他的諸如是帶上圖片的消息提示。 實現這個很簡單: 就是
簡介:Volley是Google I/O 2013上Google官方發布的一款Android平台上的網絡通信庫。以前的網絡請求,要考慮開啟線程、內存洩漏、性能等等復雜的問
本文介紹MediaPlayer的使用。MediaPlayer可以播放音頻和視頻,另外也可以通過VideoView來播放視頻,雖然VideoView比MediaPlayer