Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android逆向之旅---解析編譯之後的AndroidManifest文件格式

Android逆向之旅---解析編譯之後的AndroidManifest文件格式

編輯:關於android開發

Android逆向之旅---解析編譯之後的AndroidManifest文件格式


一、前言

今天又是周六了,閒來無事,只能寫文章了呀,今天我們繼續來看逆向的相關知識,我們今天來介紹一下Android中的AndroidManifest文件格式的內容,有的同學可能好奇了,AndroidManifest文件格式有啥好說的呢?不會是介紹那些標簽和屬性是怎麼用的吧?那肯定不會,介紹那些知識有點無聊了,而且和我們的逆向也沒關系,我們今天要介紹的是Android中編譯之後的AndroidManifest文件的格式,首先來腦補一個知識點,Android中的Apk程序其實就是一個壓縮包,我們可以用壓縮軟件進行解壓的:

\

二、技術介紹

我們可以看到這裡有三個文件我們後續都會做詳細的解讀的:AndroidManifest.xml,classes.dex,resources.arsc

其實說到這裡只要反編譯過apk的同學都知道一個工具apktool,那麼其實他的工作原理就是解析這三個文件格式,因為本身Android在編譯成apk之後,這個文件有自己的格式,用普通文本格式打開的話是亂碼的,看不懂的,所以需要解析他們成我們能看懂的東東,所以從這篇文章開始,陸續介紹這三個文件的格式解析,這樣我們在後面反編譯apk的時候,遇到錯誤能夠精確的定位到問題。

今天我們先來看一下AndroidManifest.xml格式:

\

如果我們這裡顯示全是16進制的內容,所以我們需要解析,就像我之前解析so文件一樣:

任何一個文件都一定有他自己的格式,既然編譯成apk之後,變成這樣了,那麼google就是給AndroidManifest定義了一種文件格式,我們只需要知道這種格式的話,就可以詳細的解析出來文件了:

\

看到此圖是不是又很激動呢?這又是一張神圖,詳細的解析了AndroidManifest.xml文件的格式,但是光看這張圖我們可以看不出來啥,所以要結合一個案例來解析一個文件,這樣才能理解透徹,但是這樣圖是根基,下面我們就用一個案例來解析一下吧:

案例到處都是,誰便搞一個簡單的apk,用壓縮文件打開,解壓出AndroidManifest.xml就可以了,然後就開始讀取內容進行解析:

三、格式解析

第一、頭部信息

任何一個文件格式,都會有頭部信息的,而且頭部信息也很重要,同時,頭部一般都是固定格式的。

\

這裡的頭部信息還有這些字段信息:

1、文件魔數:四個字節

2、文件大小:四個字節

下面就開始解析所有的Chunk內容了,其實每個Chunk的內容都有一個相似點,就是頭部信息:

ChunkType(四個字節)和ChunkSize(四個字節)

第二、String Chunk內容

這個Chunk主要存放的是AndroidManifest文件中所有的字符串信息

\

1、ChunkType:StringChunk的類型,固定四個字節:0x001C0001

2、ChunkSize:StringChunk的大小,四個字節

3、StringCount:StringChunk中字符串的個數,四個字節

4、StyleCount:StringChunk中樣式的個數,四個字節,但是在實際解析過程中,這個值一直是0x00000000

5、Unknown:位置區域,四個字節,在解析的過程中,這裡需要略過四個字節

6、StringPoolOffset:字符串池的偏移值,四個字節,這個偏移值是相對於StringChunk的頭部位置

7、StylePoolOffset:樣式池的偏移值,四個字節,這裡沒有Style,所以這個字段可忽略

8、StringOffsets:每個字符串的偏移值,所以他的大小應該是:StringCount*4個字節

9、SytleOffsets:每個樣式的偏移值,所以他的大小應該是SytleCount*4個字節

後面就開始是字符串內容和樣式內容了。

\

下面我們就開始來看代碼了,由於代碼的篇幅有點長,所以這裡就分段說明,代碼的整個工程,後面我會給出下載地址的,

1、首先我們需要把AndroidManifest.xml文件讀入到一個byte數組中:

