插件开发中的资源问题分析及填坑处理

       做插件开发有两个问题需要解决,一个是资源文件加载,另一个是关于四大组件生命周期的管理。这里我们就简单分析会遇到那些坑,和一些简单的处理方法或者思路。

       插件开发目前已经不是什么最新技术了,目前市面上已有很多成熟的方案和开源工程,比如任玉刚dynamic-load-apk、阿里的AndFixdexposed、360的DroidPlugin、QQ空间的nuwa。各家实现方案也是各有不同,这些开源库大多已经广泛应用于很多市面上的软件。
       说到未来,不得不提一下ReactNative,移动应用web化一定是一个必然的趋势,就好像曾经的桌面应用由C/S到B/S的转变。而怎么web化才是关键之处。但目前RN在IOS开发中优势很明显,在Android中却是挖坑不断。

普通插件开发

开发前提

       Android为我们从ClassLoader派生出了两个类:DexClassLoaderPathClassLoader。在加载类的时候,是执行父类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 {
@Override
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 {
......
@Override
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) {

@Override
public LayoutInflater cloneInContext(Context newContext) {
// TODO Auto-generated method stub
return null;
}

@Override
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) {

@Override
public LayoutInflater cloneInContext(Context newContext) {
// TODO Auto-generated method stub
return null;
}

@Override
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();
}
}
};
}

@Override
public ClassLoader getClassLoader() {
return mClassLoader ;
}

@Override
public AssetManager getAssets() {
return mResources.getAssets();
}

@Override
public Resources getResources() {
return mResources;
}

@Override
public Object getSystemService(String name) {
if (name == Context.LAYOUT_INFLATER_SERVICE)
return mInflater;
return super.getSystemService(name);
}

private Theme mTheme;
@Override
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 {
......
@Override
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;

}

@Override
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方法
@Override
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文件:
查看ABI
       这里获取到的值是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结构如下:
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;
}

       然而并无卵用。。。。。。还有那些注释的尝试,也毫无作用= 。 =
       如果大家知道原因的话,或者对这一块儿还有更好的实现方案,麻烦多多指教,在此提前献上妹子图。
妹子图

剩下的坑

       关于四大组件生命周期的管理也是一个难点,这里限于篇幅只能止步于此。如果以后有时间的话,我会努力补上。

坚持技术分享,您的支持将鼓励我继续创作!