1. 前言
這次項目接到一個需求,需要盡可能多的展示AndroidManifest.xml 里面的信息娇妓,經(jīng)過我一周的折騰和采坑伐庭,發(fā)現(xiàn)目前有以下幾種方法
- 通過 PackageManager 系統(tǒng)API讀取
- 通過開源框架 AXmlResourceParser 來解析二進制的 AndroidManifest
- 通過Gradle腳本在處理 AndroidManifest.xml 的時候拷貝一份到 Assets 目錄用爪,然后解析 AndroidManifest
- 通過反射隱藏的系統(tǒng)API PackageParser 的 parsePackage 方法來直接獲取解析的結(jié)果
- 通過反射 AssetManager 的一個私有方法獲取二進制XML解析器來解析二進制的 AndroidManifest
接下來我會慢慢分享我這一次的采坑經(jīng)歷
2. 通過 PackageManager 的方式
首先拿到這個需求原押,我第一反應(yīng)就是通過 PackageManager 來獲取胁镐,主要有兩種方式來獲取
- 通過 getPackageInfo 方法來獲取偎血,想要什么數(shù)據(jù)诸衔,用傳遞不同的 FLAG,能獲取到的數(shù)據(jù)受限于 FLAG 的個數(shù)
- 通過 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)點:
- 是系統(tǒng)API,安全可靠
- 不會有版本兼容問題帖渠,不會有解析問題
然后是缺點:
- 能獲取到的數(shù)據(jù)只是google希望我們能查看到的數(shù)據(jù)谒亦,有一些數(shù)據(jù)獲取不到
- 使用起來較為繁瑣,特別是數(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 的方式運行。
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)有一個成熟的開源的泛型解析工具了渠驼,有興趣可以看看。
后來我發(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)了還是有一些不足的地方。
- 我只需要解析 AndroidManifest ,卻新增了 Gradle 腳本照卦,提高了維護成本
- 同事每次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來接
然后是解析出來了,但是這種方式的問題太大太大了:
- 這個方法在不同的系統(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)實的孝赫。
- 因為返回結(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 的徐绑。這下子我們就清楚了獲取解析編譯后的解析器的步驟了:
- 首先通過 AssetManager 的 addAssetPath 方法獲取 cookie邪驮,注意這個方法是隱藏的,所以需要通過反射來調(diào)用
- 然后通過 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 的幾種方式草巡∈匚兀看似簡單的文章,其實其中踩了非常多的坑山憨,遇到了非常多的困難查乒。不過最后我想說的也是這次印象最深的是:在即將放棄的時候冷靜下來,也許會找到不一樣的解決之道