做插件开发有两个问题需要解决,一个是资源文件加载,另一个是关于四大组件生命周期的管理。这里我们就简单分析会遇到那些坑,和一些简单的处理方法或者思路。
插件开发目前已经不是什么最新技术了,目前市面上已有很多成熟的方案和开源工程,比如任玉刚
的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
17protected 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
5public 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
37public 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
20public 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
6public 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
21public 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
16public 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
28public 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
21public 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
82public 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
24public 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
3Field 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
51public 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
22public 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
7public 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
25static 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
82bool 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
21public 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
12public 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
17private 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
9static 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
62static 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
19public 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
11public 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
49static 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;
}
然而并无卵用。。。。。。还有那些注释的尝试,也毫无作用= 。 =
如果大家知道原因的话,或者对这一块儿还有更好的实现方案,麻烦多多指教,在此提前献上妹子图。
剩下的坑
关于四大组件生命周期的管理也是一个难点,这里限于篇幅只能止步于此。如果以后有时间的话,我会努力补上。