加载插件时需要获取到插件APK的详细信息,比如AndroidManifest.xml中注册的四大组件。Android SDK提供了接口,如下:
PackageManager pm = getPackageManager();
PackageInfo packageInfo = pm.getPackageArchiveInfo(
apkFile.getAbsolutePath(), PackageManager.GET_ACTIVITIES
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_RECEIVERS
| PackageManager.GET_META_DATA);
ActivityInfo[] activityInfos = packageInfo.activities;
ApplicationInfo applicationinfo = packageInfo.applicationInfo;
Bundle metaData = applicationinfo.metaData;
ServiceInfo[] serviceInfos = packageInfo.services;
ProviderInfo[] providerInfos = packageInfo.providers;
ActivityInfo[] receiverInfos = packageInfo.receivers;
我们重点关注一下Activity,发现ActivityInfo中基本的信息都有,包括name、processName、theme、configChanges、launchMode等,不过缺少一个非常关键的东西,那就是Intent-Filter。
为什么我会专门提到这个呢?如果我们启动插件中的组件时直接指定类名,那确实不用理会Intent-Filter了,直接按name查找组件就OK了。比如我们目前使用的插件框架,调起插件Apk中的Activity都是通过指定Activity的类名来查找对应的Activity并将其当做普通类调起,然后在一个Proxy Activity中调用其生命周期的回调。如果要调起插件Apk中的Launcher Activity,宿主APP是无法知道谁是插件的Launcher Activity的,因为我们拿不到Intent-Filter。解决办法是在插件的AndroidManifest.xml的meta-data中声明一个专用类,宿主APP会调用该类的特定函数来启动Launcher Activity,由于是插件内部类调的,所以插件自己当然知道哪个是Launcher Activity,宿主就不用操心了。这一系列做法虽然能解决问题,不过总显得很累赘和不自然,别人为了做插件需要额外增加不少工作量,而本文就是为了解决这一系列问题提出一些思路。
这里核心问题是如何在SDK未提供相应接口的情况下获取Activity的Intent Filter。
我们先看看pm.getPackageArchiveInfo的实现,或许其中可以发现点什么:
public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) {
final PackageParser parser = new PackageParser();
final File apkFile = new File(archiveFilePath);
try {
PackageParser.Package pkg = parser.parseMonolithicPackage(apkFile, 0);
PackageUserState state = new PackageUserState();
return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0, null, state);
} catch (PackageParserException e) {
return null;
}
}
这里先新建一个PackageParser,调用parseMonolithicPackage生成Package,然后调用其静态函数generatePackageInfo来解析并生成PackageInfo。值得一提的是这个parseMonolithicPackage用于解析单独的apk文件,如果是apk的目录则用parseClusterPackage函数解析。在PackageParser中有一个函数会自动根据apk文件类型来分别调用对应的解析函数,如下:
public Package parsePackage(File packageFile, int flags) throws PackageParserException {
if (packageFile.isDirectory()) {
return parseClusterPackage(packageFile, flags);
} else {
return parseMonolithicPackage(packageFile, flags);
}
}
接下来我们看看parseMonolithicPackage的实现:
public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
final AssetManager assets = new AssetManager();
try {
final Package pkg = parseBaseApk(apkFile, assets, flags);
pkg.codePath = apkFile.getAbsolutePath();
return pkg;
} finally {
IoUtils.closeQuietly(assets);
}
}
可见这里核心是调用了parseBaseApk,继续看下去:
private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags,
String[] outError) throws XmlPullParserException, IOException {
..........
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals("application")) {
..........
if (!parseBaseApplication(pkg, res, parser, attrs, flags, outError)) {
return null;
}
}
..........
}
..........
}
这里开始解析apk中的AndroidManifest.xml文件了,重点是看application标签的解析,调用的是parseBaseApplication,四大组件都在这里面解析的。
private boolean parseBaseApplication(Package owner, Resources res,
XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)
throws XmlPullParserException, IOException {
..........
final int innerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals("activity")) {
Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false,
owner.baseHardwareAccelerated);
owner.activities.add(a);
} else if (tagName.equals("receiver")) {
Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true, false);
owner.receivers.add(a);
} else if (tagName.equals("service")) {
Service s = parseService(owner, res, parser, attrs, flags, outError);
owner.services.add(s);
} else if (tagName.equals("provider")) {
Provider p = parseProvider(owner, res, parser, attrs, flags, outError);
owner.providers.add(p);
} else if (tagName.equals("activity-alias")) {
Activity a = parseActivityAlias(owner, res, parser, attrs, flags, outError);
owner.activities.add(a);
} else if (parser.getName().equals("meta-data")) {
owner.mAppMetaData = parseMetaData(res, parser, attrs, owner.mAppMetaData,
outError)
}
..........
}
..........
return true;
}
这里省略了不少代码,不过核心部分非常鲜明,就是解析xml中的标签,我们重点关注activity:
private Activity parseActivity(Package owner, Resources res,
XmlPullParser parser, AttributeSet attrs, int flags, String[] outError,
boolean receiver, boolean hardwareAccelerated)
throws XmlPullParserException, IOException {
.........
Activity a = new Activity(mParseActivityArgs, new ActivityInfo());
.........
int outerDepth = parser.getDepth();
int type;
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG
|| parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
if (parser.getName().equals("intent-filter")) {
ActivityIntentInfo intent = new ActivityIntentInfo(a);
if (!parseIntent(res, parser, attrs, true, true, intent, outError)) {
return null;
}
if (intent.countActions() > 0) {
a.intents.add(intent);
}
}
.........
}
return a;
}
private boolean parseIntent(Resources res, XmlPullParser parser, AttributeSet attrs,
boolean allowGlobs, boolean allowAutoVerify, IntentInfo outInfo, String[] outError)
throws XmlPullParserException, IOException {
..........
int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String nodeName = parser.getName();
if (nodeName.equals("action")) {
String value = attrs.getAttributeValue(
ANDROID_RESOURCES, "name");
outInfo.addAction(value);
} else if (nodeName.equals("category")) {
String value = attrs.getAttributeValue(
ANDROID_RESOURCES, "name");
outInfo.addCategory(value);
}
..........
}
..........
return true;
}
可见这里解析到了Intent Filter,并添加到了Activity的intents中。那为什么最终我们获取不到Intent Filter呢?我们来看看这些数据结构,先看看Activity:
public final static class Activity extends Component<ActivityIntentInfo> {
public final ActivityInfo info;
..........
}
这个类是PackageParser的静态内部类,是继承自Component,里面只有一个ActivityInfo,这个很眼熟,就是最终开放给我们的数据结构,里面是没有Intent Filter的,看来都在这个Component里。
public static class Component<II extends IntentInfo> {
public final Package owner;
public final ArrayList<II> intents;
public final String className;
public Bundle metaData;
ComponentName componentName;
String componentShortName;
..........
}
果然Intent Filter在这里面,是个List,刚传入的类型是ActivityIntentInfo,如下:
public final static class ActivityIntentInfo extends IntentInfo {
public final Activity activity;
..........
}
public static class IntentInfo extends IntentFilter {
public boolean hasDefault;
public int labelRes;
public CharSequence nonLocalizedLabel;
public int icon;
public int logo;
public int banner;
public int preferred;
}
public class IntentFilter implements Parcelable {
private int mPriority;
private final ArrayList<String> mActions;
private ArrayList<String> mCategories = null;
private ArrayList<String> mDataSchemes = null;
private ArrayList<PatternMatcher> mDataSchemeSpecificParts = null;
private ArrayList<AuthorityEntry> mDataAuthorities = null;
private ArrayList<PatternMatcher> mDataPaths = null;
private ArrayList<String> mDataTypes = null;
private int mVerifyState;
..........
}
好了,这么多数据结构,我们总结一下,Activity继承自Component,Component里面有个ActivityIntentInfo的列表,这个ActivityIntentInfo是继承自IntentFilter,里面有我们关注的Intent Filter的mActions和mCategories。
接下来问题来了,既然解析Apk的时候是有这些Intent Filter的,并且保存到了Activity中,这些Activity又保存到了PackageParser.Package中,为什么最终我们获取不到呢?关键是下面这个函数,将解析出来的Package作为参数,返回的是PackageInfo,大概明白了,虽然Package中什么信息都有了,但是最终返给我们的是一个阉割版的PackageInfo。
public static PackageInfo generatePackageInfo(PackageParser.Package p,
int gids[], int flags, long firstInstallTime, long lastUpdateTime,
HashSet<String> grantedPermissions, PackageUserState state, int userId) {
PackageInfo pi = new PackageInfo();
..........
if ((flags & PackageManager.GET_GIDS) != 0) {
pi.gids = gids;
}
if ((flags & PackageManager.GET_CONFIGURATIONS) != 0) {
..........
}
if ((flags & PackageManager.GET_ACTIVITIES) != 0) {
int N = p.activities.size();
..........
for (int i = 0, j = 0; i < N; i++) {
final Activity activity = p.activities.get(i);
pi.activities[j++] = generateActivityInfo(p.activities.get(i), flags,
state, userId);
}
}
if ((flags & PackageManager.GET_RECEIVERS) != 0) {
..........
}
if ((flags & PackageManager.GET_SERVICES) != 0) {
..........
}
if ((flags & PackageManager.GET_PROVIDERS) != 0) {
..........
}
if ((flags & PackageManager.GET_INSTRUMENTATION) != 0) {
..........
}
if ((flags & PackageManager.GET_PERMISSIONS) != 0) {
..........
}
if ((flags & PackageManager.GET_SIGNATURES) != 0) {
..........
}
return pi;
}
我们还是重点看看Activity的逻辑,generateActivityInfo是如何阉割Package中的Activity的:
public static final ActivityInfo generateActivityInfo(Activity a, int flags,
PackageUserState state, int userId) {
if (!copyNeeded(flags, a.owner, state, a.metaData, userId)) {
return a.info;
}
ActivityInfo ai = new ActivityInfo(a.info);
ai.metaData = a.metaData;
ai.applicationInfo = generateApplicationInfo(a.owner, flags, state, userId);
return ai;
}
可见这里就是直接返回了Activity中的ActivityInfo,而Intent Filter就这样被阉割掉了。虽然很无奈,不过没关系,接下来我们想办法怎么给这个Intent Filter再找回来。不过遗憾的是这个Package貌似没有全局缓存起来,解析完就丢掉了。这么说如果我们要重新获取到Package的话只能再解析一遍了,调用的就是我们上面提到的PackageParser的parsePackage函数。
首先我们需要获取一个PackageParser,参考getPackageArchiveInfo的做法,直接new一个就好了,不过这里我们获取不到这个类,只能通过反射了。获取到PackageParser后,调用其parsePackage函数,传入apk路径,flags设为0即可。如下:
private void parseApk(File apkFile) {
try {
Class<?> clazz = Class.forName("android.content.pm.PackageParser");
Object packageParser = MethodUtils.invokeConstructor(clazz, apkFile.getAbsolutePath());
Object packageObj = MethodUtils.invokeMethod(packageParser, "parsePackage", apkFile, apkFile.getAbsolutePath(), 0);
List activies = (List) FieldUtils.readField(packageObj, "activities");
for (Object data : activies) {
List<IntentFilter> filters = (List) FieldUtils.readField(data, "intents");
for (IntentFilter filter : filters) {
for (int i = 0; i < filter.countActions(); i++) {
Log.i("bush", "filter: " + filter.getAction(i));
}
for (int i = 0; i < filter.countCategories(); i++) {
Log.i("bush", "filter: " + filter.getCategory(i));
}
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
不过这里要注意的是适配不同sdk版本,parsePackage参数可能不一样。
好了,到这里我们的目的就算达到了,拿到了Intent Filter,调起apk的时候就可以直接启动Launcher Activity了。此外,我们可以直接通过Intent而不是指定类名来启动插件中的Activity了,因为我们可以自己去resolve这个intent从而在各个插件apk中找到最匹配的Activity。