byte[] byteSrc = null;
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try{
	fis = new FileInputStream("xmltest/AndroidManifest1.xml");
	bos = new ByteArrayOutputStream();
	byte[] buffer = new byte[1024];
	int len = 0;
	while((len=fis.read(buffer)) != -1){
		bos.write(buffer, 0, len);
	}
	byteSrc = bos.toByteArray();
}catch(Exception e){
	System.out.println("parse xml error:"+e.toString());
}finally{
	try{
		fis.close();
		bos.close();
	}catch(Exception e){

	}
}

2、下面我們就來看看解析頭部信息:

/**
 * 解析xml的頭部信息
 * @param byteSrc
 */
public static void parseXmlHeader(byte[] byteSrc){
	byte[] xmlMagic = Utils.copyByte(byteSrc, 0, 4);
	System.out.println("magic number:"+Utils.bytesToHexString(xmlMagic));
	byte[] xmlSize = Utils.copyByte(byteSrc, 4, 4);
	System.out.println("xml size:"+Utils.bytesToHexString(xmlSize));

	xmlSb.append(" ");
	xmlSb.append("\n");
}
這裡沒什麼說的,按照上面我們說的那個格式解析即可

\

3、解析StringChunk信息

/**
 * 解析StringChunk
 * @param byteSrc
 */
public static void parseStringChunk(byte[] byteSrc){
	//String Chunk的標示
	byte[] chunkTagByte = Utils.copyByte(byteSrc, stringChunkOffset, 4);
	System.out.println("string chunktag:"+Utils.bytesToHexString(chunkTagByte));
	//String Size
	byte[] chunkSizeByte = Utils.copyByte(byteSrc, 12, 4);
	//System.out.println(Utils.bytesToHexString(chunkSizeByte));
	int chunkSize = Utils.byte2int(chunkSizeByte);
	System.out.println("chunk size:"+chunkSize);
	//String Count
	byte[] chunkStringCountByte = Utils.copyByte(byteSrc, 16, 4);
	int chunkStringCount = Utils.byte2int(chunkStringCountByte);
	System.out.println("count:"+chunkStringCount);

	stringContentList = new ArrayList(chunkStringCount);

	//這裡需要注意的是,後面的四個字節是Style的內容,然後緊接著的四個字節始終是0,所以我們需要直接過濾這8個字節
	//String Offset 相對於String Chunk的起始位置0x00000008
	byte[] chunkStringOffsetByte = Utils.copyByte(byteSrc, 28, 4);

	int stringContentStart = 8 + Utils.byte2int(chunkStringOffsetByte);
	System.out.println("start:"+stringContentStart);

	//String Content
	byte[] chunkStringContentByte = Utils.copyByte(byteSrc, stringContentStart, chunkSize);

	/**
	 * 在解析字符串的時候有個問題,就是編碼:UTF-8和UTF-16,如果是UTF-8的話是以00結尾的,如果是UTF-16的話以00 00結尾的
	 */

	/**
	 * 此處代碼是用來解析AndroidManifest.xml文件的
	 */
	//這裡的格式是:偏移值開始的兩個字節是字符串的長度,接著是字符串的內容,後面跟著兩個字符串的結束符00
	byte[] firstStringSizeByte = Utils.copyByte(chunkStringContentByte, 0, 2);
	//一個字符對應兩個字節
	int firstStringSize = Utils.byte2Short(firstStringSizeByte)*2;
	System.out.println("size:"+firstStringSize);
	byte[] firstStringContentByte = Utils.copyByte(chunkStringContentByte, 2, firstStringSize+2);
	String firstStringContent = new String(firstStringContentByte);
	stringContentList.add(Utils.filterStringNull(firstStringContent));
	System.out.println("first string:"+Utils.filterStringNull(firstStringContent));

	//將字符串都放到ArrayList中
	int endStringIndex = 2+firstStringSize+2;
	while(stringContentList.size() < chunkStringCount){
		//一個字符對應兩個字節,所以要乘以2
		int stringSize = Utils.byte2Short(Utils.copyByte(chunkStringContentByte, endStringIndex, 2))*2;
		String str = new String(Utils.copyByte(chunkStringContentByte, endStringIndex+2, stringSize+2));
		System.out.println("str:"+Utils.filterStringNull(str));
		stringContentList.add(Utils.filterStringNull(str));
		endStringIndex += (2+stringSize+2);
	}

	/**
	 * 此處的代碼是用來解析資源文件xml的
	 */
	/*int stringStart = 0;
		int index = 0;
		while(index < chunkStringCount){
			byte[] stringSizeByte = Utils.copyByte(chunkStringContentByte, stringStart, 2);
			int stringSize = (stringSizeByte[1] & 0x7F);
			System.out.println("string size:"+Utils.bytesToHexString(Utils.int2Byte(stringSize)));
			if(stringSize != 0){
				//這裡注意是UTF-8編碼的
				String val = "";
				try{
					val = new String(Utils.copyByte(chunkStringContentByte, stringStart+2, stringSize), "utf-8");
				}catch(Exception e){
					System.out.println("string encode error:"+e.toString());
				}
				stringContentList.add(val);
			}else{
				stringContentList.add("");
			}
			stringStart += (stringSize+3);
			index++;
		}

		for(String str : stringContentList){
			System.out.println("str:"+str);
		}*/

	resourceChunkOffset = stringChunkOffset + Utils.byte2int(chunkSizeByte);

}
這裡我們需要解釋的幾個點:

 

