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

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

       插件开发目前已经不是什么最新技术了,目前市面上已有很多成熟的方案和开源工程,比如任玉刚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;
}

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值