Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android簽名機制之---簽名過程詳解

Android簽名機制之---簽名過程詳解

編輯:關於Android編程

一、前言

又是過了好長時間,沒寫文章的雙手都有點難受了。今天是聖誕節,還是得上班。因為前幾天有一個之前的同事,在申請微信SDK的時候,遇到簽名的問題,問了我一下,結果把我難倒了。。我說Android中的簽名大家都會熟悉的,就是為了安全,不讓別人修改你的apk,但是我們真正的有了解多少呢?所以准備兩篇文章好好介紹一下Android中簽名機制。

在說道Android簽名之前,我們需要了解的幾個知識點

1、數據摘要(數據指紋)、簽名文件,證書文件

2、jarsign工具簽名和signapk工具簽名

3、keystore文件和pk8文件,x509.pem文件的關系

4、如何手動的簽名apk

上面介紹的四個知識點,就是今天介紹的核心,我們來一一看這些問題。

 

二、准備知識

首先來看一下數據摘要,簽名文件,證書文件的知識點

1、數據摘要

這個知識點很好理解,百度百科即可,其實他也是一種算法,就是對一個數據源進行一個算法之後得到一個摘要,也叫作數據指紋,不同的數據源,數據指紋肯定不一樣,就和人一樣。

消息摘要算法(Message Digest Algorithm)是一種能產生特殊輸出格式的算法,其原理是根據一定的運算規則對原始數據進行某種形式的信息提取,被提取出的信息就被稱作原始數據的消息摘要。
著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的變體。
消息摘要的主要特點有:
1)無論輸入的消息有多長,計算出來的消息摘要的長度總是固定的。例如應用MD5算法摘要的消息有128個比特位,用SHA-1算法摘要的消息最終有160比特位的輸出。
2)一般來說(不考慮碰撞的情況下),只要輸入的原始數據不同,對其進行摘要以後產生的消息摘要也必不相同,即使原始數據稍有改變,輸出的消息摘要便完全不同。但是,相同的輸入必會產生相同的輸出。
3)具有不可逆性,即只能進行正向的信息摘要,而無法從摘要中恢復出任何的原始消息。

2、簽名文件和證書

簽名文件和證書是成對出現了,二者不可分離,而且我們後面通過源碼可以看到,這兩個文件的名字也是一樣的,只是後綴名不一樣。

其實數字簽名的概念很簡單。大家知道,要確保可靠通信,必須要解決兩個問題:首先,要確定消息的來源確實是其申明的那個人;其次,要保證信息在傳遞的過程中不被第三方篡改,即使被篡改了,也可以發覺出來。
所謂數字簽名,就是為了解決這兩個問題而產生的,它是對前面提到的非對稱加密技術與數字摘要技術的一個具體的應用。
對於消息的發送者來說,先要生成一對公私鑰對,將公鑰給消息的接收者。
如果消息的發送者有一天想給消息接收者發消息,在發送的信息中,除了要包含原始的消息外,還要加上另外一段消息。這段消息通過如下兩步生成:
1)對要發送的原始消息提取消息摘要;
2)對提取的信息摘要用自己的私鑰加密。
通過這兩步得出的消息,就是所謂的原始信息的數字簽名。
而對於信息的接收者來說,他所收到的信息,將包含兩個部分,一是原始的消息內容,二是附加的那段數字簽名。他將通過以下三步來驗證消息的真偽:
1)對原始消息部分提取消息摘要,注意這裡使用的消息摘要算法要和發送方使用的一致;
2)對附加上的那段數字簽名,使用預先得到的公鑰解密;
3)比較前兩步所得到的兩段消息是否一致。如果一致,則表明消息確實是期望的發送者發的,且內容沒有被篡改過;相反,如果不一致,則表明傳送的過程中一定出了問題,消息不可信。
通過這種所謂的數字簽名技術,確實可以有效解決可靠通信的問題。如果原始消息在傳送的過程中被篡改了,那麼在消息接收者那裡,對被篡改的消息提取的摘要肯定和原始的不一樣。並且,由於篡改者沒有消息發送方的私鑰,即使他可以重新算出被篡改消息的摘要,也不能偽造出數字簽名。
所以,綜上所述,數字簽名其實就是只有信息的發送者才能產生的別人無法偽造的一段數字串,這段數字串同時也是對信息的發送者發送信息真實性的一個有效證明。
不知道大家有沒有注意,前面講的這種數字簽名方法,有一個前提,就是消息的接收者必須要事先得到正確的公鑰。如果一開始公鑰就被別人篡改了,那壞人就會被你當成好人,而真正的消息發送者給你發的消息會被你視作無效的。而且,很多時候根本就不具備事先溝通公鑰的信息通道。那麼如何保證公鑰的安全可信呢?這就要靠數字證書來解決了。
所謂數字證書,一般包含以下一些內容:
證書的發布機構(Issuer)
證書的有效期(Validity)
消息發送方的公鑰
證書所有者(Subject)
數字簽名所使用的算法
數字簽名
可以看出,數字證書其實也用到了數字簽名技術。只不過要簽名的內容是消息發送方的公鑰,以及一些其它信息。但與普通數字簽名不同的是,數字證書中簽名者不是隨隨便便一個普通的機構,而是要有一定公信力的機構。這就好像你的大學畢業證書上簽名的一般都是德高望重的校長一樣。一般來說,這些有公信力機構的根證書已經在設備出廠前預先安裝到了你的設備上了。所以,數字證書可以保證數字證書裡的公鑰確實是這個證書的所有者的,或者證書可以用來確認對方的身份。數字證書主要是用來解決公鑰的安全發放問題。
綜上所述,總結一下,數字簽名和簽名驗證的大體流程如下圖所示:

