做插件开发有两个问题需要解决,一个是资源文件加载,另一个是关于四大组件生命周期的管理。这里我们就简单分析会遇到那些坑,和一些简单的处理方法或者思路。
插件开发目前已经不是什么最新技术了,目前市面上已有很多成熟的方案和开源工程,比如任玉刚
的dynamic-load-apk、阿里的AndFix和dexposed、360的DroidPlugin、QQ空间的nuwa。各家实现方案也是各有不同,这些开源库大多已经广泛应用于很多市面上的软件。
说到未来,不得不提一下ReactNative,移动应用web化一定是一个必然的趋势,就好像曾经的桌面应用由C/S到B/S的转变。而怎么web化才是关键之处。但目前RN在IOS开发中优势很明显,在Android中却是挖坑不断。
普通插件开发
开发前提
Android为我们从ClassLoader派生出了两个类:DexClassLoader和PathClassLoader。在加载类的时候,是执行父类ClassLoader的loadClass方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { Class<?> clazz = findLoadedClass(className); if (clazz == null) { try { clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { // Don't want to see this. } if (clazz == null) { clazz = findClass(className); } } return clazz; } |
因此DexClassLoader和PathClassLoader都属于符合双亲委派模型的类加载器(因为它们没有重载loadClass方法)。也就是说,它们在加载一个类之前,回去检查自己以及自己以上的类加载器是否已经加载了这个类。如果已经加载过了,就会直接将之返回,而不会重复加载。
这两者的区别在于DexClassLoader需要提供一个可写的outpath路径,用来释放.apk包或者.jar包中的dex文件。换个说法来说,就是PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。
因此,我们要实现插件开发,需要用DexClassLoader。
基本流程
如果只需要加载插件apk中一个普通的类,只要构造一个DexClassLoader,它的构造方法对每个参数已经说明的很清楚了,我们可以试验一下。
新建一个插件工程TestPlugin,里面放一个类Plugin.java,再放一个简单的方法,即TestPlugin/src/com/example/plugin/Plugin.java:
1 2 3 4 5 | public class Plugin{ public String getCommonStr(){ return "COMMON"; } } |
然后新建一个宿主工程TestHost,在MainActivity里面写一个加载插件的方法,即TestHost/src/com/example/host/MainActivity.java:
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 MainActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadPluginClass(); } private void loadPluginClass(){ ...... //定义DexClassLoader //第一个参数:是dex压缩文件的路径 //第二个参数:是dex解压缩后存放的目录 //第三个参数:是C/C++依赖的本地库文件目录,可以为null //第四个参数:是上一级的类加载器 DexClassLoader dexClassLoader = new DexClassLoader(this.getCacheDir().getAbsolutePath() + File.separator + "TestPlugin.apk", this.getCacheDir().getAbsolutePath(), null, getApplicationContext().getClassLoader()); Class<?> pluginClass = dexClassLoader .loadClass("com.example.plugin.Plugin"); if(pluginClass == null){ Log.e(TAG, "plugin class cann't be found"); return; } Object pluginObject = pluginClass.newInstance(); Method pluginMethod = pluginClass.getMethod("getCommonStr"); if(pluginMethod == null){ Log.e(TAG, "plugin method cann't be found"); return; } String methodStr = (String) pluginMethod .invoke(pluginObject); Log.e(TAG, "Print Method str = " + methodStr); ...... } } |
先安装宿主程序TestHost.apk,然后将插件TestPlugin.apk放到/data/data/com.example.host/cache/下面,再次运行宿主程序,会打印如下log:
Print Method str = COMMON
这个应该比较随意了,会使用DexClassLoader这个类的开发者都是轻车熟路。
加载资源
普通资源
我们知道插件apk中的资源文件是无法直接加载的,因为插件apk并没有安装,所以没有给每个资源生成特定的资源id,所以我们没法使用R.XXX去引用。
不过我们通过android系统安装apk时对资源文件的处理流程中发现可以通过AssetManager这个类完成对插件中资源的引用。Java的源码中发现,它有一个私有方法addAssetPath,只需要将apk的路径作为参数传入,我们就可以获得对应的AssetsManager对象,然后我们就可以使用AssetsManager对象,创建一个Resources对象,然后就可以从Resource对象中访问apk中的资源了。总结如下:
- 新建一个AssetManager对象
- 通过反射调用addAssetPath方法
- 以AssetsManager对象为参数,创建Resources对象即可
我们测试demo可以写一个工具类,省略了一部分,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class PluginBaseImpl extends PluginBase { ...... public Resources loadResource(Context parentContext, String apkPath) { Resources ret = null; try { AssetManager assetManager = AssetManager.class.newInstance(); Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); method.setAccessible(true); method.invoke(assetManager, apkPath); ret = new Resources(assetManager, parentContext.getResources().getDisplayMetrics(), parentContext.getResources().getConfiguration()); Log.e(TAG, "loadResources succeed"); } catch (Exception e) { Log.e(TAG, "loadResources faided"); e.printStackTrace(); } return ret; } ...... } |
然后我们再插件工程里面再添加一个方法,再放入一个简单的资源:
1 2 3 4 5 6 | public class Plugin{ ...... public String getContextStr(Resources resources){ return resources.getString(R.string.plugin_str);//<string name="plugin_str">PLUGIN</string> } } |
测试如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class MainActivity extends Activity { ...省略一些初始化代码... private void loadPluginClass(){ ...... //构造一个DexClassLoader DexClassLoader dexClassLoader = mPluginBase.makeDexClassLoader(APK_PATH, DEX_PATH, null, getApplicationContext().getClassLoader()); Class<?> pluginClass = mDexClassLoader.loadClass("com.example.plugin.Plugin"); ...... Object pluginObject = pluginClass.newInstance(); //加载插件apk资源 Resources pluginResources = mPluginBase.loadResource(this, APK_PATH); Method m2 = pluginClass.getMethod("getContextStr", Resources.class); String methodStr2 = (String) m2.invoke(pluginObject, pluginResources); Log.e(TAG, "Print Resource str = " + methodStr2); ...... } } |
运行之后,打印log如下:
Print Resource str = PLUGIN
Layout资源
如果要使用插件apk里面的layout资源,比如引用某个布局文件TestPlugin/res/layout/plugin.xml,就需要做一做处理。
一般从layout转换成view需要用到LayoutInflate,比如:
1
| View view = LayoutInflater.from(context).inflate(R.layout.plugin, null);
|
但是这个context不能直接传宿主程序的context,否则回报一个资源id没有找到异常。我们跟着LayoutInflate的源码进去看看,问题出在哪儿:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public abstract class LayoutInflater { //Inflate时会调用到 public View inflate(int resource, ViewGroup root, boolean attachToRoot) { //这句返回的resource是宿主程序ContextImpl里的resource,即宿主程序的resource final Resources res = getContext().getResources(); ...... //所以这里在宿主resource里当然找不到插件资源id了,这个里面抛出了异常 final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } } |
我们看到inflate时还是在宿主程序的资源里查找了插件资源,因此回报异常。不过我们可以投机取巧一下,重写一个LayoutInflate的Inflate第二个重载方法。在插件工程里可以做如下测试:
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 Plugin{ ...... public LinearLayout getLinearLayout(Context context, final Resources resources){ LayoutInflater inflater = new LayoutInflater(context) { public LayoutInflater cloneInContext(Context newContext) { // TODO Auto-generated method stub return null; } public View inflate(int resource, ViewGroup root, boolean attachToRoot) { // final Resources res = getContext().getResources(); //注释掉这行 final Resources res = resources; //替换为插件apk资源 final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } }; return (LinearLayout) inflater.inflate(R.layout.plugin_layout, null); } } |
然后在宿主程序里写上测试demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class MainActivity extends Activity { ...省略一些初始化代码... private void loadPluginClass(){ ...... //构造一个DexClassLoader DexClassLoader dexClassLoader = mPluginBase.makeDexClassLoader(APK_PATH, DEX_PATH, null, getApplicationContext().getClassLoader()); Class<?> pluginClass = mDexClassLoader.loadClass("com.example.plugin.Plugin"); ...... Object pluginObject = pluginClass.newInstance(); //加载插件apk资源 Resources pluginResources = mPluginBase.loadResource(this, APK_PATH); //测试插件layout文件 Method m3 = pluginClass.getMethod("getLinearLayout", Context.class, Resources.class); LinearLayout pluginView = (LinearLayout) m3.invoke(pluginObject, this, pluginResources ); this.addContentView(pluginView, new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); ...... } } |
经过测试,插件的layout布局被加入到了宿主界面上,图片就不贴了。
另外三种方式
上面的方法其实还是有些繁琐,如果要封装的完善一些可以尝试下面三种方案:
- 创建一个自己的ContextImpl,Override其方法
- 通过反射,直接替换当前context的mResources私有成员变量
- 反射替换ActivityThread里的Instrumentation,将插件资源和宿主资源整合
(1) 创建自己的Context:
要构建自己的Context,就得继承ContextWrapper类,(Context类和它的一些子类大家应该都清楚)然后重写里面的一些重要方法。实例代码如下:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | public class PluginContext extends ContextWrapper { private static final String TAG = "PluginContext"; private DexClassLoader mClassLoader ; private Resources mResources; private LayoutInflater mInflater; PluginContext(Context context, String pluginPath, String optimizedDirectory, String libraryPath) { super(context.getApplicationContext()); Resources resc = context.getResources(); //隐藏API是这样的 //AssetManager assets = new AssetManager(); AssetManager assets = AssetManager.class.newInstance(); assets.addAssetPath(pluginPath); mClassLoader = new DexClassLoader(pluginPath, optimizedDirectory, libraryPath, context.getClassLoader()); mResources = new Resources(assets, resc.getDisplayMetrics(), resc.getConfiguration(), resc.getCompatibilityInfo(), null); //隐藏API是这样的 //mInflater = PolicyManager.makeNewLayoutInflater(this); mInflater = new LayoutInflater(context) { public LayoutInflater cloneInContext(Context newContext) { // TODO Auto-generated method stub return null; } public View inflate(int resource, ViewGroup root, boolean attachToRoot) { // final Resources res = getContext().getResources(); final Resources res = mResources; final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } }; } public ClassLoader getClassLoader() { return mClassLoader ; } public AssetManager getAssets() { return mResources.getAssets(); } public Resources getResources() { return mResources; } public Object getSystemService(String name) { if (name == Context.LAYOUT_INFLATER_SERVICE) return mInflater; return super.getSystemService(name); } private Theme mTheme; public Resources.Theme getTheme() { if (mTheme == null) { int resid = Resources.selectDefaultTheme(0, getBaseContext().getApplicationInfo().targetSdkVersion); mTheme = mResources.newTheme(); mTheme.applyStyle(resid, true); } return mTheme; } } |
这样我们插件的Context就构造完成了,以后就可以使用这个Context加载插件中的资源文件了。
(2) 替换当前context的mResources私有成员变量:
这个需要在Activity的attachBaseContext方法中替换它的Context,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class MainActivity extends Activity { ...... protected void attachBaseContext(Context newBase) { replaceContextResources(newBase); super.attachBaseContext(newBase); } /** * 使用反射的方式,使用mPluginResources对象,替换Context的mResources对象 * @param context */ public void replaceContextResources(Context context){ try { Field field = context.getClass().getDeclaredField("mResources"); field.setAccessible(true); field.set(context, mPluginResources); Log.e(TAG, "replace resources succeed"); } catch (Exception e) { Log.e(TAG, "replace resources failed"); e.printStackTrace(); } } ...... } |
(3) 反射替换ActivityThread里的Instrumentation,将插件资源和宿主资源整合:
AssetManager的addAssetPath()法调用native层AssetManager对象的addAssetPath()法,通过查看c++代码可以知道,该方法可以被调用多次,每次调用都会把对应资源添加起来,而后来添加的在使用资源是会被首先搜索到。可以怎么理解,C++层的AssetManager有一个存放资源的栈,每次调用addAssetPath()法都会把资源对象压如栈,而在读取搜索资源时是从栈顶开始搜索,找不到就往下查。所以我们可以这样来处理AssetManager并得到Resources。
使用到资源的地方归纳起来有两处,一处是在Java代码中通过Context.getResources获取,一处是在xml文件(如布局文件)里指定资源,其实xml文件里最终也是通过Context来获取资源的只不过是他一般获取的是Resources里的AssetManager。所以,我们可以在Context对象被创建后且还未使用时把它里面的Resources(mResources)替换掉。整个应用的Context数目等于Application+Activity+Service的数目,Context会在这几个类创建对象的时候创建并添加进去。而这些行为都是在ActivityTHread和Instrumentation里做的。
以Activity为例,步骤如下:
1. Activity对象的创建是在ActivityThread里调用Instrumentation的newActivity方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //ActivityThread类 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ...... java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); ...... } //Instrumentation类 public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return (Activity)cl.loadClass(className).newInstance(); } |
2.Context对象的创建是在ActivityThread里调用createBaseContextForActivity方法:
1 2 3 4 5 6 | //ActivityThread类 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ...... Context appContext = createBaseContextForActivity(r, activity); ...... } |
3.Activity绑定Context是在ActivityThread里调用Activity对象的attach方法,其中appContext就是上面创建的Context对象:
1 2 3 4 5 6 7 8 9 | //ActivityThread类 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ...... activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.voiceInteractor); ...... } |
替换掉Activity里Context里的Resources最好要早,基于上面的观察,我们可以在调用Instrumentation的callActivityOnCreate()方法时把Resources替换掉。那么问题又来了,我们如何控制callActivityOnCreate()方法的执行,这里又得使用hook的思想了,即把ActivityThread里面的Instrumentation对象(mInstrumentation)给替换掉,同样得使用反射。步骤如下:
1. 获取ActivityThread对象:
ActivityThread里面有一个静态方法,该方法返回的是ActivityThread对象本身,所以我们可以调用该方法来获取ActivityTHread对象:
1 2 3 4 | //ActivityThread类 public static ActivityThread currentActivityThread() { return sCurrentActivityThread; } |
然而ActivityThread是被hide的,所以得通过反射来处理,处理如下:
1 2 3 4 5 6 7 | //获取ActivityThread类 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); //获取currentActivityThread方法 Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); //获取ActivityThread对象 Object CurrentActivityThread = currentActivityThreadMethod.invoke(null); |
2. 获取ActivityThread里的Instrumentation对象:
1 2 3 | Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation"); mInstrumentationField.setAccessible(true); Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(CurrentActivityThread); |
3. 构建我们自己的Instrumentation对象,并从写callActivityOnCreate方法
在callActivityOnCreate方法里要先获取当前Activity对象里的Context(mBase),再获取Context对象里的Resources(mResources)变量,在把mResources变量指向我们构造的Resources对象,做到移花接木。构建我们的MyInstrumentation类:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public class MyInstrumentation extends Instrumentation { private Instrumentation mInstrumentationParent; private Context mContextParent; public MyInstrumentation(Instrumentation instrumentation, Context context) { super(); mInstrumentationParent = instrumentation; mContextParent = context; } public void callActivityOnCreate(Activity activity, Bundle icicle) { try { Field mBaseField = Activity.class.getSuperclass().getSuperclass().getDeclaredField("mBase"); mBaseField.setAccessible(true); Context mBase = (Context) mBaseField.get(activity); Class<?> contextImplClazz = Class.forName("android.app.ContextImpl"); Field mResourcesField = contextImplClazz.getDeclaredField("mResources"); mResourcesField.setAccessible(true); String dexPath = activity.getCacheDir() + File.separator + "TestPlugin.apk"; String dexPath2 = mContextParent.getApplicationContext().getPackageCodePath(); AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class); addAssetPath.setAccessible(true); addAssetPath.invoke(assetManager, dexPath); addAssetPath.invoke(assetManager, dexPath2); Method ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); ensureStringBlocksMethod.setAccessible(true); ensureStringBlocksMethod.invoke(assetManager); Resources superRes = mContextParent.getResources(); Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); mResourcesField.set(mBase, resources); } catch (Exception e) { e.printStackTrace(); } super.callActivityOnCreate(activity, icicle); } } |
4. 最后,使ActivityThread里面的mInstrumentation变量指向我们构建的MyInstrumentation对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static void hookResources(Context context){ //获取ActivityThread类 Class<?> activityThreadClass; try { activityThreadClass = Class.forName("android.app.ActivityThread"); //获取currentActivityThread方法 Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); //获取ActivityThread对象 Object CurrentActivityThread = currentActivityThreadMethod.invoke(null); //获取Instrumentation变量 Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation"); mInstrumentationField.setAccessible(true); Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(CurrentActivityThread); //构建自己的Instrumentation对象 Instrumentation proxy = new MyInstrumentation(mInstrumentation, context); //移花接木 mInstrumentationField.set(CurrentActivityThread, proxy); } catch (Exception e) { e.printStackTrace(); } } |
加载SO库流程分析和填坑
插件加载带有动态库的apk时,会报UnsatisfiedLinkError找不到动态库的错误原因是我们没有动态指定so库的路径。
解决方法是在DexClassLoader中第三个参数书指定so库的目录路径,因此我们需要把动态库给解压出来放到data/data/xx(package)目录下。
这个,我把so文件放到了/data/data/com.example.host/cache/下面,然后给我们的DexClassLoader第三个参数指定了这个目录,然后在插件工程里调用System.loadLibrary方法就不会报错了。
关于解压so文件和获取手机CPU的ABI类型这里就不在赘述,网上也是大把的代码。我们主要分析一下Android找寻so和加载的流程:
SO库加载过程
在Android中如果想使用so的话,首先得先加载,加载现在主要有两种方法,一种是直接System.loadLibrary方法加载工程中的libs目录下的默认so文件,这里的加载文件名是xxx,而整个so的文件名为:libxxx.so。还有一种是加载指定目录下的so文件,使用System.load方法,这里需要加载的文件名是全路径,比如:xxx/xxx/libxxx.so。
我们可以看看System类的这两个方法:
1 2 3 4 5 6 7 | public static void load(String pathName) { Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader()); } public static void loadLibrary(String libName) { Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader()); } |
这两个方法都会进入到Runtime类的不同方法中,我们继续跟进去:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //load方法比较简单 void load(String absolutePath, ClassLoader loader) { if (absolutePath == null) { throw new NullPointerException("absolutePath == null"); } //都会调用doLoad方法 String error = doLoad(absolutePath, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } } //loadLibrary比较复杂 void loadLibrary(String libraryName, ClassLoader loader) { if (loader != null) {//这个loader就是加载目标类的ClassLoader,宿主工程为系统指定的PathClassLoader,插件工程为我们构造的DexClassLoader //首先会从一些指定目录中查找指定名字的so文件 String filename = loader.findLibrary(libraryName); //如果没有找到就会抛异常 if (filename == null) {//这个异常就是我们没有指定DexClassLoader第三个参数时报的异常 // It's not necessarily true that the ClassLoader used // System.mapLibraryName, but the default setup does, and it's // misleading to say we didn't find "libMyLibrary.so" when we // actually searched for "liblibMyLibrary.so.so". throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\""); } //都会调用doLoad方法 String error = doLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; } //下面逻辑是当指定ClassLoader为null时,就在一些系统so库目录中查找 String filename = System.mapLibraryName(libraryName); List<String> candidates = new ArrayList<String>(); String lastError = null; for (String directory : mLibPaths) { String candidate = directory + filename; candidates.add(candidate); if (IoUtils.canOpenReadOnly(candidate)) { String error = doLoad(candidate, loader); if (error == null) { return; // We successfully loaded the library. Job done. } lastError = error; } } if (lastError != null) { throw new UnsatisfiedLinkError(lastError); } throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates); } |
我们这里详细分析一下loadLibrary方法。首先会判断指定的ClassLoader是否为空,这里传入的值为VMStack.getCallingClassLoader(),就是加载目标类的ClassLoader,宿主工程为系统指定的PathClassLoader,插件工程为我们构造的DexClassLoader。
然后执行:String filename = loader.findLibrary(libraryName);
这一步其实是调用PathClassLoader和DexClassLoader共同父类BaseDexClassLoader的findLibrary方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); //pathList在构造方法中赋值 this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } //BaseDexClassLoader的findLibrary方法 public String findLibrary(String name) { return pathList.findLibrary(name); } |
BaseDexClassLoader的findLibrary方法内部又调用了DexPathList的findLibrary方法:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | //DexPathList的findLibrary方法 public String findLibrary(String libraryName) { //转换指定libraryName为so库文件名,例如turn "MyLibrary" into "libMyLibrary.so". String fileName = System.mapLibraryName(libraryName); //在nativeLibraryDirectories中遍历目标so库是否存在 for (File directory : nativeLibraryDirectories) { String path = new File(directory, fileName).getPath(); if (IoUtils.canOpenReadOnly(path)) { return path; } } return null; } private final File[] nativeLibraryDirectories; public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { ...... //也是在构造方法中给nativeLibraryDirectories 赋值; //libraryPath就是我们在DexClassLoader中指定的第三个参数,系统的PathClassLoader指定为/data/app-lib/xxx(包名) this.nativeLibraryDirectories = splitLibraryPath(libraryPath); } //GO ON 继续跟踪 private static File[] splitLibraryPath(String path) { // Native libraries may exist in both the system and // application library paths, and we use this search order: // // 1. this class loader's library path for application libraries // 2. the VM's library path from the system property for system libraries // // This order was reversed prior to Gingerbread; see http://b/2933456. //System.getProperty("java.library.path")返回的是/vendor/lib:/system/lib ArrayList<File> result = splitPaths(path, System.getProperty("java.library.path"), true); return result.toArray(new File[result.size()]); } //NEXT path1为我们在DexClassLoader中指定的第三个参数,系统的PathClassLoader指定为/data/app-lib/xxx(包名);path2为/vendor/lib:/system/lib;wantDirectories为true private static ArrayList<File> splitPaths(String path1, String path2, boolean wantDirectories) {// ArrayList<File> result = new ArrayList<File>(); splitAndAdd(path1, wantDirectories, result); splitAndAdd(path2, wantDirectories, result); return result; } //FINALLY 用“:”分割路径字符串,并且将这些路径都放入到一个ArrayList中 private static void splitAndAdd(String searchPath, boolean directoriesOnly, ArrayList<File> resultList) { if (searchPath == null) { return; } for (String path : searchPath.split(":")) { try { StructStat sb = Libcore.os.stat(path); if (!directoriesOnly || S_ISDIR(sb.st_mode)) { resultList.add(new File(path)); } } catch (ErrnoException ignored) { } } } |
上述代码就是查找so库文件的逻辑了,会分别在/vendor/lib、/system/lib、/data/app-lib/xxx(包名)、和指定目录下查找,如果找不到,就会报UnsatisfiedLinkError异常。
查找逻辑就先到这里,继续回到Runtime类中。接着就会调用doLoad方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private String doLoad(String name, ClassLoader loader) { ...... String ldLibraryPath = null; if (loader != null && loader instanceof BaseDexClassLoader) { //ldLibraryPath就是上面提到的vendor/lib、/system/lib、/data/app-lib/xxx(包名)、和指定目录用“:”连接的字符串 ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath(); } synchronized (this) { //最后会调用nativeLoad方法 return nativeLoad(name, loader, ldLibraryPath); } } private static native String nativeLoad(String filename, ClassLoader loader, String ldLibraryPath); |
这里调用了本地方法,不过悲催的是,我的ART版本代码没有找到,所以只能看 Dalvik版本的。 Runtime类的成员函数nativeLoad在C++层对应的函数为Dalvik_java_lang_Runtime_nativeLoad,这个函数定义在文件dalvik/vm/native/java_lang_Runtime.c中,如下所示:
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 | static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args, JValue* pResult) { StringObject* fileNameObj = (StringObject*) args[0]; //so库名 Object* classLoader = (Object*) args[1]; //类加载器 char* fileName = NULL; StringObject* result = NULL; char* reason = NULL; bool success; assert(fileNameObj != NULL); //将 fileNameObj 转化为C++层字符串 fileName = dvmCreateCstrFromString(fileNameObj); //调用dvmLoadNativeCode方法 success = dvmLoadNativeCode(fileName, classLoader, &reason); if (!success) { const char* msg = (reason != NULL) ? reason : "unknown failure"; result = dvmCreateStringFromCstr(msg); dvmReleaseTrackedAlloc((Object*) result, NULL); } free(reason); free(fileName); RETURN_PTR(result); } |
参数args[0]保存的是一个Java层的String对象,这个String对象描述的就是要加载的so文件,函数Dalvik_java_lang_Runtime_nativeLoad首先是调用函数dvmCreateCstrFromString来将它转换成一个C++层的字符串fileName,然后再调用函数dvmLoadNativeCode来执行加载so文件的操作。
接下来,我们就继续分析函数dvmLoadNativeCode的实现,这个函数定义在文件dalvik/vm/Native.c中:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | bool dvmLoadNativeCode(const char* pathName, Object* classLoader, char** detail) { SharedLib* pEntry; void* handle; ...... pEntry = findSharedLibEntry(pathName); if (pEntry != NULL) { if (pEntry->classLoader != classLoader) { ...... return false; } ...... if (!checkOnLoadResult(pEntry)) return false; return true; } ...... handle = dlopen(pathName, RTLD_LAZY); ...... /* create a new entry */ SharedLib* pNewEntry; pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib)); pNewEntry->pathName = strdup(pathName); pNewEntry->handle = handle; pNewEntry->classLoader = classLoader; ...... /* try to add it to the list */ SharedLib* pActualEntry = addSharedLibEntry(pNewEntry); if (pNewEntry != pActualEntry) { ...... freeSharedLibEntry(pNewEntry); return checkOnLoadResult(pActualEntry); } else { ...... bool result = true; void* vonLoad; int version; vonLoad = dlsym(handle, "JNI_OnLoad"); if (vonLoad == NULL) { LOGD("No JNI_OnLoad found in %s %p, skipping init\n", pathName, classLoader); } else { ...... OnLoadFunc func = vonLoad; ...... version = (*func)(gDvm.vmList, NULL); ...... if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && version != JNI_VERSION_1_6) { ....... result = false; } else { LOGV("+++ finished JNI_OnLoad %s\n", pathName); } } ...... if (result) pNewEntry->onLoadResult = kOnLoadOkay; else pNewEntry->onLoadResult = kOnLoadFailed; ...... return result; } } |
函数dvmLoadNativeCode首先是检查参数pathName所指定的so文件是否已经加载过了,这是通过调用函数findSharedLibEntry来实现的。如果已经加载过,那么就可以获得一个SharedLib对象pEntry。这个SharedLib对象pEntry描述了有关参数pathName所指定的so文件的加载信息,例如,上次用来加载它的类加载器和上次的加载结果。如果上次用来加载它的类加载器不等于当前所使用的类加载器,或者上次没有加载成功,那么函数dvmLoadNativeCode就回直接返回false给调用者,表示不能在当前进程中加载参数pathName所描述的so文件。
这里有一个检测异常的代码,而这个错误,是我们在使用插件开发加载so的时候可能会遇到的错误,比如现在我们使用DexClassLoader类去加载插件,但是因为我们为了插件能够实时更新,所以每次都会赋值新的DexClassLoader对象,但是第一次加载so文件到内存中了,这时候退出程序,但是没有真正意义上的退出,只是关闭了Activity了,这时候再次启动又会赋值新的加载器对象,那么原先so已经加载到内存中了,但是这时候是新的类加载器那么就报错了,解决办法其实很简单,主要有两种方式:
第一种方式:在退出程序的时候采用真正意义上的退出,比如调用System.exit(0)方法,这时候进程被杀了,加载到内存的so也就被释放了,那么下次赋值新的类加载就在此加载so到内存了,
第二种方式:就是全局定义一个static类型的类加载DexClassLoader也是可以的,因为static类型是保存在当前进程中,如果进程没有被杀就一直存在这个对象,下次进入程序的时候判断当前类加载器是否为null,如果不为null就不要赋值了,但是这个方法有一个弊端就是类加载器没有从新赋值,如果插件这时候更新了,但是还是使用之前的加载器,那么新插件将不会进行加载。
我们假设参数pathName所指定的so文件还没有被加载过,这时候函数dvmLoadNativeCode就会先调用dlopen来在当前进程中加载它,并且将获得的句柄保存在变量handle中,接着再创建一个SharedLib对象pNewEntry来描述它的加载信息。这个SharedLib对象pNewEntry还会通过函数addSharedLibEntry被缓存起来,以便可以知道当前进程都加载了哪些so文件。
注意,在调用函数addSharedLibEntry来缓存新创建的SharedLib对象pNewEntry的时候,如果得到的返回值pActualEntry指向的不是SharedLib对象pNewEntry,那么就表示另外一个线程也正在加载参数pathName所指定的so文件,并且比当前线程提前加载完成。在这种情况下,函数addSharedLibEntry就什么也不用做而直接返回了。否则的话,函数addSharedLibEntry就要继续负责调用前面所加载的so文件中的一个指定的函数来注册它里面的JNI方法。
这个指定的函数的名称为“JNI_OnLoad”,也就是说,每一个用来实现JNI方法的so文件都应该定义有一个名称为“JNI_OnLoad”的函数,并且这个函数的原型为:
1
| jint JNI_OnLoad(JavaVM* vm, void* reserved);
|
函数dvmLoadNativeCode通过调用函数dlsym就可以获得在前面加载的so中名称为“JNI_OnLoad”的函数的地址,最终保存在函数指针func中。有了这个函数指针之后,我们就可以直接调用它来执行注册JNI方法的操作了。注意,在调用该JNI_OnLoad函数时,第一个要传递进行的参数是一个JavaVM对象,这个JavaVM对象描述的是在当前进程中运行的Dalvik虚拟机,第二个要传递的参数可以设置为NULL,这是保留给以后使用的。
到这里我们就总结一下Android中加载so的流程:
- 调用System.loadLibrary和System.load方法进行加载so文件
- 通过Runtime.java类的nativeLoad方法进行最终调用,这里需要通过类加载器获取到nativeLib路径
- 到底层之后,就开始使用dlopen方法加载so文件,然后使用dlsym方法调用JNI_OnLoad方法,最终开始了so的执行
释放SO库文件
我们在使用System.loadLibrary加载so的时候,传递的是so文件的libxxx.so中的xxx部分,那么系统是如何找到这个so文件然后进行加载的呢?这个就要先从apk文件安装时机说起。
Android系统在启动的过程中,会启动一个应用程序管理服务PackageManagerService,这个服务负责扫描系统中特定的目录,找到里面的应用程序文件,即以Apk为后缀的文件,然后对这些文件进解析,得到应用程序的相关信息,完成应用程序的安装过程。
应用程序管理服务PackageManagerService安装应用程序的过程,其实就是解析析应用程序配置文件AndroidManifest.xml的过程,并从里面得到得到应用程序的相关信息,例如得到应用程序的组件Activity、Service、Broadcast Receiver和Content Provider等信息,有了这些信息后,通过ActivityManagerService这个服务,我们就可以在系统中正常地使用这些应用程序了。
下面我们一步一步分析:
我们知道Android系统系统启动时会启动Zygote进程,Zygote进程又会启动SystemServer组件,启动的时候就会调用它的main函数,然后会初始化一系列服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public final class SystemServer { private PackageManagerService mPackageManagerService; ...... public static void main(String[] args) { new SystemServer().run(); } private void run() { ...... startBootstrapServices(); ...... } private void startBootstrapServices() { mPackageManagerService = PackageManagerService.main(mSystemContext, mInstaller, mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore); } ...... } |
中间会启动PackageManagerService,这个函数定义在frameworks/base/services/java/com/android/server/PackageManagerService.java文件中:
1 2 3 4 5 6 7 8 9 10 11 12 | public class PackageManagerService extends IPackageManager.Stub { ...... public static final PackageManagerService main(Context context, Installer installer, boolean factoryTest, boolean onlyCore) { PackageManagerService m = new PackageManagerService(context, installer, factoryTest, onlyCore); ServiceManager.addService("package", m); return m; } ...... } |
这个函数创建了一个PackageManagerService服务实例,然后把这个服务添加到ServiceManager中去, 在创建这个PackageManagerService服务实例时,会在PackageManagerService类的构造函数中开始执行安装应用程序的过程:
1 2 3 4 5 6 7 | public PackageManagerService(Context context, Installer installer, boolean factoryTest, boolean onlyCore) { ...... scanPackageLI(scanFile, reparseFlags, scanFlags, 0, null); ...... } |
PackageManagerService的构造方法中就完成了对apk文件的解包,还有对xm文件的解析等等,感兴趣的可以自己分析。这里我们限于篇幅,就只分析so文件的解包过程。
这里会调用scanPackageLI方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags, long currentTime, UserHandle user) throws PackageManagerException { ...... // Note that we invoke the following method only if we are about to unpack an application PackageParser.Package scannedPkg = scanPackageLI(pkg, parseFlags, scanFlags | SCAN_UPDATE_SIGNATURE, currentTime, user); ...... } private PackageParser.Package scanPackageLI(PackageParser.Package pkg, int parseFlags, int scanFlags, long currentTime, UserHandle user) throws PackageManagerException { ...... final PackageParser.Package res = scanPackageDirtyLI(pkg, parseFlags, scanFlags, currentTime, user); ...... } |
经过一系列重载方法调用,最终会调用scanPackageDirtyLI方法:
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 38 39 40 41 42 | private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg, int parseFlags, int scanFlags, long currentTime, UserHandle user) throws PackageManagerException { //初始化so库放置的目录,并赋值给pkg setNativeLibraryPaths(pkg); final boolean isAsec = isForwardLocked(pkg) || isExternal(pkg); //nativeLibraryRootStr 指定为/data/app-lib/xxx(包名) final String nativeLibraryRootStr = pkg.applicationInfo.nativeLibraryRootDir; //false final boolean useIsaSpecificSubdirs = pkg.applicationInfo.nativeLibraryRootRequiresIsa; NativeLibraryHelper.Handle handle = null; ...... //标记打开apk handle = NativeLibraryHelper.Handle.create(scanFile); final File nativeLibraryRoot = new File(nativeLibraryRootStr); // Null out the abis so that they can be recalculated. pkg.applicationInfo.primaryCpuAbi = null; pkg.applicationInfo.secondaryCpuAbi = null; ...... String[] abiList = (cpuAbiOverride != null) ? new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS; ...... final int copyRet; if (isAsec) { copyRet = NativeLibraryHelper.findSupportedAbi(handle, abiList); } else { //解压对应ABI的so文件到指定目录 copyRet = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle, nativeLibraryRoot, abiList, useIsaSpecificSubdirs); } ...... } |
scanPackageDirtyLI首先调用setNativeLibraryPaths方法,这个方法主要是指定一下so库释放路径:
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 | private void setNativeLibraryPaths(PackageParser.Package pkg) { final ApplicationInfo info = pkg.applicationInfo; final String codePath = pkg.codePath; final File codeFile = new File(codePath); final boolean bundledApp = isSystemApp(info) && !isUpdatedSystemApp(info); final boolean asecApp = isForwardLocked(info) || isExternal(info); info.nativeLibraryRootDir = null; info.nativeLibraryRootRequiresIsa = false; info.nativeLibraryDir = null; info.secondaryNativeLibraryDir = null; if (isApkFile(codeFile)) { // Monolithic install if (bundledApp) { ...... } else if (asecApp) { ...... } else { final String apkName = deriveCodePathName(codePath); //mAppLib32InstallDir为/data/app-lib/ info.nativeLibraryRootDir = new File(mAppLib32InstallDir, apkName) .getAbsolutePath(); } info.nativeLibraryRootRequiresIsa = false; info.nativeLibraryDir = info.nativeLibraryRootDir; } else { ...... } } |
然后调用NativeLibraryHelper.Handle.create(scanFile)标记打开apk文件:
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 38 39 40 41 42 43 44 45 46 47 | public static class Handle implements Closeable { ...... final long[] apkHandles; final boolean multiArch; public static Handle create(File packageFile) throws IOException { try { final PackageLite lite = PackageParser.parsePackageLite(packageFile, 0); return create(lite); } catch (PackageParserException e) { throw new IOException("Failed to parse package: " + packageFile, e); } } public static Handle create(Package pkg) throws IOException { return create(pkg.getAllCodePaths(), (pkg.applicationInfo.flags & ApplicationInfo.FLAG_MULTIARCH) != 0); } public static Handle create(PackageLite lite) throws IOException { return create(lite.getAllCodePaths(), lite.multiArch); } //最后调用到这里 private static Handle create(List<String> codePaths, boolean multiArch) throws IOException { final int size = codePaths.size(); final long[] apkHandles = new long[size]; for (int i = 0; i < size; i++) { final String path = codePaths.get(i); //调用这个native方法,打开apk,并将JNI层返回的句柄保留到java层 apkHandles[i] = nativeOpenApk(path); ...... } return new Handle(apkHandles, multiArch); } Handle(long[] apkHandles, boolean multiArch) { this.apkHandles = apkHandles; this.multiArch = multiArch; mGuard.open("close"); } } //NativeLibraryHelper的nativeOpenApk方法 private static native long nativeOpenApk(String path); |
经过一系列重载方法调用,最后会调用NativeLibraryHelper的nativeOpenApk方法,打开apk,并将JNI层返回的句柄保留到java层。这个方法的实现位于frameworks/base/core/jni/com_android_internal_content_NativeLibraryHelper.cpp中:
1 2 3 4 5 6 7 8 9 | static jlong com_android_internal_content_NativeLibraryHelper_openApk(JNIEnv *env, jclass, jstring apkPath) { ScopedUtfChars filePath(env, apkPath); ZipFileRO* zipFile = ZipFileRO::open(filePath.c_str()); return reinterpret_cast<jlong>(zipFile); } |
上述代码调用了ZipFileRO的open方法,并返回一个ZipFileRO类型的指针,然后强转为java层的long型对象返回给java层。open方法实现位于frameworks/base/libs/androidfw/ZipFileRO.cpp中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /* * Open the specified file read-only. We memory-map the entire thing and * close the file before returning. */ /* static */ ZipFileRO* ZipFileRO::open(const char* zipFileName) { ZipArchiveHandle handle; //调用ZipArchive库打开zip文件 const int32_t error = OpenArchive(zipFileName, &handle); if (error) { ALOGW("Error opening archive %s: %s", zipFileName, ErrorCodeString(error)); return NULL; } return new ZipFileRO(handle, strdup(zipFileName)); } |
这些就是JNI层打开apk文件的操作了。我么继续回到scanPackageDirtyLI方法中,接着调用NativeLibraryHelper.copyNativeBinariesForSupportedAbi方法:
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 | public static int copyNativeBinariesForSupportedAbi(Handle handle, File libraryRoot, String[] abiList, boolean useIsaSubdir) throws IOException { //如果目录不存或者是个文件,就重新创建目录 createNativeLibrarySubdir(libraryRoot); /* * If this is an internal application or our nativeLibraryPath points to * the app-lib directory, unpack the libraries if necessary. */ //查找对应的ABI类型 int abi = findSupportedAbi(handle, abiList); if (abi >= 0) { /* * If we have a matching instruction set, construct a subdir under the native * library root that corresponds to this instruction set. */ //获取so释放之后的目录 final String instructionSet = VMRuntime.getInstructionSet(abiList[abi]); final File subDir; if (useIsaSubdir) { final File isaSubdir = new File(libraryRoot, instructionSet); createNativeLibrarySubdir(isaSubdir); subDir = isaSubdir; } else { subDir = libraryRoot; } //拷贝so int copyRet = copyNativeBinaries(handle, subDir, abiList[abi]); if (copyRet != PackageManager.INSTALL_SUCCEEDED) { return copyRet; } } return abi; } |
我们挑一些重要的分析一下。这里先获取abiList的值,这个通过Build.SUPPORTED_ABIS来获取到的:
1
| public static final String[] SUPPORTED_ABIS = getStringList("ro.product.cpu.abilist", ",");
|
最终是通过获取系统属性ro.product.cpu.abilist的值来得到的,我们可以使用getprop命令来查看这个属性值,或者直接cat一下/system/build.prop文件:
这里获取到的值是x86。然后去分析findSupportedAbi方法:
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 | public static int findSupportedAbi(Handle handle, String[] supportedAbis) { int finalRes = NO_NATIVE_LIBRARIES; for (long apkHandle : handle.apkHandles) { //这里调用了native方法 final int res = nativeFindSupportedAbi(apkHandle, supportedAbis); if (res == NO_NATIVE_LIBRARIES) { // No native code, keep looking through all APKs. } else if (res == INSTALL_FAILED_NO_MATCHING_ABIS) { // Found some native code, but no ABI match; update our final // result if we haven't found other valid code. if (finalRes < 0) { finalRes = INSTALL_FAILED_NO_MATCHING_ABIS; } } else if (res >= 0) { // Found valid native code, track the best ABI match if (finalRes < 0 || res < finalRes) { finalRes = res; } } else { // Unexpected error; bail return res; } } return finalRes; } private native static int nativeFindSupportedAbi(long handle, String[] supportedAbis); |
NativeLibraryHelper类的findSupportedAbi方法,其实这个方法就是查找系统当前支持的架构型号索引值。调用的本地方法实现为:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | static jint com_android_internal_content_NativeLibraryHelper_findSupportedAbi(JNIEnv *env, jclass clazz, jlong apkHandle, jobjectArray javaCpuAbisToSearch) { return (jint) findSupportedAbi(env, apkHandle, javaCpuAbisToSearch); } //会调用这个方法 static int findSupportedAbi(JNIEnv *env, jlong apkHandle, jobjectArray supportedAbisArray) { const int numAbis = env->GetArrayLength(supportedAbisArray); Vector<ScopedUtfChars*> supportedAbis; for (int i = 0; i < numAbis; ++i) { supportedAbis.add(new ScopedUtfChars(env, (jstring) env->GetObjectArrayElement(supportedAbisArray, i))); } //读取apk文件 ZipFileRO* zipFile = reinterpret_cast<ZipFileRO*>(apkHandle); if (zipFile == NULL) { return INSTALL_FAILED_INVALID_APK; } UniquePtr<NativeLibrariesIterator> it(NativeLibrariesIterator::create(zipFile)); if (it.get() == NULL) { return INSTALL_FAILED_INVALID_APK; } ZipEntryRO entry = NULL; char fileName[PATH_MAX]; int status = NO_NATIVE_LIBRARIES; //这里开始遍历apk中每一个文件 while ((entry = it->next()) != NULL) { // We're currently in the lib/ directory of the APK, so it does have some native // code. We should return INSTALL_FAILED_NO_MATCHING_ABIS if none of the // libraries match. if (status == NO_NATIVE_LIBRARIES) { status = INSTALL_FAILED_NO_MATCHING_ABIS; } const char* fileName = it->currentEntry(); const char* lastSlash = it->lastSlash(); // Check to see if this CPU ABI matches what we are looking for. const char* abiOffset = fileName + APK_LIB_LEN; const size_t abiSize = lastSlash - abiOffset; //遍历apk中的子文件,获取so文件的全路径,如果这个路径包含了cpu架构值,就记录返回索引 for (int i = 0; i < numAbis; i++) { const ScopedUtfChars* abi = supportedAbis[i]; if (abi->size() == abiSize && !strncmp(abiOffset, abi->c_str(), abiSize)) { // The entry that comes in first (i.e. with a lower index) has the higher priority. if (((i < status) && (status >= 0)) || (status < 0) ) { status = i; } } } } for (int i = 0; i < numAbis; ++i) { delete supportedAbis[i]; } return status; } |
这里看到了,会先读取apk文件,然后遍历apk文件中的so文件,得到全路径然后在和传递进来的abiList进行比较,得到合适的索引值。我们刚才拿到的abiList为:x86,然后就开始比较apk中有没有这些架构平台的so文件,如果有,就直接返回abiList中的索引值即可。比如apk中libs结构如下:
那么这个时候就只有这么一种架构,libs文件下也有相关的ABI类型,就只能返回0了;
假设我们的abiList为:arm64-v8a,armeabi-v7a,armeabi。那么这时候返回来的索引值就是0,代表的是arm64-v8a架构的。如果apk文件中没有arm64-v8a目录的话,那么就返回1,代表的是armeabi-v7a架构的。依次类推。得到应用支持的架构索引之后就可以获取so释放到设备中的目录了。
下一步就是获取so释放之后的目录,调用VMRuntime.java中的getInstructionSet方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static String getInstructionSet(String abi) { final String instructionSet = ABI_TO_INSTRUCTION_SET_MAP.get(abi); if (instructionSet == null) { throw new IllegalArgumentException("Unsupported ABI: " + abi); } return instructionSet; } private static final Map<String, String> ABI_TO_INSTRUCTION_SET_MAP = new HashMap<String, String>(); static { ABI_TO_INSTRUCTION_SET_MAP.put("armeabi", "arm"); ABI_TO_INSTRUCTION_SET_MAP.put("armeabi-v7a", "arm"); ABI_TO_INSTRUCTION_SET_MAP.put("mips", "mips"); ABI_TO_INSTRUCTION_SET_MAP.put("mips64", "mips64"); ABI_TO_INSTRUCTION_SET_MAP.put("x86", "x86"); ABI_TO_INSTRUCTION_SET_MAP.put("x86_64", "x86_64"); ABI_TO_INSTRUCTION_SET_MAP.put("arm64-v8a", "arm64"); } |
这一步主要是对获得的ABI架构字符串做了一下转换,比如从x86—>x86,armeabi—>arm等等。
最后就是释放so了,调用copyNativeBinaries方法:
1 2 3 4 5 6 7 8 9 10 11 | public static int copyNativeBinaries(Handle handle, File sharedLibraryDir, String abi) { for (long apkHandle : handle.apkHandles) { int res = nativeCopyNativeBinaries(apkHandle, sharedLibraryDir.getPath(), abi); if (res != INSTALL_SUCCEEDED) { return res; } } return INSTALL_SUCCEEDED; } private native static int nativeCopyNativeBinaries(long handle, String sharedLibraryPath, String abiToCopy); |
JNI层实现如下:
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 38 39 40 41 42 43 44 45 46 47 48 49 | static jint com_android_internal_content_NativeLibraryHelper_copyNativeBinaries(JNIEnv *env, jclass clazz, jlong apkHandle, jstring javaNativeLibPath, jstring javaCpuAbi) { //调用iterateOverNativeFiles方法,copyFileIfChanged是个函数指针,完成释放 return (jint) iterateOverNativeFiles(env, apkHandle, javaCpuAbi, copyFileIfChanged, &javaNativeLibPath); } static install_status_t iterateOverNativeFiles(JNIEnv *env, jlong apkHandle, jstring javaCpuAbi, iterFunc callFunc, void* callArg) { ZipFileRO* zipFile = reinterpret_cast<ZipFileRO*>(apkHandle); if (zipFile == NULL) { return INSTALL_FAILED_INVALID_APK; } UniquePtr<NativeLibrariesIterator> it(NativeLibrariesIterator::create(zipFile)); if (it.get() == NULL) { return INSTALL_FAILED_INVALID_APK; } const ScopedUtfChars cpuAbi(env, javaCpuAbi); if (cpuAbi.c_str() == NULL) { // This would've thrown, so this return code isn't observable by // Java. return INSTALL_FAILED_INVALID_APK; } ZipEntryRO entry = NULL; while ((entry = it->next()) != NULL) { const char* fileName = it->currentEntry(); const char* lastSlash = it->lastSlash(); // Check to make sure the CPU ABI of this file is one we support. const char* cpuAbiOffset = fileName + APK_LIB_LEN; const size_t cpuAbiRegionSize = lastSlash - cpuAbiOffset; if (cpuAbi.size() == cpuAbiRegionSize && !strncmp(cpuAbiOffset, cpuAbi.c_str(), cpuAbiRegionSize)) { //释放so,这一句才是关键,copyFileIfChanged完成释放 install_status_t ret = callFunc(env, callArg, zipFile, entry, lastSlash + 1); if (ret != INSTALL_SUCCEEDED) { ALOGV("Failure for entry %s", lastSlash + 1); return ret; } } } return INSTALL_SUCCEEDED; |
最后的释放工作都交给了copyFileIfChanged函数,我们看看这个函数:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | /* * Copy the native library if needed. * * This function assumes the library and path names passed in are considered safe. */ static install_status_t copyFileIfChanged(JNIEnv *env, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntry, const char* fileName) { jstring* javaNativeLibPath = (jstring*) arg; ScopedUtfChars nativeLibPath(env, *javaNativeLibPath); size_t uncompLen; long when; long crc; time_t modTime; if (!zipFile->getEntryInfo(zipEntry, NULL, &uncompLen, NULL, NULL, &when, &crc)) { ALOGD("Couldn't read zip entry info\n"); return INSTALL_FAILED_INVALID_APK; } else { struct tm t; ZipUtils::zipTimeToTimespec(when, &t); modTime = mktime(&t); } // Build local file path const size_t fileNameLen = strlen(fileName); char localFileName[nativeLibPath.size() + fileNameLen + 2]; if (strlcpy(localFileName, nativeLibPath.c_str(), sizeof(localFileName)) != nativeLibPath.size()) { ALOGD("Couldn't allocate local file name for library"); return INSTALL_FAILED_INTERNAL_ERROR; } *(localFileName + nativeLibPath.size()) = '/'; if (strlcpy(localFileName + nativeLibPath.size() + 1, fileName, sizeof(localFileName) - nativeLibPath.size() - 1) != fileNameLen) { ALOGD("Couldn't allocate local file name for library"); return INSTALL_FAILED_INTERNAL_ERROR; } // Only copy out the native file if it's different. //只有so本地文件改变了才拷贝 struct stat64 st; if (!isFileDifferent(localFileName, uncompLen, modTime, crc, &st)) { return INSTALL_SUCCEEDED; } char localTmpFileName[nativeLibPath.size() + TMP_FILE_PATTERN_LEN + 2]; if (strlcpy(localTmpFileName, nativeLibPath.c_str(), sizeof(localTmpFileName)) != nativeLibPath.size()) { ALOGD("Couldn't allocate local file name for library"); return INSTALL_FAILED_INTERNAL_ERROR; } *(localFileName + nativeLibPath.size()) = '/'; if (strlcpy(localTmpFileName + nativeLibPath.size(), TMP_FILE_PATTERN, TMP_FILE_PATTERN_LEN - nativeLibPath.size()) != TMP_FILE_PATTERN_LEN) { ALOGI("Couldn't allocate temporary file name for library"); return INSTALL_FAILED_INTERNAL_ERROR; } //生成一个临时文件,用于拷贝 int fd = mkstemp(localTmpFileName); if (fd < 0) { ALOGI("Couldn't open temporary file name: %s: %s\n", localTmpFileName, strerror(errno)); return INSTALL_FAILED_CONTAINER_ERROR; } //解压so文件 if (!zipFile->uncompressEntry(zipEntry, fd)) { ALOGI("Failed uncompressing %s to %s\n", fileName, localTmpFileName); close(fd); unlink(localTmpFileName); return INSTALL_FAILED_CONTAINER_ERROR; } close(fd); // Set the modification time for this file to the ZIP's mod time. struct timeval times[2]; times[0].tv_sec = st.st_atime; times[1].tv_sec = modTime; times[0].tv_usec = times[1].tv_usec = 0; if (utimes(localTmpFileName, times) < 0) { ALOGI("Couldn't change modification time on %s: %s\n", localTmpFileName, strerror(errno)); unlink(localTmpFileName); return INSTALL_FAILED_CONTAINER_ERROR; } // Set the mode to 755 static const mode_t mode = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; if (chmod(localTmpFileName, mode) < 0) { ALOGI("Couldn't change permissions on %s: %s\n", localTmpFileName, strerror(errno)); unlink(localTmpFileName); return INSTALL_FAILED_CONTAINER_ERROR; } // Finally, rename it to the final name. if (rename(localTmpFileName, localFileName) < 0) { ALOGI("Couldn't rename %s to %s: %s\n", localTmpFileName, localFileName, strerror(errno)); unlink(localTmpFileName); return INSTALL_FAILED_CONTAINER_ERROR; } ALOGV("Successfully moved %s to %s\n", localTmpFileName, localFileName); return INSTALL_SUCCEEDED; } |
上述就是解压so文件的实现。先判断so名字合不合法,然后判断是不是文件改变了,再者创建一个临时文件,最后解压,用临时文件拷贝so到指定目录,结尾处关闭一些链接。
小结一下上述SO释放流程:
- 通过遍历apk文件中的so文件的全路径,然后和系统的abiList中的类型值进行比较,如果匹配到了就返回arch类型的索引值
- 得到了应用所支持的arch类型之后,就开始获取创建本地释放so的目录
- 然后开始释放so文件
失败的尝试
上面我们分析了插件apk中加载so库,必须指定DexClassLoader中第三个参数,这就要我们解压apk中的so了。所以我试着调用系统的NativeLibraryHelper相关方法,做了如下实验:
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 38 39 | @SuppressLint("NewApi") @Override public boolean loadSO(File apkFile, File nativeLibraryRoot) { NativeLibraryHelper.Handle handle = null; try { handle = NativeLibraryHelper.Handle.create(apkFile); //private static Handle create(List<String> codePaths, boolean multiArch) throws IOException /*Method create2 = NativeLibraryHelper.Handle.class.getDeclaredMethod("create", List.class, boolean.class); create2.setAccessible(true); List<String> apkList = new ArrayList<String>(); apkList.add(apkFile.getAbsolutePath()); handle = (Handle) create2.invoke(null, apkList, false);*/ /*Method nativeOpenApk = NativeLibraryHelper.class.getDeclaredMethod("nativeOpenApk", String.class); nativeOpenApk.setAccessible(true); long apkHandle = (long) nativeOpenApk.invoke(null, apkFile.getAbsolutePath()); Method nativeClose = NativeLibraryHelper.class.getDeclaredMethod("nativeClose", long.class); nativeOpenApk.setAccessible(true); nativeClose.invoke(null, apkHandle); Constructor<Handle> constructMethod = NativeLibraryHelper.Handle.class.getConstructor(long[].class, boolean.class); constructMethod.setAccessible(true); handle = constructMethod.newInstance(new long[]{apkHandle}, false);*/ NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle, nativeLibraryRoot, Build.SUPPORTED_ABIS, false); } catch (Exception e) { e.printStackTrace(); }finally{ if (handle != null) { try { handle.close(); } catch (Exception e) { e.printStackTrace(); } } } return false; } |
然而并无卵用。。。。。。还有那些注释的尝试,也毫无作用= 。 =
如果大家知道原因的话,或者对这一块儿还有更好的实现方案,麻烦多多指教,在此提前献上妹子图。