1、在上面的格式說明中,我們需要注意,有一個Unknow字段,四個字節,所以我們需要略過

2、在解析字符串內容的時候,字符串內容的結束符是:0x0000

3、每個字符串開始的前兩個字節是字符串的長度

所以我們有了每個字符串的偏移值和大小,那麼解析字符串內容就簡單了:

\

這裡我們看到0x000B(高位和低位相反)就是字符串的大小,結尾是0x0000

\
一個字符對應的是兩個字節,而且這裡有一個方法:Utils.filterStringNull(firstStringContent):

 

public static String filterStringNull(String str){
	if(str == null || str.length() == 0){
		return str;
	}
	byte[] strByte = str.getBytes();
	ArrayList newByte = new ArrayList();
	for(int i=0;i其實邏輯很簡單,就是過濾空字符串:在C語言中是NULL,在Java中就是00,如果不過濾的話,會出現下面的這種情況:

 

\ 每個字符是寬字符,很難看,其實願意就是每個字符後面多了一個00,所以過濾之後就可以了

\

這樣就好看多了。

上面我們就解析了AndroidManifest.xml中所有的字符串內容。這裡我們需要用一個全局的字符列表,用來存儲這些字符串的值,後面會用索引來獲取這些字符串的值。

 

第三、解析ResourceIdChunk

這個Chunk主要是存放的是AndroidManifest中用到的系統屬性值對應的資源Id,比如android:versionCode中的versionCode屬性,android是前綴,後面會說道

\

1、ChunkType:ResourceIdChunk的類型,固定四個字節:0x00080108

2、ChunkSize:ResourceChunk的大小,四個字節

3、ResourceIds:ResourceId的內容,這裡大小是ResourceChunk大小除以4,減去頭部的大小8個字節(ChunkType和ChunkSize)

\

 

/**
 * 解析Resource Chunk
 * @param byteSrc
 */
public static void parseResourceChunk(byte[] byteSrc){
	byte[] chunkTagByte = Utils.copyByte(byteSrc, resourceChunkOffset, 4);
	System.out.println(Utils.bytesToHexString(chunkTagByte));
	byte[] chunkSizeByte = Utils.copyByte(byteSrc, resourceChunkOffset+4, 4);
	int chunkSize = Utils.byte2int(chunkSizeByte);
	System.out.println("chunk size:"+chunkSize);
	//這裡需要注意的是chunkSize是包含了chunkTag和chunkSize這兩個字節的,所以需要剔除
	byte[] resourceIdByte = Utils.copyByte(byteSrc, resourceChunkOffset+8, chunkSize-8);
	ArrayList resourceIdList = new ArrayList(resourceIdByte.length/4);
	for(int i=0;i解析結果:

 

\ 我們看到這裡解析出來的id到底是什麼呢?

這裡需要腦補一個知識點了:

我們在寫Android程序的時候,都會發現有一個R文件,那裡面就是存放著每個資源對應的Id,那麼這些id值是怎麼得到的呢?

Package ID相當於是一個命名空間,限定資源的來源。Android系統當前定義了兩個資源命令空間,其中一個系統資源命令空間,它的Package ID等於0x01,另外一個是應用程序資源命令空間,它的Package ID等於0x7f。所有位於[0x01, 0x7f]之間的Package ID都是合法的,而在這個范圍之外的都是非法的Package ID。前面提到的系統資源包package-export.apk的Package ID就等於0x01,而我們在應用程序中定義的資源的Package ID的值都等於0x7f,這一點可以通過生成的R.java文件來驗證。 Type ID是指資源的類型ID。資源的類型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種,每一種都會被賦予一個ID。 Entry ID是指每一個資源在其所屬的資源類型中所出現的次序。注意,不同類型的資源的Entry ID有可能是相同的,但是由於它們的類型不同,我們仍然可以通過其資源ID來區別開來。 關於資源ID的更多描述,以及資源的引用關系,可以參考frameworks/base/libs/utils目錄下的README文件

我們可以得知系統資源對應id的xml文件是在哪裡:frameworks\base\core\res\res\values\public.xml

\

那麼我們用上面解析到的id,去public.xml文件中查詢一下:

\

查到了,是versionCode,對於這個系統資源id存放文件public.xml還是很重要的,後面在講解resource.arsc文件格式的時候還會繼續用到。

第四、解析StartNamespaceChunk

這個Chunk主要包含一個AndroidManifest文件中的命令空間的內容,Android中的xml都是采用Schema格式的,所以肯定有Prefix和Uri的。

這裡在腦補一個知識點:xml格式有兩種:DTD和Schema,不了解的同學可以閱讀這篇文章

\

1、ChunkType:Chunk的類型,固定四個字節:0x00100100

2、ChunkSize:Chunk的大小,四個字節

3、LineNumber:在AndroidManifest文件中的行號,四個字節

4、Unknown:未知區域,四個字節

5、Prefix:命名空間的前綴(在字符串中的索引值),比如:android

6、Uri:命名空間的uri(在字符串中的索引值):比如:http://schemas.android.com/apk/res/android

\

解析代碼:

/**
 * 解析StartNamespace Chunk
 * @param byteSrc
 */
public static void parseStartNamespaceChunk(byte[] byteSrc){
	//獲取ChunkTag
	byte[] chunkTagByte = Utils.copyByte(byteSrc, 0, 4);
	System.out.println(Utils.bytesToHexString(chunkTagByte));
	//獲取ChunkSize
	byte[] chunkSizeByte = Utils.copyByte(byteSrc, 4, 4);
	int chunkSize = Utils.byte2int(chunkSizeByte);
	System.out.println("chunk size:"+chunkSize);

	//解析行號
	byte[] lineNumberByte = Utils.copyByte(byteSrc, 8, 4);
	int lineNumber = Utils.byte2int(lineNumberByte);
	System.out.println("line number:"+lineNumber);

	//解析prefix(這裡需要注意的是行號後面的四個字節為FFFF,過濾)
	byte[] prefixByte = Utils.copyByte(byteSrc, 16, 4);
	int prefixIndex = Utils.byte2int(prefixByte);
	String prefix = stringContentList.get(prefixIndex);
	System.out.println("prefix:"+prefixIndex);
	System.out.println("prefix str:"+prefix);

	//解析Uri
	byte[] uriByte = Utils.copyByte(byteSrc, 20, 4);
	int uriIndex = Utils.byte2int(uriByte);
	String uri = stringContentList.get(uriIndex);
	System.out.println("uri:"+uriIndex);
	System.out.println("uri str:"+uri);

	uriPrefixMap.put(uri, prefix);
	prefixUriMap.put(prefix, uri);
}

解析的結果如下:

\
這裡的內容就是上面我們解析完String之後的對應的字符串索引值,這裡我們需要注意的是,一個xml中可能會有多個命名空間,所以這裡我們用Map存儲Prefix和Uri對應的關系,後面在解析節點內容的時候會用到。

第五、StratTagChunk

這個Chunk主要是存放了AndroidManifest.xml中的標簽信息了,也是最核心的內容,當然也是最復雜的內容

\

1、ChunkType:Chunk的類型,固定四個字節:0x00100102

2、ChunkSize:Chunk的大小,固定四個字節

3、LineNumber:對應於AndroidManifest中的行號,四個字節

4、Unknown:未知領域,四個字節

5、NamespaceUri:這個標簽用到的命名空間的Uri,比如用到了android這個前綴,那麼就需要用http://schemas.android.com/apk/res/android這個Uri去獲取,四個字節

6、Name:標簽名稱(在字符串中的索引值),四個字節

7、Flags:標簽的類型,四個字節,比如是開始標簽還是結束標簽等

8、AttributeCount:標簽包含的屬性個數,四個字節

9、ClassAtrribute:標簽包含的類屬性,四個字節

10,Atrributes:屬性內容,每個屬性算是一個Entry,這個Entry固定大小是大小為5的字節數組:

[Namespace,Uri,Name,ValueString,Data],我們在解析的時候需要注意第四個值,要做一次處理:需要右移24位。所以這個字段的大小是:屬性個數*5*4個字節

\

解析代碼:

 

/**
 * 解析StartTag Chunk
 * @param byteSrc
 */
public static void parseStartTagChunk(byte[] byteSrc){
	//解析ChunkTag
	byte[] chunkTagByte = Utils.copyByte(byteSrc, 0, 4);
	System.out.println(Utils.bytesToHexString(chunkTagByte));

	//解析ChunkSize
	byte[] chunkSizeByte = Utils.copyByte(byteSrc, 4, 4);
	int chunkSize = Utils.byte2int(chunkSizeByte);
	System.out.println("chunk size:"+chunkSize);

	//解析行號
	byte[] lineNumberByte = Utils.copyByte(byteSrc, 8, 4);
	int lineNumber = Utils.byte2int(lineNumberByte);
	System.out.println("line number:"+lineNumber);

	//解析prefix
	byte[] prefixByte = Utils.copyByte(byteSrc, 8, 4);
	int prefixIndex = Utils.byte2int(prefixByte);
	//這裡可能會返回-1,如果返回-1的話,那就是說沒有prefix
	if(prefixIndex != -1 && prefixIndex attrList = new ArrayList(attrCount);
	for(int i=0;i> 24);
				attrData.type = value;
				break;
			case 4:
				attrData.data = value;
				break;
			}
			values[j] = value;
		}
		attrList.add(attrData);
	}

	for(int i=0;i代碼有點長,我們來分析一下:

解析屬性:

//解析屬性
//這裡需要注意的是每個屬性單元都是由五個元素組成,每個元素占用四個字節:namespaceuri, name, valuestring, type, data
//在獲取到type值的時候需要右移24位
ArrayList attrList = new ArrayList(attrCount);
for(int i=0;i> 24);
			attrData.type = value;
			break;
		case 4:
			attrData.data = value;
			break;
		}
		values[j] = value;
	}
	attrList.add(attrData);
}
看到第四個值的時候,需要額外的處理一下,就是需要右移24位。

解析完屬性之後,那麼就可以得到一個標簽的名稱和屬性名稱和屬性值了:

\

看解析的結果:

\

 

標簽manifest包含的屬性:

\

這裡有幾個問題需要解釋一下:

1、為什麼我們看到的是三個屬性,但是解析打印的結果是5個?

因為系統在編譯apk的時候,會添加兩個屬性:platformBuildVersionCode和platformBuildVersionName

這個是發布的到設備的版本號和版本名稱

\

這個是解析之後的結果

2、當沒有android這樣的前綴的時候,NamespaceUri是null

\

3、當dataType不同,對應的data值也是有不同的含義的:

\

這個方法就是用來轉義的,後面在解析resource.arsc的時候也會用到這個方法。

4、每個屬性理論上都會含有一個NamespaceUri的,這個也決定了屬性的前綴Prefix,默認都是android,但是有時候我們會自定義一個控件的時候,這時候就需要導入NamespaceUri和Prefix了。所以一個xml中可能會有多個Namespace,每個屬性都會包含NamespaceUri的。

其實到這裡我們就算解析完了大部分的工作了,至於還有EndTagChunk,那個和StartTagChunk非常類似,這裡就不在詳解了:

/**
 * 解析EndTag Chunk
 * @param byteSrc
 */
public static void parseEndTagChunk(byte[] byteSrc){
	byte[] chunkTagByte = Utils.copyByte(byteSrc, 0, 4);
	System.out.println(Utils.bytesToHexString(chunkTagByte));
	byte[] chunkSizeByte = Utils.copyByte(byteSrc, 4, 4);
	int chunkSize = Utils.byte2int(chunkSizeByte);
	System.out.println("chunk size:"+chunkSize);

	//解析行號
	byte[] lineNumberByte = Utils.copyByte(byteSrc, 8, 4);
	int lineNumber = Utils.byte2int(lineNumberByte);
	System.out.println("line number:"+lineNumber);

	//解析prefix
	byte[] prefixByte = Utils.copyByte(byteSrc, 8, 4);
	int prefixIndex = Utils.byte2int(prefixByte);
	//這裡可能會返回-1,如果返回-1的話,那就是說沒有prefix
	if(prefixIndex != -1 && prefixIndex

 

但是我們在解析的時候,我們需要做一個循環操作:

\

因為我們知道,Android中在解析Xml的時候提供了很多種方式,但是這裡我們沒有用任何一種方式,而是用純代碼編寫的,所以用一個循環,來遍歷解析Tag,其實這種方式類似於SAX解析XML,這時候上面說到的那個Flag字段就大有用途了。

這裡我們還做了一個工作就是將解析之後的xml格式化一下:

\

難度不大,這裡也就不繼續解釋了,這裡有一個地方需要優化的就是,可以利用LineNumber屬性來,精確到格式化行數,不過這個工作量有點大,這裡就不想做了,有興趣的同學可以考慮一下,格式化完之後的結果:

\

帥氣不帥氣,把手把手的將之前的16進制的內容解析出來了,吊吊的,成就感爆棚呀~~

這裡有一個問題,就是我們看到這裡還有很多@7F070001這類的東西,這個其實是資源Id,這個需要我們後面解析完resource.arsc文件之後,就可以對應上這個資源了,後面會在提到一下。這裡就知道一下可以了。

這裡其實還有一個問題,就是我們發現這個可以解析AndroidManifest文件了,那麼同樣也可以解析其他的xml文件:

\

擦,我們發現解析其他xml的時候,發現報錯了,定位代碼發現是在解析StringChunk的地方報錯了,我們修改一下:

\

因為其他的xml中的字符串格式和AndroidManifest.xml中的不一樣,所以這裡需要單獨解析一下:

\

修改之後就可以了。

四、技術拓展

在反編譯的時候,有時候我們只想反編譯AndroidManifest內容,所以ApkTool工具就有點繁瑣了,不過網上有個牛逼的大神已經寫好了這個工具AXMLPrinter.jar,這個工具很好用的:java -jar AXMLPrinter.java xxx.xml >demo.xml

\

從項目結構我們可以發現,他用的是Android中自帶的Pull解析xml的,主函數是:

\

五、為什麼要寫這篇文章

那麼現在我們也可以不用這個工具了,因為我們自己也寫了一個工具解析,是不是很吊吊的呢?那麼我們這篇文章僅僅是為了解析AndroidManifest嗎?肯定不是,寫這篇文章其實是另有目的的,為我們後面在反編譯apk做准備,其實現在有很多同學都發現了,在使用apktool來反編譯apk的時候經常報出一些異常信息,其實那些就是加固的人,用來對抗apktool工具的,他們專門找apktool的漏洞,然後進行加固,從而達到反編譯失敗的效果,所以我們有必要了解apktool的源碼和解析原理,這樣才能遇到反編譯失敗的錯誤的時候,能定位到問題,在修復apktool工具即可,那麼apktool的工具解析原理其實很簡單,就是解析AndroidManifest.xml,然後是解析resource.arsc到public.xml(這個文件一般是反編譯之後存放在values文件夾下面的,是整個反編譯之後的工程對應的Id列表),其次就是classes.dex。還有其他的布局,資源xml等,那麼針對於這幾個問題,我們這篇文章就講解了:解析XML文件的問題。後面還會繼續講解如何解析resource.arsc和classes.dex文件的格式。當然後面我會介紹一篇關於如果通過修改AndroidManifest文件內容來達到加固的效果,以及如何我們做修復來破解這種加固。

六、總結

這篇文章到這裡就算結束了,寫的有點累了,解析代碼已經有下載地址了,有不理解的同學可以聯系我,加入公眾號,留言問題,我會在適當的時間給予回復,謝謝,同時記得關注後面的兩篇解析resource.arsc和classes.dex文件格式的文章。謝謝~~

PS: 關注微信,最新Android技術實時推送

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved