前面我们已经编译好了资源共享库lib-out.apk(包名:com.google.android.test.shared_library),已经引用这个共享库的应用app-out.apk(包名:com.google.android.test.lib_client),剩下的就是安装、运行起来了。这个自然不必说,但是运行的时候我们的这个App使如何加载我们的资源共享库的呢?还记不记得我们在上期的两个疑问,还有一个期待中的车祸现场?
简要说一下应用的启动过程
我们知道,当init进程起来后,它会解析init.****.rc文件,进行许多操作,比如挂载分区、权限控制等,还会启动许多服务和进程,比如service_manager、media_server、Zygote等。其中Zygote进程是system_server以及我们所有Android应用程序进程的父进程。它的代码就在frameworks/base/cmds/app_process下,我们这里不做过多介绍,后面会有文章专门讨论。Zygote进程起来后,会创建虚拟机,然后加载ZygoteInit.java,执行其main函数,然后就进入java世界,fork出systemServer,然后它就会创建socket,作为服务端等待并处理来自systemServer的fork出其它应用进程的请求。
当我们的App进程被fork出来后,它会调用ActivityTread的main方法,在ActivityThread的main方法里,它会通过AttachApplication这个binder调用把自己的IApplicationThread接口传给systemServer(或者说AMS),这样就建立起了我们的应用进程和AMS的双向通信机制(跨进程的,一方是应用进程,一方是systemServer进程)。AMS在处理我们的应用进程的AttachApplication请求时,会再次通过IApplicationThread回调应用进程的bindApplication方法,并且会带上一大堆的参数。我们的应用进程则会根据这些参数,设置自己的进程名,创建Application,并执行它的onCreate方法,这就进入到我们熟悉的流程了。
从bindApplication的一个参数说起
从AMS的bindApplication方法传过来的参数有很多:
我们现在只关注第二个,也就是ApplicationInfo。不知道大家是否还对《Android资源管理中的SharedLibrary和Dynamic Reference-------之资源共享库(一)》中介绍的ApplicationInfo有印象。我们一起回忆一下:
是的,ApplicatonInfo的sharedLibraryFiles变量将会是我们的资源共享库包com.google.android.test.shared_library的路径。
ApplicatonInfo.sharedLibraryFiles的值从哪儿来
既然这个sharedLibraryFiles就是我们的App引用的资源库的路径,那么系统是怎么获取到它的呢?其实这个很简单,包相关的信息都是PMS负责解析和保存的,这个肯定也不例外。我们在我们的App的AndroidManifest.xml里使用了uses-library标签,那么PMS肯定能解析到。我们简单跟一下:
private final boolean attachApplicationLocked(int pid) {
.........
ApplicationInfo appInfo = app.instrumentationInfo != null
? app.instrumentationInfo : app.info;
.........
thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,
profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,
app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace,
isRestrictedBackupMode || !normalMode, app.persistent,
new Configuration(mConfiguration), app.compat,
getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked());
}
这里的app是ProcessRecord的实例,而ProcessRecord在构造的时候会传入一个ApplicationInfo实例。这个ApplicationInfo实例是这么来的呢:
ActivityThread.java的实现:
直接从ServiceManager里获取代理了,对应的实现类为PackageManagerService,也就是
PMS,跟进去:
这个比较简单,进入PackageParser.java。顾名思义,这个类就是用来解析包信息的:
public static ApplicationInfo generateApplicationInfo(Package p, int flags,
PackageUserState state, int userId) {
if (p == null) return null;
if (!checkUseInstalledOrHidden(flags, state)) {
return null;
}
if (!copyNeeded(flags, p, state, null, userId)
&& ((flags&PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS) == 0
|| state.enabled != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED)) {
// In this case it is safe to directly modify the internal ApplicationInfo state:
// - CompatibilityMode is global state, so will be the same for every call.
// - We only come in to here if the app should reported as installed; this is the
// default state, and we will do a copy otherwise.
// - The enable state will always be reported the same for the application across
// calls; the only exception is for the UNTIL_USED mode, and in that case we will
// be doing a copy.
updateApplicationInfo(p.applicationInfo, flags, state);
return p.applicationInfo;
}
// Make shallow copy so we can store the metadata/libraries safely
ApplicationInfo ai = new ApplicationInfo(p.applicationInfo);
if (userId != 0) {
ai.uid = UserHandle.getUid(userId, ai.uid);
ai.dataDir = PackageManager.getDataDirForUser(userId, ai.packageName);
}
if ((flags & PackageManager.GET_META_DATA) != 0) {
ai.metaData = p.mAppMetaData;
}
if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0) {
ai.sharedLibraryFiles = p.usesLibraryFiles;
}
if (state.stopped) {
ai.flags |= ApplicationInfo.FLAG_STOPPED;
} else {
ai.flags &= ~ApplicationInfo.FLAG_STOPPED;
}
updateApplicationInfo(ai, flags, state);
return ai;
}
我们终于看到了,sharedLibraryFiles来自于PackageParser里的包信息。那PackageParser又是如何得到的呢?当然是解析AndroidManifest.xml得到的了,我们这次直接看它的parseBaseApk方法:
private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags,
String[] outError) throws XmlPullParserException, IOException {
//解析包名
//解析version name
//解析version code
//解析sharedUId信息
//解析每一个元素
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
.........
//终于解析到 uses-library了,好亲切啊
} else if (tagName.equals("uses-library")) {
sa = res.obtainAttributes(attrs,
com.android.internal.R.styleable.AndroidManifestUsesLibrary);
// Note: don't allow this value to be a reference to a resource
// that may change.
String lname = sa.getNonResourceString(
com.android.internal.R.styleable.AndroidManifestUsesLibrary_name);
boolean req = sa.getBoolean(
com.android.internal.R.styleable.AndroidManifestUsesLibrary_required,
true);
sa.recycle();
if (lname != null) {
lname = lname.intern();
if (req) {
//这个是必须依赖的库,没有的话,我们安装的时候就会失败
//对应 android:required="true"
owner.usesLibraries = ArrayUtils.add(owner.usesLibraries, lname);
} else {
//这个是可选的依赖库,安装时不做检查,但是如果运行时找不到,还会出错
//对应android:required="false"
owner.usesOptionalLibraries = ArrayUtils.add(
owner.usesOptionalLibraries, lname);
}
}
XmlUtils.skipCurrentTag(parser);
}
.........
}
}
终于解析完了,但还不完全,我们说了ApplicationInfo.sharedLibraryFiles是资源库的路径,但是此时我们看到PackageParser.Package中存储的却是:
这个属性的值:也就是我们使用的库的名字,这对不上啊!
其实,PMS给我们来了一个小魔法:
SharedLibraryEntry类中,path表示我们的共享库的路径,apk则是共享库的包名。mSharedLibraries则是一个Map,它的key就是共享库的名字。我们看updateSharedLibrariesLPw和addSharedLibraryLPw方法:它会遍历我们的App必须依赖和可选依赖的所有库,并取出他们的名字(注意是库的名字,不是包名),然后根据库的名字,拿到对应的SharedLibraryEntry,再从SharedLibraryEntry中根据包名,找到对应的Package对象,再取出真正的路径,并把这个路径放到一个临时数组usesLibraryFiles中。最后,用这个临时数组替换掉Package对象中对应的数组!
不过,现在有个问题,mSharedLibraries这个Map中的元素哪儿来的呢?肯定是PMS扫描包的时候,从一个一个的共享库包中添加过来的。其实它有两处来源,一处是在PMS构造的时候:
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
..........
ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
for (int i=0; i<libConfig.size(); i++) {
mSharedLibraries.put(libConfig.keyAt(i),
new SharedLibraryEntry(libConfig.valueAt(i), null));
}
..........
}
我们看到,它会从SystemConfig里读取,不过SystemConfig里的数据,似乎只有库的名字,和路径,包名是null:new SharedLibraryEntry(libConfig.valueAt(i), null)。我们看看SystemConfig是怎么实现的:
//frameworks/base/services/core/java/com/android/server/SystemConfig.java
SystemConfig() {
// Read configuration from system
readPermissions(Environment.buildPath(
Environment.getRootDirectory(), "etc", "sysconfig"), false);
// Read configuration from the old permissions dir
readPermissions(Environment.buildPath(
Environment.getRootDirectory(), "etc", "permissions"), false);
// Only read features from OEM config
readPermissions(Environment.buildPath(
Environment.getOemDirectory(), "etc", "sysconfig"), true);
readPermissions(Environment.buildPath(
Environment.getOemDirectory(), "etc", "permissions"), true);
}
构造的时候从/etc和/oem/etc目录的permissions和sysconfig目录读取配置文件:
void readPermissions(File libraryDir, boolean onlyFeatures) {
.........
for (File f : libraryDir.listFiles()) {
// We'll read platform.xml last
if (f.getPath().endsWith("etc/permissions/platform.xml")) {
platformFile = f;
continue;
}
if (!f.getPath().endsWith(".xml")) {
Slog.i(TAG, "Non-xml file " + f + " in " + libraryDir + " directory, ignoring");
continue;
}
if (!f.canRead()) {
Slog.w(TAG, "Permissions library file " + f + " cannot be read");
continue;
}
readPermissionsFromXml(f, onlyFeatures);
}
if (platformFile != null) {
readPermissionsFromXml(platformFile, onlyFeatures);
}
}
解析之:
private void readPermissionsFromXml(File permFile, boolean onlyFeatures) {
...........
XmlPullParser parser = Xml.newPullParser();
..........
else if ("library".equals(name) && !onlyFeatures) {
String lname = parser.getAttributeValue(null, "name");
String lfile = parser.getAttributeValue(null, "file");
if (lname == null) {
Slog.w(TAG, "<library> without name in " + permFile + " at "
+ parser.getPositionDescription());
} else if (lfile == null) {
Slog.w(TAG, "<library> without file in " + permFile + " at "
+ parser.getPositionDescription());
} else {
//Log.i(TAG, "Got library " + lname + " in " + lfile);
mSharedLibraries.put(lname, lfile);
}
XmlUtils.skipCurrentTag(parser);
continue;
}
}
这部分代码很简单,解析<library>标签,并把结果写入mSharedLibraries,我们就不多做说明,大家可以看一下/etc/permissions/platform.xml里的内容,有个直观的了解:
果然,只有库的名字和路径。从这里我们看到,我们可以通过这样的配置文件来添加一个资源共享库,但这需要系统权限,这种方法适合各个手机厂商。
PMS的mSharedLibraries还有另外一处来源,就是在扫描各个包的时候,我们就不做详细介绍了,有兴趣的同学可以自己到PMS源码里查看。
系统用ApplicatonInfo.sharedLibraryFiles做了啥
现在我们已经完全知道了ApplicatonInfo.sharedLibraryFiles的来龙,那么去脉呢?Framework都用它做了什么呢?我们继续回到App启动时的bindApplication方法,当然这是个binder方法,会从AMS那边调用到我们的App进程。前面我们讲到这时候应用进程会设置自己的进程名,创建Base Context,创建Application,并执行它的onCreate方法等等。这里我们重点看一下Base Context的创建:
//frameworks/base/core/java/android/app/ContextImpl.java
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
return new ContextImpl(null, mainThread,
packageInfo, null, null, false, null, null);
}
Context创建的时候会传入两个非常重要的参数:mainThread和packageInfo。其实,Context我们平常都会经常使用,并叫它上下文,但这是一个非常模糊的概念,不同的人对Context的理解可能有所不同,我个人认为主要可以从三个方面来理解:
1. 它内部有ActivityThread的实例。而ActivityThread及其内部Binder接口IApplicationThread是我们的应用进程和SystemServer双向交互的桥梁,也就是说Context会代表我们的应用进程SystemServer交互,接受SystemServer的调度。比如我们的Application、四大组件的生命周期管理,都离不开这个交互调度的桥梁。
2.它的内部有LoadedApk的实例。LoadedApk虽然还有其它作用,但在Context里面,它的作用主要体现为包信息载体。比如我们的应用包名、版本、使用的库等等信息。
3.它的内部有我们的资源。资源的加载,引用,都是通过它来完成的。
这里我们先简要说一下资源的封装:ContextImpl--->Resources----->AssetManager。AssetManager是Resources的一个成员,Resources又是ContextImpl的一个成员。ContextImpl在构造的时候,会去加载资源,会走到LoadedApk的getResources方法:
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
我们接下来看看mApplicationInfo.sharedLibraryFiles 最终传给了谁:
//ActivityThread.java
/**
* Creates the top level resources for the given package.
*/
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, Configuration overrideConfiguration,
LoadedApk pkgInfo) {
return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null);
}
传给了ResourcesManager:
public Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
Resources r;
//先去查缓存,如果已经加载过,直接返回
..............
//创建AssetManager,真正的资源管理类对象
AssetManager assets = new AssetManager();
//我们的应用本身也是一个资源包,应该把它的路径加入到AssetManager
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
...............
//加入所有引用的资源共享库路径
if (libDirs != null) {
for (String libDir : libDirs) {
if (assets.addAssetPath(libDir) == 0) {
Slog.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
.................
//创建Resources类对象
r = new Resources(assets, dm, config, compatInfo, token);
return r;
}
到此,我们已经十分熟悉了,熟悉的Resources类,我们的已经把资源库加入到它的AssetManager里面去了,但是我们能够通过Resources.getString(),拿到资源库里的string资源吗?