Android:解析AndroidManifest的踩坑經(jīng)歷

1. 前言

這次項目接到一個需求,需要盡可能多的展示AndroidManifest.xml 里面的信息娇妓,經(jīng)過我一周的折騰和采坑伐庭,發(fā)現(xiàn)目前有以下幾種方法

  1. 通過 PackageManager 系統(tǒng)API讀取
  2. 通過開源框架 AXmlResourceParser 來解析二進制的 AndroidManifest
  3. 通過Gradle腳本在處理 AndroidManifest.xml 的時候拷貝一份到 Assets 目錄用爪,然后解析 AndroidManifest
  4. 通過反射隱藏的系統(tǒng)API PackageParser 的 parsePackage 方法來直接獲取解析的結(jié)果
  5. 通過反射 AssetManager 的一個私有方法獲取二進制XML解析器來解析二進制的 AndroidManifest

接下來我會慢慢分享我這一次的采坑經(jīng)歷

2. 通過 PackageManager 的方式

首先拿到這個需求原押,我第一反應(yīng)就是通過 PackageManager 來獲取胁镐,主要有兩種方式來獲取

  1. 通過 getPackageInfo 方法來獲取偎血,想要什么數(shù)據(jù)诸衔,用傳遞不同的 FLAG,能獲取到的數(shù)據(jù)受限于 FLAG 的個數(shù)
  2. 通過 getApplicationInfo颇玷、getActivityInfo 等方式獲取笨农,能獲取到的數(shù)據(jù)受限于 get***Info 方法的個數(shù)
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 這種方式的優(yōu)缺點

首先是優(yōu)點:

  1. 是系統(tǒng)API,安全可靠
  2. 不會有版本兼容問題帖渠,不會有解析問題

然后是缺點:

  1. 能獲取到的數(shù)據(jù)只是google希望我們能查看到的數(shù)據(jù)谒亦,有一些數(shù)據(jù)獲取不到
  2. 使用起來較為繁瑣,特別是數(shù)據(jù)需要組合的情況下空郊,需要多次調(diào)用 getPackageInfo 方法來獲取

我當(dāng)然不會因為這個就止步于此份招,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;
}

開始解析谐腰,結(jié)果解析失敗,查看錯誤日志發(fā)現(xiàn) AndroidManifest.xml 全是亂碼涩盾。原來 Android 在打包 APK 的時候十气,會對非 Assets 目錄 下的 xml 進行二次編碼。

可以查看這篇博客了解AndroidManifest的二次編碼規(guī)則以及解析步驟

后來我在網(wǎng)上搜索到有一個大牛寫了一個開源的解析二進制 XML 的解析器春霍,名字叫 AXMLPrinter.jar砸西。這個解析器可以直接用 Java 的方式運行。

jar包及源碼下載地址

java -jar AXMLPrinter.jar AndroidManifest.xml > log.xml

當(dāng)然我肯定是把 jar 包引入到工程中址儒,用他demo里面的方法進行解析籍胯。

發(fā)現(xiàn) AXmlResourceParser 解析器是實現(xiàn)了 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 的源碼都看了一遍,也沒什么不對勁彼念。

正當(dāng)我煩惱時挪圾,無意間看見了StringBlock的包名浅萧,頓時心中明了了。StringBlock的包名居然是 android 開頭的哲思。我馬上在項目中全局搜索 StringBlock洼畅,果然,搜出來兩個 StringBlock棚赔,除了我剛才引入的帝簇,android 系統(tǒng)API 也有一個 StringBlock,并且這兩個的包名還一樣的靠益,只是內(nèi)容不一樣丧肴。然后我發(fā)現(xiàn)開源包里的很多類,系統(tǒng)都有了胧后。

我想作者能寫出來這種開源框架闪湾,不至于犯這種錯誤吧。網(wǎng)上搜了作者這個開源庫的時間發(fā)現(xiàn)是2008年寫的绩卤,也許那個時候 Android 并沒有把這些類集成到系統(tǒng)中吧途样,所以才在jar包中引入了。