\

 

3、jarsign和signapk工具

了解到完了簽名中的三個文件的知識點之後,下面繼續來看看Android中簽名的兩個工具:jarsign和signapk

關於這兩個工具開始的時候很容易混淆,感覺他們兩到底有什麼區別嗎?

其實這兩個工具很好理解,jarsign是Java本生自帶的一個工具,他可以對jar進行簽名的。而signapk是後面專門為了Android應用程序apk進行簽名的工具,他們兩的簽名算法沒什麼區別,主要是簽名時使用的文件不一樣,這個就要引出第三個問題了。

4、keystore文件和pk8,x509.pem文件的區別

我們上面了解到了jarsign和signapk兩個工具都可以進行Android中的簽名,那麼他們的區別在於簽名時使用的文件不一樣

jarsign工具簽名時使用的是keystore文件

signapk工具簽名時使用的是pk8,x509.pem文件

其中我們在使用Eclipse工具寫程序的時候,出Debug包的時候,默認用的是jarsign工具進行簽名的,而且Eclipse中有一個默認簽名文件:

\

我們可以看到這個默認簽名的keystore文件,當然我們可以選擇我們自己指定的keystore文件。

這裡還有一個知識點:

我們看到上面有MD5和SHA1的摘要,這個就是keystore文件中私鑰的數據摘要,這個信息也是我們在申請很多開發平台賬號的時候需要填入的信息,比如申請百度地圖,微信SDK等,會需要填寫應用的MD5或者是SHA1信息

5、手動的簽名Apk包

1》使用keytool和jarsigner來進行簽名

當然,我們在正式簽名處release包的時候,我們需要創建一個自己的keystore文件:

\

\

這裡我們可以對keystore文件起自己的名字,而且後綴名也是無關緊要的。創建完文件之後,也會生成MD5和SHA1的值,這個值可以不用記錄的,可以通過命令查看keystore文件的MD5和SHA1的值。

keytool -list -keystore debug.keystore

\

當然我們都知道這個keytstore文件的重要性,說白了就相當於你的銀行卡密碼。你懂得。

這裡我們看到用Eclipse自動簽名和生成一個keystore文件,我們也可以使用keytool工具生成一個keystore文件。這個方法網上有,這裡就不做太多的介紹了。然後我們可以使用jarsign來對apk包進行簽名了。

我們可以手動的生成一個keystore文件:

