Android插件化探索(三)免安装运行Activity(上)
2016-05-29 08:56
471 查看
转载请注明本文出自maplejaw的博客(http://blog.csdn.net/maplejaw_)
【Android插件化探索(一)类加载器DexClassLoader】
【Android插件化探索(二)资源加载】
现在我们就从源码角度来分析getResourcesForApplication。源码在ApplicationPackageManager中,如下:
可以看出内部调用了重载方法。getApplicationInfo返回的是ApplicationInfo对象。
最终走的仍旧是ActivityThread的getTopLevelResources,ActivityThread里面的相关源码我就不分析了,跟上一篇是一样的也是调用ResourcesManager中的getTopLevelResources,这里不做赘述。
现在我们主要来看看getApplicationInfo里面做了什么?
mPM的初始化源码如下,可以看出是一个PMS(PackageManagerService)对象。
继续深究,找出PMS中相关源码。
可以看出会去mPackages中找,然而根本就找不到,因为根本就没有安装。
从上面可以看出,AssetManager比getResourcesForApplication要灵活很多,使用场景也更广。
我们先来看看按照之前的写法会发生什么状况吧,首先在插件的PluginClass中加入启动Activity的代码如下:
然后修改核心测试代码,分别测试两种形式。
果然没有想象中那么轻松,直接报错提示。
提示找不到Activity,是否在AndroidManifest.xml中声明?说的也是,并没有在宿主APK中进行声明啊,插件APK的清单是没有效果的。于是怀着满满的自信在AndroidManifest.xml中加入声明。
笔者心想,这回应该可以了吧,再次运行测试。WTF!又报错。
从打印信息可以看出提示没有找到该类。这就奇怪了,明明可以找到PluginClass类,为什么提示找不到PluginActivity这个类呢?简直没有道理啊。
为了进行对比,笔者故意修改核心测试代码去加载一个不存在的PluginClass2类,看看有什么提示。
同样提示找不到该类。
但是!!!注意看
我们先不想其他问题,暂时不去研究startActivity的源码(下篇探索动态代理会进行研究)。我们先来想一个解决思路,有没有一种方法可以将dex目录指向到插件APK的dex?
还记得我们在第一篇DexClassLoader中提到过,一个BaseClassLoader对应一个
那么我们把启动Activity的那个ClassLoader替换成我们的,不就间接的改变了dex目录指向吗?你可能会担心,替换成我们的ClassLoader,那宿主APK中的类还找得到吗?由于双亲委托原则,会首先从父ClassLoader中去找,只要我们的父ClassLoader是默认的系统ClassLoader即可。
所以,我们现在的任务是要把ClassLoader替换掉,翻了翻源码,发现ClassLoader对象在LoadedApk中
![](https://img-blog.csdn.net/20160528200011523)
而ActivityThread中有着相关引用。
![](https://img-blog.csdn.net/20160528200350857)
于是做了如下反射替换。
插件Activity的代码如下:
运行测试通过,Activity是能启动了,生命周期完全正常,但是发现资源却完全加载不了,一片白(也有加载到宿主界面的,那是因为资源id刚好和插件的重复)!控制台打印信息如下:
![](https://img-blog.csdn.net/20160528202227942)
可以看出代码路径和资源路径全部指向了宿主APK,即使使用loadResources也完全没有效果,因为一个Activity一个Context,我们的loadResources只对那个Activity的Context有效果。迫不得已,又去翻看了源码,最后在上面的反射基础中加入如下反射修改LoadedApk中的mResDir代码。
测试,启动成功,加载出插件的界面。查看控制台,发现成功修改资源目录,生命周期完全正常。
![](http://static.zybuluo.com/maplejaw/97q1j6ljl1afx9db0906d279/image_1ajsv6rti1ocj1umj10un1kqc11b5l.png)
但是呢,这种方法是有弊端的,因为反射导致它彻底改变了资源目录,假如你要回到宿主Activity还要重新切换目录才行。不由得想,要是资源也有双亲委托该有多好啊。
首先,一个ClassLoader一个DexPathList。
![](https://img-blog.csdn.net/20160528204512200)
然后,一个DexPathList中含有一个dexElements数组
![](https://img-blog.csdn.net/20160528204650326)
最后,加载类时从dexElements数组中遍历。
![](https://img-blog.csdn.net/20160528204748154)
好了,思路很清晰,通过反射,将插件的dexElements与宿主的合并,并赋值给宿主的dexPathList。
实现方案如下:
测试通过,成功启动Activity。
但是,同样需要在清单文件注册,同样加载不了资源。仍然需要去反射替换掉LoadedApk中的资源目录。
源码下载地址:https://github.com/maplejaw/HotPluginDemo
但是,这两种方案总归很麻烦。有没有更好的方案呢?没错,就是动态代理!
下一篇准备探索动态代理启动Activity。
【Android插件化探索(一)类加载器DexClassLoader】
【Android插件化探索(二)资源加载】
前情提要
在上一篇中有一个细节没有提到,那就是getResourcesForApplication和AssetManager的区别。getResourcesForApplication
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这里就不做赘述了,上一篇已经简单看过,可以直接指定目录。换言之,也就更加灵活。从上面可以看出,AssetManager比getResourcesForApplication要灵活很多,使用场景也更广。
免安装运行Activity(上)
看完了前面的部分,我们知道可以通过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中加入声明。
<activity android:name="com.maplejaw.hotplugin.PluginActivity"/>
笔者心想,这回应该可以了吧,再次运行测试。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?
替换ClassLoader
要更改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中有着相关引用。
于是做了如下反射替换。
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);
测试,启动成功,加载出插件的界面。查看控制台,发现成功修改资源目录,生命周期完全正常。
![](http://static.zybuluo.com/maplejaw/97q1j6ljl1afx9db0906d279/image_1ajsv6rti1ocj1umj10un1kqc11b5l.png)
但是呢,这种方法是有弊端的,因为反射导致它彻底改变了资源目录,假如你要回到宿主Activity还要重新切换目录才行。不由得想,要是资源也有双亲委托该有多好啊。
合并DexPathList
这种方式类似于热修复方案。将插件的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。
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories