Android:解析AndroidManifest的踩坑经历

1. 前言

这次项目接到一个需求,需要尽可能多的展示AndroidManifest.xml 里面的信息,经过我一周的折腾和采坑,发现目前有以下几种方法

  1. 通过 PackageManager 系统API读取
  2. 通过开源框架 AXmlResourceParser 来解析二进制的 AndroidManifest
  3. 通过Gradle脚本在处理 AndroidManifest.xml 的时候拷贝一份到 Assets 目录,然后解析 AndroidManifest
  4. 通过反射隐藏的系统API PackageParser 的 parsePackage 方法来直接获取解析的结果
  5. 通过反射 AssetManager 的一个私有方法获取二进制XML解析器来解析二进制的 AndroidManifest

接下来我会慢慢分享我这一次的采坑经历

2. 通过 PackageManager 的方式

首先拿到这个需求,我第一反应就是通过 PackageManager 来获取,主要有两种方式来获取

  1. 通过 getPackageInfo 方法来获取,想要什么数据,用传递不同的 FLAG,能获取到的数据受限于 FLAG 的个数
  2. 通过 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 这种方式的优缺点

首先是优点:

  1. 是系统API,安全可靠
  2. 不会有版本兼容问题,不会有解析问题

然后是缺点:

  1. 能获取到的数据只是google希望我们能查看到的数据,有一些数据获取不到
  2. 使用起来较为繁琐,特别是数据需要组合的情况下,需要多次调用 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<String, String> attributeMap = new HashMap<>();
    // 子节点键值对
    private Map<String, List<XMLParser>> 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<XMLParser> sonTags = new ArrayList<>();
            sonTagMap.put(tagNames[level], sonTags);
            XMLParser son = new XMLParser();
            son.level = level + 1;
            son.register(action);
            sonTags.add(son);
        } else {
            List<XMLParser> 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<XMLParser> 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<String, String> 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<String, List<XMLParser>> 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 方法来直接获取解析的结果

在完成上述步骤后,我自己也比较满意,提交代码后我发现了还是有一些不足的地方。

  1. 我只需要解析 AndroidManifest ,却新增了 Gradle 脚本,提高了维护成本
  2. 同事每次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来接

然后是解析出来了,但是这种方式的问题太大太大了:

  1. 这个方法在不同的系统版本,方法名、方法参数都有很大的区别。比如 API26 PackageParser的构造器是无参构造,而 API19 的构造器是有参构造。API26 的方法是 parseBaseApk(File, AssetManager, int),而 API19 的方法是 parsePackage(File, String, DisplayMetrics, int)。这就需要反射时需要针对不同的系统版本而反射不同,并且新的版本出来后还要去适配一下,而这是极不现实的。
  2. 因为返回结果只能用 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 的。这下子我们就清楚了获取解析编译后的解析器的步骤了:

  1. 首先通过 AssetManager 的 addAssetPath 方法获取 cookie,注意这个方法是隐藏的,所以需要通过反射来调用
  2. 然后通过 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 的几种方式。看似简单的文章,其实其中踩了非常多的坑,遇到了非常多的困难。不过最后我想说的也是这次印象最深的是:在即将放弃的时候冷静下来,也许会找到不一样的解决之道

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容