既然知道原因了濒憋,那就好辦了何暇,既然源碼到手,直接把系統(tǒng)已經(jīng)有的類去掉凛驮,然后換個包名就行了裆站。這里要注意,有些類文件比如 StringBlock 的內(nèi)容和系統(tǒng)的 StringBlock 內(nèi)容不一樣黔夭,這里只能換個包名而不能刪了用系統(tǒng)的宏胯,否則會編譯報錯。

包名換完后本姥,再次在我的 Demo 工程跑一下肩袍,終于成功了,成功解析出來了婚惫。我高高興興的集成到項目工程氛赐,結(jié)果一潑冷水就過來了,項目工程解析異常先舷,異常日志如下:

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 跟蹤了一下感覺解析的步驟不正確牲芋,應(yīng)該是 Android 后來在較高版本調(diào)整了 AndroidManifest 的二次編碼規(guī)則吧,這個開源框架2008年就停止維護了。沒辦法缸浦,只有放棄這個方法了夕冲。

4. 通過Gradle腳本拷貝 AndroidManifest 到 Assets 目錄,然后解析 AndroidManifest

經(jīng)過上面的步驟餐济,我發(fā)現(xiàn)想要解析二進制的 AndroidManifest 不是一件輕松的事,因此就想想能不能解析未二次編碼的原味的 AndroidManifest呢

突然想到之前有個需求將臺灣資源的strings.xml 拷貝一份到 香港資源目錄胆剧,現(xiàn)在要拷貝的是 AndroidManifest絮姆,有異曲同工之妙啊。

查閱了一些資料發(fā)現(xiàn)秩霍,Gradle 在構(gòu)建 APK 的時候篙悯,會在 processManifest 這個 Task 合并所有 Module 的 AndroidManifest,那我不就可以在這個 Task 后加一個 Action铃绒,把合并后的 AndroidManifest 拷貝到一個目錄鸽照,然后就可以直接解析了嗎?拷貝的目錄當(dāng)然是選擇 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 一下灾票,發(fā)現(xiàn) assets 目錄下是不是就多了 AndroidManifest.xml 文件啦峡谊,接下來就是常規(guī)的 XML 解析了。美滋滋

然后我就開始一步一步解析刊苍,先解析 manifest 標簽既们、然后 uses-permission 標簽、然后 application 標簽正什、然后 Activity 標簽......

寫著寫著我就發(fā)現(xiàn)不對勁啥纸,這樣寫下去要寫到啥時候,AndroidManifest 可配置的標簽?zāi)敲炊嘤さy道我都要挨著挨著寫嗎脾拆?后續(xù)如果要新增標簽或者屬性址晕,我還要索引半天找到文件观腊?這要的個鬼

于是我開始考慮寫一個通用的 XML 解析工具。起初我想寫一個類似 Gson 的將 XML 和 JavaBean 用泛型互相轉(zhuǎn)換的工具泡垃,網(wǎng)上搜了一下旨怠,已經(jīng)有一個成熟的開源的泛型解析工具了渠驼,有興趣可以看看。

xstream-xml泛型解析

后來我發(fā)現(xiàn)行不通鉴腻。因為 Application 標簽下 有 Activity迷扇、Service百揭、Receiver等多個標簽,JavaBean 的 List可不允許多泛型蜓席,普通對象的又不允許動態(tài)添加泛型類型器一。思來想去無果,只好放棄厨内。如果有哪位大神有解決辦法祈秕,可以告訴我。

最后雏胃,我想到一個通用解析方式的工具请毛,既然 PULL 是標簽觸發(fā)的方式解析的,那我也可以采用遞歸的方式瞭亮,讓自己觸發(fā)或者自己的子標簽觸發(fā)解析操作方仿。并且可以通過配置化的方式來決定是否解析某些標簽。覺得這個方案可行统翩,就開始著手仙蚜,雖然看起來很簡單的需求,實現(xiàn)出來也只有一個類厂汗,300行代碼左右鳍征,但是還是經(jīng)歷了一些坑和一些困難,所幸最后我都一一克服寫了出來了

/**
 * 通用XML解析器
 */
public class XMLParser {

    private static final String SEPARATOR = "#";
    private static final String ENCODING = "UTF-8";
    private static final String REFLECT_METHOD = "addAssetPath";

    // 當(dāng)前解析路徑
    private static final StringBuilder parsePath = new StringBuilder();
    // 是否解析namespace
    private static final boolean needParseNameSpace = false;

    // 自己的標簽名
    private String tagName;
    // 是否已經(jīng)解析完畢
    private boolean isParseComplete;
    // 路徑
    private String path;
    // 層級
    private int level = 1;
    // 屬性鍵值對
    private Map<String, String> attributeMap = new HashMap<>();
    // 子節(jié)點鍵值對
    private Map<String, List<XMLParser>> sonTagMap = new HashMap<>();

    /**
     * 通用解析XML方法面徽,可以解析所有的xml結(jié)構(gòu)艳丛,需要在方法調(diào)用之前正確設(shè)置{@link #register(String)}
     *
     * @param context
     * @param xmlParser 在外界注冊好后傳進來
     * @param in        xml的輸入流
     * @return 返回已經(jīng)解析完的結(jié)果
     * @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();
        // 如果沒有子節(jié)點戴差,就返回
        if (level >= tagNames.length) {
            return;
        }
        // 添加子節(jié)點,并預(yù)置一個解析對象铛嘱,遞歸調(diào)用本方法注冊子標簽
        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);
        }

    }

    /**
     * 遞歸解析開始標簽暖释,內(nèi)部完成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");
        }
        // 設(shè)置當(dāng)前解析路徑,用于找到具體的解析器解析
        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 {
        // 首先匹配當(dāng)前tagName
        String[] parseTags = parsePath.toString().split(SEPARATOR);
        if (parseTags.length < level) {
            return;
        }
        // 當(dāng)前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);
            }
        }
    }

    /**
     * 遞歸解析結(jié)束標簽
     */
    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);
        }
        // 設(shè)置解析路徑
        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 方法來直接獲取解析的結(jié)果

在完成上述步驟后帖烘,我自己也比較滿意亮曹,提交代碼后我發(fā)現(xiàn)了還是有一些不足的地方。

  1. 我只需要解析 AndroidManifest ,卻新增了 Gradle 腳本照卦,提高了維護成本
  2. 同事每次Sync 后式矫,都會在 Assets 目錄生成一個未加入版本管理的 AndroidManifest.xml,會造成疑惑

于是我又把重心放在了到底能不能解析二進制的 AndroidManifest上面來役耕。按道理肯定是有辦法的采转,官方肯定也是有隱形支持的,不然系統(tǒng)API為何能解析瞬痘,只不過沒有對外暴露出來而已故慈。

在一次查閱資料中找到了突破口,Android 系統(tǒng)是 通過 PackageParser 類來解析的图云,但是這個類被隱藏了惯悠,所以外界是不能使用的邻邮,這個時候當(dāng)然反射就派上用場了竣况。一頓操作猛如虎后,還是給解析出來了

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 的內(nèi)部類 Package筒严,因為被隱藏了不能直接使用丹泉,所以只好用Object來接

然后是解析出來了,但是這種方式的問題太大太大了:

  1. 這個方法在不同的系統(tǒng)版本鸭蛙,方法名摹恨、方法參數(shù)都有很大的區(qū)別。比如 API26 PackageParser的構(gòu)造器是無參構(gòu)造娶视,而 API19 的構(gòu)造器是有參構(gòu)造晒哄。API26 的方法是 parseBaseApk(File, AssetManager, int),而 API19 的方法是 parsePackage(File, String, DisplayMetrics, int)肪获。這就需要反射時需要針對不同的系統(tǒng)版本而反射不同寝凌,并且新的版本出來后還要去適配一下,而這是極不現(xiàn)實的孝赫。
  2. 因為返回結(jié)果只能用 Object 來接较木,所以再獲取數(shù)據(jù)的時候就非常的麻煩。同樣只能用反射的方式去讀取青柄,這個難度是極大的伐债。

所以這種方式是行不通的,那么就沒有辦法了嗎致开?當(dāng)然不是峰锁。

6. 通過AssetManager 的私有方法獲取二進制XML解析器

上面的 PackageParser 雖然不能直接調(diào)用方法,但是肯定是有可以借鑒的地方双戳。于是在查看了 parseBaseApk 方法的源代碼后發(fā)現(xiàn)祖今,他是通過 AssetManager 來實現(xiàn)的,下面貼出部分源碼

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的路徑轉(zhuǎn)換為了類型為 int 的 cookie千诬,然后調(diào)用 AssetManager 的 openXmlResourceParser 方法取到了 XmlResourceParser 耍目。查看注釋發(fā)現(xiàn)返回的 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 轉(zhuǎn)換為 cookie 的徐绑。這下子我們就清楚了獲取解析編譯后的解析器的步驟了:

  1. 首先通過 AssetManager 的 addAssetPath 方法獲取 cookie邪驮,注意這個方法是隱藏的,所以需要通過反射來調(diào)用
  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. 總結(jié)

這篇文章總結(jié)了我這次采坑的幾次經(jīng)歷,也挖掘了 Android 想要讀取 AndroidManifest 的幾種方式草巡∈匚兀看似簡單的文章,其實其中踩了非常多的坑山憨,遇到了非常多的困難查乒。不過最后我想說的也是這次印象最深的是:在即將放棄的時候冷靜下來,也許會找到不一樣的解決之道

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末郁竟,一起剝皮案震驚了整個濱河市玛迄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棚亩,老刑警劉巖蓖议,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異讥蟆,居然都是意外死亡勒虾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門攻询,熙熙樓的掌柜王于貴愁眉苦臉地迎上來从撼,“玉大人,你說我怎么就攤上這事钧栖〉土悖” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵拯杠,是天一觀的道長掏婶。 經(jīng)常有香客問我,道長潭陪,這世上最難降的妖魔是什么雄妥? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任最蕾,我火速辦了婚禮,結(jié)果婚禮上老厌,老公的妹妹穿的比我還像新娘瘟则。我一直安慰自己,他們只是感情好枝秤,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布醋拧。 她就那樣靜靜地躺著,像睡著了一般淀弹。 火紅的嫁衣襯著肌膚如雪丹壕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天薇溃,我揣著相機與錄音菌赖,去河邊找鬼。 笑死沐序,一個胖子當(dāng)著我的面吹牛琉用,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播薄啥,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辕羽,長吁一口氣:“原來是場噩夢啊……” “哼逛尚!你這毒婦竟也來了垄惧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绰寞,失蹤者是張志新(化名)和其女友劉穎到逊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滤钱,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡觉壶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了件缸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铜靶。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖他炊,靈堂內(nèi)的尸體忽然破棺而出争剿,到底是詐尸還是另有隱情,我是刑警寧澤痊末,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布蚕苇,位于F島的核電站,受9級特大地震影響凿叠,放射性物質(zhì)發(fā)生泄漏涩笤。R本人自食惡果不足惜嚼吞,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蹬碧。 院中可真熱鬧舱禽,春花似錦、人聲如沸恩沽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽飒筑。三九已至片吊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間协屡,已是汗流浹背俏脊。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肤晓,地道東北人爷贫。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像补憾,于是被迫代替她去往敵國和親漫萄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容