keytool -genkeypair -v -keyalg DSA -keysize 1024 -sigalg SHA1withDSA -validity 20000 -keystore D:\jiangwei.keystore -alias jiangwei -keypass jiangwei -storepass jiangwei

\

這個命令有點長,有幾個重要的參數需要說明:

-alias是定義別名,這裡為debug

-keyalg是規定簽名算法,這裡是DSA,這裡的算法直接關系到後面apk中簽名文件的後綴名,到後面會詳細說明

 

在用jarsigner工具進行簽名

jarsigner -verbose -sigalg SHA1withDSA -digestalg SHA1 -keystore D:\jiangwei.keystore -storepass jiangwei D:\123.apk jiangwei

\

這樣我們就成功的對apk進行簽名了。

簽名的過程中遇到的問題:

1》證書鏈找不到的問題

\

這個是因為最後一個參數alias,是keystore的別名輸錯了。


2》生成keystore文件的時候提示密碼錯誤\
這個原因是因為在當前目錄已經有debug.ketystore了,在生成一個debug.keystore的話,就會報錯
3》找不到別名的問題\
這個問題的原因是因為我們在使用keytool生成keystore的時候,起了debug的別名,這個問題困擾了我很久,最後做了很多例子才發現的,就是只要我們的keystore文件的別名是debug的話,就會報這樣的錯誤。這個應該和系統默認的簽名debug.keystore中的別名是debug有關系吧?沒有找到jarsigner的源碼,所以只能猜測了,但是這三個問題在這裡標注一下,以防以後在遇到。

 

注意:Android中是允許使用多個keystore對apk進行簽名的,這裡我就不在粘貼命令了,我又創建了幾個keystore對apk進行簽名:

\

這裡我把簽名之後的apk進行解壓之後,發現有三個簽名文件和證書(.SF/.DSA)

這裡我也可以注意到,我們簽名時用的是DSA算法,這裡的文件後綴名就是DSA

而且文件名是keystore的別名

哎,這裡算是理清楚了我們上面的如何使用keytool產生keystore以及,用jarsigner來進行簽名。

 

2》使用signapk來進行簽名

下面我們再來看看signapk工具進行簽名:

java -jar signapk.jar .testkey.x509.pem testkey.pk8 debug.apk debug.sig.apk

這裡需要兩個文件:.pk8和.x509.pem這兩個文件

pk8是私鑰文件

x509.pem是含有公鑰的文件

這裡簽名的話就不在演示了,這裡沒什麼問題的。

但是這裡需要注意的是:signapk簽名之後的apk中的META-INF文件夾中的三個文件的名字是這樣的,因為signapk在前面的時候不像jarsigner會自動使用別名來命名文件,這裡就是寫死了是CERT的名字,不過文件名不影響的,後面分析Android中的Apk校驗過程中會說道,只會通過後綴名來查找文件。

\

 

3》兩種的簽名方式有什麼區別

那麼問題來了,jarsigner簽名時用的是keystore文件,signapk簽名時用的是pk8和x509.pem文件,而且都是給apk進行簽名的,那麼keystore文件和pk8,x509.pem他們之間是不是有什麼聯系呢?答案是肯定的,網上搜了一下,果然他們之間是可以轉化的,這裡就不在分析如何進行轉化的,網上的例子貌似很多,有專門的的工具可以進行轉化:

\

那麼到這裡我們就弄清楚了這兩個簽名工具的區別和聯系。

 

三、分析Android中簽名流程機制

下面我們開始從源碼的角度去看看Android中的簽名機制和原理流程

因為網上沒有找到jarsigner的源碼,但是找到了signapk的源碼,那麼下面我們就來看看signapk的源碼吧:

源碼位置:com/android/signapk/sign.java

通過上面的簽名時我們可以看到,Android簽名apk之後,會有一個META-INF文件夾,這裡有三個文件:

MANIFEST.MF

CERT.RSA

CERT.SF

下面來看看這三個文件到底是干啥的?

1、MANIFEST.MF

\

我們來看看源碼:

 

