1. 前言
这次项目接到一个需求,需要尽可能多的展示AndroidManifest.xml 里面的信息,经过我一周的折腾和采坑,发现目前有以下几种方法
- 通过 PackageManager 系统API读取
- 通过开源框架 AXmlResourceParser 来解析二进制的 AndroidManifest
- 通过Gradle脚本在处理 AndroidManifest.xml 的时候拷贝一份到 Assets 目录,然后解析 AndroidManifest
- 通过反射隐藏的系统API PackageParser 的 parsePackage 方法来直接获取解析的结果
- 通过反射 AssetManager 的一个私有方法获取二进制XML解析器来解析二进制的 AndroidManifest
接下来我会慢慢分享我这一次的采坑经历
2. 通过 PackageManager 的方式
首先拿到这个需求,我第一反应就是通过 PackageManager 来获取,主要有两种方式来获取
- 通过 getPackageInfo 方法来获取,想要什么数据,用传递不同的 FLAG,能获取到的数据受限于 FLAG 的个数
- 通过 getApplicationInfo、getActivityInfo 等方式获取,能获取到的数据受限于 get***Info 方法的个数
private void readPackageInfo() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
Log.d(ManifestParser.class.getSimpleName(), packageInfo.activities.toString());
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
private void readApplication() {
try {
ApplicationInfo appInfo = this.getPackageManager()
.getApplicationInfo(getPackageName(),
PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
private void readActivity() {
ActivityInfo info;
try {
info = this.getPackageManager().getActivityInfo(getComponentName(),
PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
private void readService() {
try {
ComponentName cn = new ComponentName(this, DemoService.class);
ServiceInfo info = this.getPackageManager().getServiceInfo(cn,
PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
接着分析一下通过 PackageManager 这种方式的优缺点
首先是优点:
- 是系统API,安全可靠
- 不会有版本兼容问题,不会有解析问题
然后是缺点:
- 能获取到的数据只是google希望我们能查看到的数据,有一些数据获取不到
- 使用起来较为繁琐,特别是数据需要组合的情况下,需要多次调用 getPackageInfo 方法来获取
我当然不会因为这个就止步于此,PackageManager 的两个缺点就无法满足项目的需求,接着我开始把眼光放在二进制的AndroidManifest
3. 通过开源框架 AXmlResourceParser 来解析二进制的 AndroidManifest
首先,我们知道可以在代码中获取到本APK的路径
getApplicationInfo().sourceDir
然后我们就可以直接获取到本APK中 AndroidManifest 的 InputStream
private static InputStream getBinaryManifestInputStream(Context context) {
if (context == null) {
return null;
}
ApplicationInfo info = context.getApplicationInfo();
String source = info.sourceDir;
try {
JarFile jarFile = new JarFile(source);
Enumeration> entries = jarFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = ((ZipEntry) entries.nextElement());
String entryName = entry.getName();
if (entryName.equals("AndroidManifest.xml")) {
return jarFile.getInputStream(entry);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
获取到 InputStream 后,就可以用 PULL 等方式解析 XML 了,不熟悉 PULL 的小伙伴自行百度,这个是 Android 推荐的 XML 解析方式
public static ManifestInfo parseManifestInfo(Context context) {
if (context == null) {
return null;
}
ManifestInfo manifestInfo = new ManifestInfo(true);
try {
InputStream in = getBinaryManifestInputStream(context);
if (in == null) {
return null;
}
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(in, "UTF-8");
int eventType = XmlPullParser.START_DOCUMENT;
do {
eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
manifestInfo.startParse(parser);
break;
case XmlPullParser.END_TAG:
manifestInfo.stopParse(parser);
break;
default:
break;
}
} while (eventType != (XmlPullParser.END_DOCUMENT));
} catch (XmlPullParserException | IOException | XMLParseException e) {
e.printStackTrace();
}
return manifestInfo;
}
开始解析,结果解析失败,查看错误日志发现 AndroidManifest.xml 全是乱码。原来 Android 在打包 APK 的时候,会对非 Assets 目录 下的 xml 进行二次编码。
可以查看这篇博客了解AndroidManifest的二次编码规则以及解析步骤
后来我在网上搜索到有一个大牛写了一个开源的解析二进制 XML 的解析器,名字叫 AXMLPrinter.jar。这个解析器可以直接用 Java 的方式运行。
jar包及源码下载地址
java -jar AXMLPrinter.jar AndroidManifest.xml > log.xml
当然我肯定是把 jar 包引入到工程中,用他demo里面的方法进行解析。
发现 AXmlResourceParser 解析器是实现了 XmlResourceParser 接口,而 XmlResourceParser 接口又是继承的 XmlPullParser 接口,因此使用方法和 PULL 几乎一模一样。有一点点的不同就是如果使用 setInput 方法会直接抛出异常,需要用 open 方法来取代。
public static ManifestInfo parseBinaryManifestInfo(Context context) {
if (context == null) {
return null;
}
InputStream in = getBinaryManifestInputStream(context);
if (in == null) {
return null;
}
AXmlResourceParser parser = new AXmlResourceParser();
parser.open(in);
ManifestInfo manifestInfo = new ManifestInfo(false);
try {
int eventType = XmlPullParser.START_DOCUMENT;
do {
eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
manifestInfo.startParse(parser);
break;
case XmlPullParser.END_TAG:
manifestInfo.stopParse(parser);
break;
default:
break;
}
} while (eventType != (XmlPullParser.END_DOCUMENT));
} catch (XmlPullParserException | XMLParseException | IOException e) {
e.printStackTrace();
}
return manifestInfo;
}
在我的 Demo 工程一跑,失败了,错误日志如下
AndroidRuntime: java.lang.IllegalAccessError: tried to access class android.content.res.StringBlock from class android.content.res.AXmlResourceParser
这蛋疼的日志也看不出来什么,搜遍了 stackoverflow、百度、google 也没搜出来有用的信息,甚至提问的人都没有。我把 AXmlResourceParser 和 StringBlock 的源码都看了一遍,也没什么不对劲。
正当我烦恼时,无意间看见了StringBlock的包名,顿时心中明了了。StringBlock的包名居然是 android 开头的。我马上在项目中全局搜索 StringBlock,果然,搜出来两个 StringBlock,除了我刚才引入的,android 系统API 也有一个 StringBlock,并且这两个的包名还一样的,只是内容不一样。然后我发现开源包里的很多类,系统都有了。
我想作者能写出来这种开源框架,不至于犯这种错误吧。网上搜了作者这个开源库的时间发现是2008年写的,也许那个时候 Android 并没有把这些类集成到系统中吧,所以才在jar包中引入了。
既然知道原因了,那就好办了,既然源码到手,直接把系统已经有的类去掉,然后换个包名就行了。这里要注意,有些类文件比如 StringBlock 的内容和系统的 StringBlock 内容不一样,这里只能换个包名而不能删了用系统的,否则会编译报错。
包名换完后,再次在我的 Demo 工程跑一下,终于成功了,成功解析出来了。我高高兴兴的集成到项目工程,结果一泼冷水就过来了,项目工程解析异常,异常日志如下:
java.lang.ArrayIndexOutOfBoundsException: 1777
at android.content.res.StringBlock.getShort(StringBlock.java:231)
at android.content.res.StringBlock.getString(StringBlock.java:91)
at android.content.res.AXmlResourceParser.getName(AXmlResourceParser.java:140)
at test.AXMLPrinter.main(AXMLPrinter.java:56)
同样在 stackoverflow、百度、google 搜索未果,debug 跟踪了一下感觉解析的步骤不正确,应该是 Android 后来在较高版本调整了 AndroidManifest 的二次编码规则吧,这个开源框架2008年就停止维护了。没办法,只有放弃这个方法了。
4. 通过Gradle脚本拷贝 AndroidManifest 到 Assets 目录,然后解析 AndroidManifest
经过上面的步骤,我发现想要解析二进制的 AndroidManifest 不是一件轻松的事,因此就想想能不能解析未二次编码的原味的 AndroidManifest呢
突然想到之前有个需求将台湾资源的strings.xml 拷贝一份到 香港资源目录,现在要拷贝的是 AndroidManifest,有异曲同工之妙啊。
查阅了一些资料发现,Gradle 在构建 APK 的时候,会在 processManifest 这个 Task 合并所有 Module 的 AndroidManifest,那我不就可以在这个 Task 后加一个 Action,把合并后的 AndroidManifest 拷贝到一个目录,然后就可以直接解析了吗?拷贝的目录当然是选择 assets 啦,因为 assets 目录下的文件会原封不动的打进 APK包,不会生成 id 也不会二次编码。不熟悉的小朋友记得先回去补补功课哦。不太熟悉 Gradle 的也只有自行查阅资料了,毕竟这不是本篇文章的重点。下面直接给出Gradle拷贝的Task
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
String fileName = "AndroidManifest.xml"
File manifestFile = new File(output.processManifest.manifestOutputDirectory, fileName)
if (!manifestFile.exists()) {
new IllegalArgumentException("AndroidManifest File is not exist :" + manifestFile);
}
File outDir = new File(project.projectDir, "src/main/assets")
if (!outDir.exists()) {
outDir.mkdirs();
}
File outFile = new File(outDir, fileName);
if (outFile.exists()) {
println "AndroidManifest File in Assets is Exist, Now Delete it"
outFile.deleteOnExit()
}
println "AndroidManifest Src File is " + manifestFile.getAbsolutePath()
println "AndroidManifest Dest Dir is " + outDir.getAbsolutePath()
copy {
from(manifestFile)
into(outDir)
}
println "AndroidManifest File Copy Success"
}
}
}
Sync 一下,发现 assets 目录下是不是就多了 AndroidManifest.xml 文件啦,接下来就是常规的 XML 解析了。美滋滋
然后我就开始一步一步解析,先解析 manifest 标签、然后 uses-permission 标签、然后 application 标签、然后 Activity 标签......
写着写着我就发现不对劲,这样写下去要写到啥时候,AndroidManifest 可配置的标签那么多,难道我都要挨着挨着写吗?后续如果要新增标签或者属性,我还要索引半天找到文件?这要的个鬼
于是我开始考虑写一个通用的 XML 解析工具。起初我想写一个类似 Gson 的将 XML 和 JavaBean 用泛型互相转换的工具,网上搜了一下,已经有一个成熟的开源的泛型解析工具了,有兴趣可以看看。
xstream-xml泛型解析
后来我发现行不通。因为 Application 标签下 有 Activity、Service、Receiver等多个标签,JavaBean 的 List可不允许多泛型,普通对象的又不允许动态添加泛型类型。思来想去无果,只好放弃。如果有哪位大神有解决办法,可以告诉我。
最后,我想到一个通用解析方式的工具,既然 PULL 是标签触发的方式解析的,那我也可以采用递归的方式,让自己触发或者自己的子标签触发解析操作。并且可以通过配置化的方式来决定是否解析某些标签。觉得这个方案可行,就开始着手,虽然看起来很简单的需求,实现出来也只有一个类,300行代码左右,但是还是经历了一些坑和一些困难,所幸最后我都一一克服写了出来了
/**
* 通用XML解析器
*/
public class XMLParser {
private static final String SEPARATOR = "#";
private static final String ENCODING = "UTF-8";
private static final String REFLECT_METHOD = "addAssetPath";
// 当前解析路径
private static final StringBuilder parsePath = new StringBuilder();
// 是否解析namespace
private static final boolean needParseNameSpace = false;
// 自己的标签名
private String tagName;
// 是否已经解析完毕
private boolean isParseComplete;
// 路径
private String path;
// 层级
private int level = 1;
// 属性键值对
private Map attributeMap = new HashMap<>();
// 子节点键值对
private Map> sonTagMap = new HashMap<>();
/**
* 通用解析XML方法,可以解析所有的xml结构,需要在方法调用之前正确设置{@link #register(String)}
*
* @param context
* @param xmlParser 在外界注册好后传进来
* @param in xml的输入流
* @return 返回已经解析完的结果
* @throws XmlPullParserException
* @throws IOException
*/
public static synchronized XMLParser parse(Context context, XMLParser xmlParser, InputStream in)
throws XmlPullParserException, IOException {
if (context == null || xmlParser == null || in == null) {
return null;
}
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(in, ENCODING);
return parse(context, xmlParser, parser);
}
/**
* xml解析模板方法
*/
public static synchronized XMLParser parse(Context context, XMLParser xmlParser, XmlPullParser parser)
throws XmlPullParserException, IOException {
if (context == null || xmlParser == null || parser == null) {
return null;
}
int eventType = XmlPullParser.START_DOCUMENT;
do {
eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
xmlParser.startParse(parser);
break;
case XmlPullParser.END_TAG:
xmlParser.stopParse(parser);
break;
default:
break;
}
} while (eventType != (XmlPullParser.END_DOCUMENT));
return xmlParser;
}
/**
* 在解析之前,需要先注册需要解析的标签,注册采用链式注册的方式,用{@link #SEPARATOR} 来隔开父与子的标签,比如解析AndroidManifest时:
* XMLParser manifestInfo = new XMLParser();
* manifestInfo.register("manifest#uses-sdk");
* manifestInfo.register("manifest#instrumentation");
* manifestInfo.register("manifest#uses-permission");
* manifestInfo.register("manifest#supports-screens");
* manifestInfo.register("manifest#application#uses-library");
* manifestInfo.register("manifest#application#meta-data");
* manifestInfo.register("manifest#application#activity#intent-filter#data");
* manifestInfo.register("manifest#application#activity#intent-filter#action");
* manifestInfo.register("manifest#application#activity#intent-filter#category");
* manifestInfo.register("manifest#application#activity#meta-data");
* manifestInfo.register("manifest#application#receiver#intent-filter#action");
* manifestInfo.register("manifest#application#receiver#meta-data");
* manifestInfo.register("manifest#application#provider#intent-filter#action");
* manifestInfo.register("manifest#application#provider#meta-data");
* manifestInfo.register("manifest#application#service#intent-filter#action");
* manifestInfo.register("manifest#application#service#meta-data");
*/
public void register(String action) {
if (TextUtils.isEmpty(action)) {
return;
}
// 没有分隔符,只是赋值自己的tagName
if (!action.contains(SEPARATOR)) {
path = action;
tagName = action;
return;
}
String[] tagNames = action.split(SEPARATOR);
// 依然没有分隔符
if (tagNames.length < 2) {
path = tagNames[0];
tagName = tagNames[0];
return;
}
// 赋值tagName和path
tagName = tagNames[level - 1];
StringBuilder pathBuilder = new StringBuilder();
for (int i = 0; i < level; i++) {
pathBuilder.append(tagNames[i]).append(SEPARATOR);
}
pathBuilder.setLength(pathBuilder.length() - SEPARATOR.length());
path = pathBuilder.toString();
// 如果没有子节点,就返回
if (level >= tagNames.length) {
return;
}
// 添加子节点,并预置一个解析对象,递归调用本方法注册子标签
if (!sonTagMap.containsKey(tagNames[level])) {
List sonTags = new ArrayList<>();
sonTagMap.put(tagNames[level], sonTags);
XMLParser son = new XMLParser();
son.level = level + 1;
son.register(action);
sonTags.add(son);
} else {
List sonTags = sonTagMap.get(tagNames[level]);
XMLParser son = sonTags.get(0);
son.register(action);
}
}
/**
* 递归解析开始标签,内部完成attribute属性的解析和子标签的解析
*/
private void startParse(XmlPullParser parser) throws XmlPullParserException {
String parseTagName = parser.getName();
if (TextUtils.isEmpty(tagName) || TextUtils.isEmpty(parseTagName)) {
throw new XmlPullParserException("tagName is Empty");
}
// 设置当前解析路径,用于找到具体的解析器解析
if (parsePath.length() == 0) {
parsePath.append(parseTagName);
} else if (!parsePath.toString().endsWith(parseTagName)) {
parsePath.append(SEPARATOR).append(parseTagName);
}
// 首先解析自己的键值对
if (tagName.equals(parseTagName)) {
parseAttribute(parser);
} else {
parseSonTag(parser, true);
}
}
/**
* 解析自己的attribute属性
*/
private void parseAttribute(XmlPullParser parser) throws XmlPullParserException {
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attributeNamespace = parser.getAttributeNamespace(i);
String attributeName = parser.getAttributeName(i);
String attributeValue = parser.getAttributeValue(i);
if (TextUtils.isEmpty(attributeName)) {
throw new XmlPullParserException("attributeName is null");
}
if (TextUtils.isEmpty(attributeValue)) {
continue;
}
if (needParseNameSpace) {
String key = TextUtils.isEmpty(attributeNamespace)
? attributeName
: attributeNamespace + ":" + attributeName;
attributeMap.put(key, attributeValue);
} else {
attributeMap.put(attributeName, attributeValue);
}
}
}
/**
* 解析子标签
*/
private void parseSonTag(XmlPullParser parser, boolean isStartTag) throws XmlPullParserException {
// 首先匹配当前tagName
String[] parseTags = parsePath.toString().split(SEPARATOR);
if (parseTags.length < level) {
return;
}
// 当前tag是否与路径匹配
if (!tagName.equals(parseTags[level - 1])) {
return;
}
// 查看子类
if (parseTags.length < level + 1) {
return;
}
String sonTag = parseTags[level];
List sonTags = null;
if (!sonTagMap.containsKey(sonTag)) {
sonTags = new ArrayList<>();
sonTagMap.put(sonTag, sonTags);
} else {
sonTags = sonTagMap.get(sonTag);
}
if (sonTags.isEmpty()) {
if (isStartTag) {
XMLParser son = new XMLParser();
son.level = level + 1;
son.register(path + SEPARATOR + sonTag);
son.startParse(parser);
sonTags.add(son);
}
return;
}
// 取出上一个未完成的
XMLParser lastSon = sonTags.get(sonTags.size() - 1);
if (lastSon.isParseComplete) {
if (isStartTag) {
//没有未完成的,则新建
XMLParser son = new XMLParser();
son.level = level + 1;
son.register(path + SEPARATOR + sonTag);
son.startParse(parser);
sonTags.add(son);
}
} else {
if (isStartTag) {
lastSon.startParse(parser);
} else {
lastSon.stopParse(parser);
}
}
}
/**
* 递归解析结束标签
*/
private void stopParse(XmlPullParser parser) throws XmlPullParserException {
String parseTagName = parser.getName();
if (TextUtils.isEmpty(tagName) || TextUtils.isEmpty(parseTagName)) {
throw new XmlPullParserException("tagName is Empty");
}
if (tagName.equals(parseTagName)) {
isParseComplete = true;
} else {
parseSonTag(parser, false);
}
// 设置解析路径
if (parsePath.toString().endsWith(parseTagName)) {
if (parsePath.lastIndexOf(SEPARATOR) != -1) {
parsePath.setLength(parsePath.lastIndexOf(SEPARATOR));
} else {
parsePath.setLength(0);
}
}
}
@Override
public String toString() {
// 先输出自己的键值对
StringBuilder sb = new StringBuilder();
sb.append(tagName).append(":");
for (Map.Entry entry : attributeMap.entrySet()) {
if (!TextUtils.isEmpty(entry.getKey()) && !TextUtils.isEmpty(entry.getValue())) {
sb.append("[").append(entry.getKey()).append("=").append(entry.getValue()).append("]").append(",");
}
}
sb.setLength(sb.length() - 1);
sb.append("\n");
for (Map.Entry> sonTags : sonTagMap.entrySet()) {
for (XMLParser son : sonTags.getValue()) {
String sonString = son.toString();
if (!TextUtils.isEmpty(sonString)) {
for (int i = 0; i < level; i++) {
sb.append("\t");
}
sb.append(sonString);
}
}
}
if (sb.length() == tagName.length() + "\n".length()) {
return "";
}
return sb.toString();
}
}
使用方法就很简单了,只需要正确配置需要解析的标签,目前暂定的是用 ‘#’ 来分割父标签与子标签:
public static synchronized XMLParser parseManifestInfoByRecursion(Context context) {
if (context == null) {
return null;
}
XMLParser manifestInfo = new XMLParser();
manifestInfo.register("manifest#uses-sdk");
manifestInfo.register("manifest#instrumentation");
manifestInfo.register("manifest#uses-permission");
manifestInfo.register("manifest#supports-screens");
manifestInfo.register("manifest#application#uses-library");
manifestInfo.register("manifest#application#meta-data");
manifestInfo.register("manifest#application#activity#intent-filter#data");
manifestInfo.register("manifest#application#activity#intent-filter#action");
manifestInfo.register("manifest#application#activity#intent-filter#category");
manifestInfo.register("manifest#application#activity#meta-data");
manifestInfo.register("manifest#application#receiver#intent-filter#action");
manifestInfo.register("manifest#application#receiver#meta-data");
manifestInfo.register("manifest#application#provider#intent-filter#action");
manifestInfo.register("manifest#application#provider#meta-data");
manifestInfo.register("manifest#application#service#intent-filter#action");
manifestInfo.register("manifest#application#service#meta-data");
try {
InputStream in = context.getAssets().open("tempxml");
if (in == null) {
return manifestInfo;
}
XMLParser.parse(context, manifestInfo, in);
} catch (Exception e) {
e.printStackTrace();
}
return manifestInfo;
}
5. 通过反射 PackageParser 的 parsePackage 方法来直接获取解析的结果
在完成上述步骤后,我自己也比较满意,提交代码后我发现了还是有一些不足的地方。
- 我只需要解析 AndroidManifest ,却新增了 Gradle 脚本,提高了维护成本
- 同事每次Sync 后,都会在 Assets 目录生成一个未加入版本管理的 AndroidManifest.xml,会造成疑惑
于是我又把重心放在了到底能不能解析二进制的 AndroidManifest上面来。按道理肯定是有办法的,官方肯定也是有隐形支持的,不然系统API为何能解析,只不过没有对外暴露出来而已。
在一次查阅资料中找到了突破口,Android 系统是 通过 PackageParser 类来解析的,但是这个类被隐藏了,所以外界是不能使用的,这个时候当然反射就派上用场了。一顿操作猛如虎后,还是给解析出来了
public static synchronized Object parse(Context context) {
try {
Class clazz = Class.forName("android.content.pm.PackageParser");
Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
if (declaredConstructors == null || declaredConstructors.length == 0) {
return null;
}
Constructor constructor = declaredConstructors[0];
Class[] parameterTypes = constructor.getParameterTypes();
Object packageParser = null;
if (parameterTypes.length == 0) {
packageParser = constructor.newInstance();
} else{
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class paramType = parameterTypes[i];
parameters[i] = paramType.newInstance();
}
packageParser = constructor.newInstance(parameters);
}
Method[] declaredMethods = clazz.getDeclaredMethods();
Method parseBaseApk = null;
for (Method method : declaredMethods) {
if(method.getName().equals("parsePackage")
&& method.getParameterTypes() != null
&& method.getParameterTypes().length == 4
&& method.getParameterTypes()[0].getSimpleName().equals(File.class.getSimpleName())){
parseBaseApk = method;
}
}
if(parseBaseApk == null){
return null;
}
parseBaseApk.setAccessible(true);
Object result = parseBaseApk.invoke(packageParser, new File(context.getApplicationInfo().sourceDir), "AndroidManifest.xml", context.getResources().getDisplayMetrics(), 1);
return result;
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
返回的 Object 就是 PackageParser 的内部类 Package,因为被隐藏了不能直接使用,所以只好用Object来接
然后是解析出来了,但是这种方式的问题太大太大了:
- 这个方法在不同的系统版本,方法名、方法参数都有很大的区别。比如 API26 PackageParser的构造器是无参构造,而 API19 的构造器是有参构造。API26 的方法是 parseBaseApk(File, AssetManager, int),而 API19 的方法是 parsePackage(File, String, DisplayMetrics, int)。这就需要反射时需要针对不同的系统版本而反射不同,并且新的版本出来后还要去适配一下,而这是极不现实的。
- 因为返回结果只能用 Object 来接,所以再获取数据的时候就非常的麻烦。同样只能用反射的方式去读取,这个难度是极大的。
所以这种方式是行不通的,那么就没有办法了吗?当然不是。
6. 通过AssetManager 的私有方法获取二进制XML解析器
上面的 PackageParser 虽然不能直接调用方法,但是肯定是有可以借鉴的地方。于是在查看了 parseBaseApk 方法的源代码后发现,他是通过 AssetManager 来实现的,下面贴出部分源码
final int cookie = loadApkIntoAssetManager(assets, apkPath, flags);
Resources res = null;
XmlResourceParser parser = null;
try {
res = new Resources(assets, mMetrics, null);
parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
final String[] outError = new String[1];
final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError);
......
return pkg;
} catch (PackageParserException e) {
throw e;
} catch (Exception e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed to read manifest from " + apkPath, e);
} finally {
IoUtils.closeQuietly(parser);
}
可以看出来,首先是用 loadApkIntoAssetManager 方法将 APK的路径转换为了类型为 int 的 cookie,然后调用 AssetManager 的 openXmlResourceParser 方法取到了 XmlResourceParser 。查看注释发现返回的 XmlResourceParser 就是用来解析编译后的 AndroidManifest,openXmlResourceParser 的源码如下
/**
* Retrieve a parser for a compiled XML file.
*
* @param cookie Identifier of the package to be opened.
* @param fileName The name of the file to retrieve.
*/
public final XmlResourceParser openXmlResourceParser(int cookie,
String fileName) throws IOException {
XmlBlock block = openXmlBlockAsset(cookie, fileName);
XmlResourceParser rp = block.newParser();
block.close();
return rp;
}
然后我们来看看最开始的 loadApkIntoAssetManager 是怎么回事儿
private static int loadApkIntoAssetManager(AssetManager assets, String apkPath, int flags)
throws PackageParserException {
if ((flags & PARSE_MUST_BE_APK) != 0 && !isApkPath(apkPath)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,
"Invalid package file: " + apkPath);
}
// The AssetManager guarantees uniqueness for asset paths, so if this asset path
// already exists in the AssetManager, addAssetPath will only return the cookie
// assigned to it.
int cookie = assets.addAssetPath(apkPath);
if (cookie == 0) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Failed adding asset path: " + apkPath);
}
return cookie;
}
原来也是通过 AssetManager 的 addAssetPath 方法来将 APK 的 path 转换为 cookie 的。这下子我们就清楚了获取解析编译后的解析器的步骤了:
- 首先通过 AssetManager 的 addAssetPath 方法获取 cookie,注意这个方法是隐藏的,所以需要通过反射来调用
- 然后通过 AssetManager 的 openXmlResourceParser 方法,传入 cookie, 返回解析器
一切都明朗了,赶紧把代码写出来
private static XmlResourceParser getBinaryXmlParser(Context context, String binaryFilePath, String binaryXmlFileName)
throws ReflectiveOperationException, IOException {
if (TextUtils.isEmpty(binaryFilePath) || TextUtils.isEmpty(binaryXmlFileName)) {
return null;
}
AssetManager assetManager = context.getAssets();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
int cookie = (int) addAssetPath.invoke(assetManager, binaryFilePath);
return assetManager.openXmlResourceParser(cookie, binaryXmlFileName);
}
因为 XmlResourceParser 是继承的 XmlPullParser,所以接下来就是普通的 PULL 解析了。
7. 总结
这篇文章总结了我这次采坑的几次经历,也挖掘了 Android 想要读取 AndroidManifest 的几种方式。看似简单的文章,其实其中踩了非常多的坑,遇到了非常多的困难。不过最后我想说的也是这次印象最深的是:在即将放弃的时候冷静下来,也许会找到不一样的解决之道