public static void main(String[] args) {
    if (args.length != 4) {
        System.err.println("Usage: signapk " +
                "publickey.x509[.pem] privatekey.pk8 " +
                "input.jar output.jar");
        System.exit(2);
    }

    JarFile inputJar = null;
    JarOutputStream outputJar = null;

    try {
        X509Certificate publicKey = readPublicKey(new File(args[0]));

        // Assume the certificate is valid for at least an hour.
        long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;

        PrivateKey privateKey = readPrivateKey(new File(args[1]));
        inputJar = new JarFile(new File(args[2]), false);  // Don't verify.
        outputJar = new JarOutputStream(new FileOutputStream(args[3]));
        outputJar.setLevel(9);

        JarEntry je;

        // MANIFEST.MF
        Manifest manifest = addDigestsToManifest(inputJar);
        je = new JarEntry(JarFile.MANIFEST_NAME);
        je.setTime(timestamp);
        outputJar.putNextEntry(je);
        manifest.write(outputJar);

        // CERT.SF
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initSign(privateKey);
        je = new JarEntry(CERT_SF_NAME);
        je.setTime(timestamp);
        outputJar.putNextEntry(je);
        writeSignatureFile(manifest,
                new SignatureOutputStream(outputJar, signature));

        // CERT.RSA
        je = new JarEntry(CERT_RSA_NAME);
        je.setTime(timestamp);
        outputJar.putNextEntry(je);
        writeSignatureBlock(signature, publicKey, outputJar);

        // Everything else
        copyFiles(manifest, inputJar, outputJar, timestamp);
    } catch (Exception e) {
        e.printStackTrace();
        System.exit(1);
    } finally {
        try {
            if (inputJar != null) inputJar.close();
            if (outputJar != null) outputJar.close();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
}
在main函數中,我們看到需要輸入四個參數,然後就做了三件事:

 

寫MANIFEST.MF

//MANIFEST.MF
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
在進入方法看看:

 

 

/** Add the SHA1 of every file to the manifest, creating it if necessary. */
private static Manifest addDigestsToManifest(JarFile jar)
        throws IOException, GeneralSecurityException {
    Manifest input = jar.getManifest();
    Manifest output = new Manifest();
    Attributes main = output.getMainAttributes();
    if (input != null) {
        main.putAll(input.getMainAttributes());
    } else {
        main.putValue("Manifest-Version", "1.0");
        main.putValue("Created-By", "1.0 (Android SignApk)");
    }

    BASE64Encoder base64 = new BASE64Encoder();
    MessageDigest md = MessageDigest.getInstance("SHA1");
    byte[] buffer = new byte[4096];
    int num;

    // We sort the input entries by name, and add them to the
    // output manifest in sorted order.  We expect that the output
    // map will be deterministic.

    TreeMap byName = new TreeMap();

    for (Enumeration e = jar.entries(); e.hasMoreElements(); ) {
        JarEntry entry = e.nextElement();
        byName.put(entry.getName(), entry);
    }

    for (JarEntry entry: byName.values()) {
        String name = entry.getName();
        if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
            !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
            (stripPattern == null ||
             !stripPattern.matcher(name).matches())) {
            InputStream data = jar.getInputStream(entry);
            while ((num = data.read(buffer)) > 0) {
                md.update(buffer, 0, num);
            }

            Attributes attr = null;
            if (input != null) attr = input.getAttributes(name);
            attr = attr != null ? new Attributes(attr) : new Attributes();
            attr.putValue("SHA1-Digest", base64.encode(md.digest()));
            output.getEntries().put(name, attr);
        }
    }

    return output;
}
代碼邏輯還是很簡單的,主要看那個循環的意思:

 

除了三個文件(MANIFEST.MF,CERT.RSA,CERT.SF),其他的文件都會對文件內容做一次SHA1算法,就是計算出文件的摘要信息,然後用Base64進行編碼即可,下面我們用工具來做個案例看看是不是這樣:

首先安裝工具:HashTab

下載地址:http://www.baidu.com/s?wd=hashtab&rsv_spt=1&issp=1&f=8&rsv_bp=0&ie=utf-8&tn=baiduhome_pg&bs=hashtable

然後還有一個網站就是在線計算Base64:http://tomeko.net/online_tools/hex_to_base64.php?lang=en

那下面就開始我們的驗證工作吧:

我們就來驗證一下AndroidManifest.xml文件,首先在MANIFEST.MF文件中找到這個條目,記錄SHA1的值

\

然後我們安裝HashTab之後,找到AndroidManifest.xml文件,右擊,選擇Hashtab:

\

復制SHA-1的值:9C64812DE7373B201C294101473636A3697FD73C,到上面的那個Base64轉化網站,轉化一下:

\

nGSBLec3OyAcKUEBRzY2o2l/1zw=

和MANIFEST.MF中的條目內容一模一樣啦啦

那麼從上面的分析我們就知道了,其實MANIFEST.MF中存儲的是:

逐一遍歷裡面的所有條目,如果是目錄就跳過,如果是一個文件,就用SHA1(或者SHA256)消息摘要算法提取出該文件的摘要然後進行BASE64編碼後,作為“SHA1-Digest”屬性的值寫入到MANIFEST.MF文件中的一個塊中。該塊有一個“Name”屬性,其值就是該文件在apk包中的路徑。

 

2、下面再來看一下CERT.SF文件內容

\

這裡的內容感覺和MANIFEST.MF的內容差不多,來看看代碼吧:

 

//CERT.SF
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureFile(manifest,new SignatureOutputStream(outputJar, signature));
進入到writeSignatureFile方法中:

 

 

/** Write a .SF file with a digest the specified manifest. */
private static void writeSignatureFile(Manifest manifest, OutputStream out)
        throws IOException, GeneralSecurityException {
    Manifest sf = new Manifest();
    Attributes main = sf.getMainAttributes();
    main.putValue("Signature-Version", "1.0");
    main.putValue("Created-By", "1.0 (Android SignApk)");

    BASE64Encoder base64 = new BASE64Encoder();
    MessageDigest md = MessageDigest.getInstance("SHA1");
    PrintStream print = new PrintStream(
            new DigestOutputStream(new ByteArrayOutputStream(), md),
            true, "UTF-8");

    // Digest of the entire manifest
    manifest.write(print);
    print.flush();
    main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));

    Map entries = manifest.getEntries();
    for (Map.Entry entry : entries.entrySet()) {
        // Digest of the manifest stanza for this entry.
        print.print("Name: " + entry.getKey() + "\r\n");
        for (Map.Entry

 

首先我們可以看到,需要對之前的MANIFEST.MF文件整個內容做一個SHA1放到SHA1-Digest-Manifest字段中:

\

我們看看出入的manifest變量就是剛剛寫入了MANIFEST.MF文件的

\
這個我們可以驗證一下:

\

然後轉化一下

\

看到了吧,和文件中的值是一樣的啦啦

\

下面我們繼續看代碼,有一個循環:

 

Map entries = manifest.getEntries();
for (Map.Entry entry : entries.entrySet()) {
    // Digest of the manifest stanza for this entry.
    print.print("Name: " + entry.getKey() + "\r\n");
    for (Map.Entry
這裡還是用到了剛剛傳入的mainfest變量,遍歷他的條目內容,然後進行SHA算法計算在Base64一下:

 

其實就是對MANIFEST.MF文件中的每個條目內容做一次SHA,在保存一下即可,做個例子驗證一下:

用AndroidManifest.xml為例,我們把MANIFEST.MF文件中的條目拷貝保存到txt文檔中:

\
這裡需要注意的是,我們保存之後,需要添加兩個換行,我們可以在代碼中看到邏輯:

\

然後我們計算txt文檔的SHA值:

\\

看到了吧,這裡計算的值是一樣的啦啦

\

到這裡我們就知道CERT.SF文件做了什麼:

1》計算這個MANIFEST.MF文件的整體SHA1值,再經過BASE64編碼後,記錄在CERT.SF主屬性塊(在文件頭上)的“SHA1-Digest-Manifest”屬性值值下

2》逐條計算MANIFEST.MF文件中每一個塊的SHA1,並經過BASE64編碼後,記錄在CERT.SF中的同名塊中,屬性的名字是“SHA1-Digest

 

3、最後我們在來看一下CERT.RSA文件

\

這裡我們看到的都是二進制文件,因為RSA文件加密了,所以我們需要用openssl命令才能查看其內容

openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs –text

\

關於這些信息,可以看下面這張圖:

\

我們來看一下代碼:

 

/** Write a .RSA file with a digital signature. */
private static void writeSignatureBlock(
        Signature signature, X509Certificate publicKey, OutputStream out)
        throws IOException, GeneralSecurityException {
    SignerInfo signerInfo = new SignerInfo(
            new X500Name(publicKey.getIssuerX500Principal().getName()),
            publicKey.getSerialNumber(),
            AlgorithmId.get("SHA1"),
            AlgorithmId.get("RSA"),
            signature.sign());

    PKCS7 pkcs7 = new PKCS7(
            new AlgorithmId[] { AlgorithmId.get("SHA1") },
            new ContentInfo(ContentInfo.DATA_OID, null),
            new X509Certificate[] { publicKey },
            new SignerInfo[] { signerInfo });

    pkcs7.encodeSignedData(out);
}
我們看到,這裡會把之前生成的 CERT.SF文件, 用私鑰計算出簽名, 然後將簽名以及包含公鑰信息的數字證書一同寫入 CERT.RSA 中保存。CERT.RSA是一個滿足PKCS7格式的文件。

四、為何要這麼來簽名

上面我們就介紹了簽名apk之後的三個文件的詳細內容,那麼下面來總結一下,Android中為何要用這種方式進行加密簽名,這種方加密是不是最安全的呢?下面我們來分析一下,如果apk文件被篡改後會發生什麼。

首先,如果你改變了apk包中的任何文件,那麼在apk安裝校驗時,改變後的文件摘要信息與MANIFEST.MF的檢驗信息不同,於是驗證失敗,程序就不能成功安裝。
其次,如果你對更改的過的文件相應的算出新的摘要值,然後更改MANIFEST.MF文件裡面對應的屬性值,那麼必定與CERT.SF文件中算出的摘要值不一樣,照樣驗證失敗。
最後,如果你還不死心,繼續計算MANIFEST.MF的摘要值,相應的更改CERT.SF裡面的值,那麼數字簽名值必定與CERT.RSA文件中記錄的不一樣,還是失敗。
那麼能不能繼續偽造數字簽名呢?不可能,因為沒有數字證書對應的私鑰。
所以,如果要重新打包後的應用程序能再Android設備上安裝,必須對其進行重簽名。

從上面的分析可以得出,只要修改了Apk中的任何內容,就必須重新簽名,不然會提示安裝失敗,當然這裡不會分析,後面一篇文章會注重分析為何會提示安裝失敗。

五、知識點梳理

1、數據指紋,簽名文件,證書文件的含義

1》數據指紋就是對一個數據源做SHA/MD5算法,這個值是唯一的

2》簽名文件技術就是:數據指紋+RSA算法

3》證書文件中包含了公鑰信息和其他信息

4》在Android簽名之後,其中SF就是簽名文件,RSA就是證書文件我們可以使用openssl來查看RSA文件中的證書信息和公鑰信息

2、我們了解了Android中的簽名有兩種方式:jarsigner和signapk 這兩種方式的區別是:

1》jarsigner簽名時,需要的是keystore文件,而signapk簽名的時候是pk8,x509.pem文件

2》jarsigner簽名之後的SF和RSA文件名默認是keystore的別名,而signapk簽名之後文件名是固定的:CERT

3》Eclipse中我們在跑Debug程序的時候,默認用的是jarsigner方式簽名的,用的也是系統默認的debug.keystore簽名文件

4》keystore文件和pk8,x509.pem文件之間可以互相轉化

六、思考

我們在分析了簽名技術之後,無意中發現一個問題,就是CERT.SF,MANIFEST.MF,這兩個文件中的內容的name字段都是apk中的資源名,那麼就有一個問題了,如果資源名很長,而且apk中的資源很多,那麼這兩個文件就會很大,那麼這裡我們是不是可以優化呢?後面在分析如何減小apk大小的文章中會繼續講解,這裡先提出這個問題。

\

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