Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> apktool編譯和反編譯apk與ecplise多渠道打包

apktool編譯和反編譯apk與ecplise多渠道打包

編輯:關於Android編程

想自己做個apk,還在為素材而苦惱嗎?

看到優秀的apk設計,還在為怎麼看到別人的實現代碼而苦惱嗎?

看著AndroidStudio 多渠道打包那麼爽,而自己坑爹的還在用Eclipse,始終搞不定多渠道打包而苦惱嗎?

現在這些統統都不是問題,以前全要10塊8塊的,現在全部都要2塊。。

看一下本篇博客的大綱:

使用apktool 反編譯apk 使用apktool 重新編譯打包apk 使用jdk中的jarsigner對新打包的apk進行簽名。 編寫程序實現Eclipse 的 Android 多渠道打包。 獲取渠道值

在開始之前,上傳一個目錄圖,便於下面的進行
這裡寫圖片描述

apktool 反編譯apk

apktool 是 Android apk 的一個編譯和反編譯工具,他是一個jar 包。使用它肯定要先去下載該jar包。可以去官網下載,這裡提供一個csdn 的下載地址 apktool v2.1.1

在使用之前需要配置jdk 環境 ,這個應該搞android 的都配置了。。

下面開始反編譯:

關鍵命令

java -jar apktool.jar d -f -s apkName

跳轉到我們編譯的目錄,最好將需要編譯的文件和apktool放在一起。

使用命令

java -jar apktool.jar d -f -s test.apk

這裡寫圖片描述vcv509DXytS0zsS8/ra8xNy08r+qo6yy6b+0oaM8L3A+DQo8aDMgaWQ9"apktool-編譯apk">apktool 編譯apk

反編譯之後,我們就用反編譯後的文件在進行重新編譯。有人會說了,你咋這麼蛋疼,一會你就明白了。

命令:

java -jar apktool.jar b 需要編譯的文件 編譯後的名字

開始操作

仍然在當前目錄下,執行如下命令

java -jar apktool.jar b test name.apk

這裡寫圖片描述

這時候會在需要編譯文件的目錄裡多出一個dist目錄,裡面包含的就是我們編譯好的文件。

我的目錄如下

E:\apk_build\test\dist

不知道為什麼,定義的編譯後的名字沒有作用,和我們原apk同名。

對新編譯的 apk 重新簽名

這裡就需要使用 JDK 中的簽名工具jarsigner.具體目錄位置為

C:\Program Files\Java\jdk1.8.0_91\bin

當然如果配置了JDK環境變量,就不需要寫全路徑了。

命令:

jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore 簽名文件 -storepass 倉庫密碼 -keypass 口令密碼 -signedjar 簽名後的文件 待簽名的文件 口令名

開始操作:

最好將簽名文件也放到當前目錄:

jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore alex.keystore -storepass 123456 -keypass 111111 -signedjar test_signer.apk E:\apk_build\test\dist\test.apk test

這一段很長,就不貼全了,貼個最後簽名好的結果
這裡寫圖片描述

Eclipse 實現多渠道打包

前面鋪墊了這麼多,終於開始搞大頭了。

先說一下步驟:

定義txt文件,保存不同的渠道信息。 程序讀取渠道信息。 對apk 反編譯 。 替換清單文件中關於渠道包的關鍵字段。 重新編譯打包。 簽名。

是不是一氣呵成。下面開始搞,將以友盟多渠道打包舉例(流程過程中只貼部分代碼,最後的工具類會貼到博客最後):

先看一下初始的文件目錄,稍後會將該工程共享到github。

這裡寫圖片描述

定義txt 文件,保存不同的渠道信息,名字不要改,代碼中使用的就是這個名字,改了會出問題。
baidu
yingyongbao
wandoujia

注意: 每一個渠道單獨一行。

程序讀取渠道信息
/**
     * 獲取渠道字段
     */
    private void getCannelFile() {
        File file = new File("channel.txt");
        // 如果文件不存在,則提示
        if (file.exists() && file.isFile()) {
            BufferedReader br = null;
            FileReader fr = null;
            try {

                // 獲取到渠道的輸入流
                br = new BufferedReader(new FileReader(file));

                String line = null;

                while ((line = br.readLine()) != null) {
                    // 獲取到渠道
                    channelList.add(line.trim());
                }

                System.out.println(channelList);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fr != null) {
                        fr.close();
                    }
                    if (br != null) {
                        br.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("*********獲取渠道成功 ***********");
        } else {
            System.err
                    .println("*********error: channel.txt文件不存在,請添加渠道文件***********");
        }
    }

獲取到的渠道保存到一個集合中存儲,後面用。

對apk 反編譯

使用apk 反編譯,需要用到控制台命令,所以在此編譯了一個類,用以調用控制台。


    /**
     * 執行控制台指令
     * 
     * @param cmd
     */
    public void runCmd(String cmd) {
        Runtime rt = Runtime.getRuntime();
        BufferedReader br = null;
        InputStreamReader isr = null;
        try {
            // 執行
            Process p = rt.exec(cmd);
            // 獲取對應流,一遍打印控制台輸出的信息
            isr = new InputStreamReader(p.getInputStream());
            br = new BufferedReader(isr);
            String msg = null;
            while ((msg = br.readLine()) != null) {
                System.out.println(msg);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (isr != null) {
                    isr.close();
                }
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

指令很簡單,就和我們之前編譯的apk 幾乎相似

        // 1, 將該App 反編譯
        String cmdUnpack = "cmd.exe /C java -jar apktool.jar d -f -s "
                + apkName;
        runCmd(cmdUnpack);

apkName是待編譯的apk。

替換清單文件,該過程稍微復雜。因為我們需要替換多個清單文件,所以需要保持一份最初的已做替換
// 2, 移動清單文件,作為備份

        // 獲取編譯後後的目錄名 和目錄文件
        String decodeDir = apkName.split(".apk")[0];
        File decodeDirFile = new File(decodeDir);

        // 獲取清單文件
        String maniPath = decodeDirFile.getAbsolutePath()
                + "\\AndroidManifest.xml";

        // 獲取備份清單文件目錄 工程根目錄
        String maniPathSave = basePath + "\\AndroidManifest_back.xml";

        // 備份清單文件
        new File(maniPath).renameTo(new File(maniPathSave));
        System.out.println("*********備份清單文件 ***********");
備份完清單文件之後,就開始對每一個渠道執行 替換清單文件 -> 重新編譯打包 -> 重新簽名。

替換清單文件,在此單獨寫了一個方法

/**
     * 修改渠道值
     * 
     * @param sourcePath
     *            備份清單文件地址
     * @param targetPath
     *            目標清單文件地址
     * @param channelStr
     *            要求該的渠道值
     */
    public void updateChannel(String sourcePath, String targetPath,
            String channelStr) {

        BufferedReader br = null;
        FileReader fr = null;
        FileWriter fw = null;
        try {
            // 從備份中讀取內容
            fr = new FileReader(sourcePath);
            br = new BufferedReader(fr);
            String line = null;
            StringBuffer sb = new StringBuffer();
            while ((line = br.readLine()) != null) {
                // 如果某一行存在qwertyy 則替換該值
                if (line.contains("qwertyy")) {
                    line = line.replaceAll("qwertyy", channelStr);
                }
                sb.append(line + "\n");
            }
            // 寫到目標清單文件
            fw = new FileWriter(targetPath);
            fw.write(sb.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fr != null) {
                    fr.close();
                }
                if (br != null) {
                    br.close();
                }
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

說一下思路: 先從備份的清單文件中將數據按行讀取出來,如果某一行定義了我們的標示,則替換標示為我們的指定渠道值,這樣我們就獲取到了一個即將打包的清單文件字符串。將其寫入到需要打包的目錄中。

在此定義的標示為qwertyy,應該不會有重復值,如果有,可以自己改,但此值要與清單文件中的對應,例如我測試包中的清單文件的標示為:

 
開始打包
// 重新打包
            String cmdPack = String.format(
                    "cmd.exe /C java -jar apktool.jar b %s %s", decodeDir,
                    apkName);

            runCmd(cmdPack);
重新簽名
            // 簽名文件地址
            String keyStorePath = basePath + "\\" + keystoreName;

            // 未簽名的apk 地址
            String unsign_apk_path = decodeDir + "\\dist\\" + apkName;

            // 簽名後的apk
            String sign_apk_path = basePath + "\\" + channelList.get(i)
                    + ".apk";

            String signCmd = jarsignerPath
                    + " -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore "
                    + keyStorePath + " -storepass " + storepass + " -keypass "
                    + keypass + " -signedjar " + sign_apk_path + " "
                    + unsign_apk_path + " " + keyName;

            runCmd(signCmd);

signCmd有一點特殊,應為我無法再代碼中獲取到jarsigner命令,所以走了一個迂回的方式。

jarsignerPath的值為:C:\\Program Files\\Java\\jdk1.8.0_91\\bin\\jarsigner.exe,其實就是我本地jdk中該程序的地址。

最後看一下整體的類:



public class SplitApk {
    HashMap qudao = new HashMap();// 渠道號,渠道名

    ArrayList channelList = new ArrayList<>();

    String basePath;// 當前文件夾路徑

    // apk 名
    private String apkName;

    // 秘鑰文件名
    private String keystoreName ;

    //倉庫密碼
    private String storepass ;

    // 口令密碼
    private String keypass ;

    // 口令名
    private String keyName ;

    // 簽名工具地址(全路徑)
    private String jarsignerPath ;

    public SplitApk(String apkName, String keystoreName, String storepass,
            String keyName, String keypass, String jarsignerPath) {
        this.apkName = apkName;
        this.keystoreName = keystoreName;
        this.storepass = storepass;
        this.keypass = keypass;
        this.keyName = keyName;
        this.jarsignerPath = jarsignerPath;

        this.basePath = new File("").getAbsolutePath();
    }

    public void mySplit() {
        getCannelFile();// 獲得自定義的渠道號

        modifyChannel(); // 開始打包

    }

    /**
     * 修改渠道字段
     */
    public void modifyChannel() {

        // 1, 將該App 反編譯
        String cmdUnpack = "cmd.exe /C java -jar apktool.jar d -f -s "
                + apkName;
        runCmd(cmdUnpack);

        System.out.println("*********反編譯Apk 成功 ***********");

        // 2, 移動清單文件,作為備份

        // 獲取編譯後後的目錄名 和目錄文件
        String decodeDir = apkName.split(".apk")[0];

        // 
        File decodeDirFile = new File(decodeDir);

        // 獲取清單文件
        String maniPath = decodeDirFile.getAbsolutePath()
                + "\\AndroidManifest.xml";

        // 獲取備份清單文件目錄 工程根目錄
        String maniPathSave = basePath + "\\AndroidManifest_back.xml";

        // 備份清單文件
        new File(maniPath).renameTo(new File(maniPathSave));
        System.out.println("*********備份清單文件 ***********");

        for (int i = 0; i < channelList.size(); i++) {

            System.out.println("*********開始搞----"+channelList.get(i)+" ***********");
            // 獲取備份文件的內容,修改渠道值,並保存到maniPath 中
            updateChannel(maniPathSave, maniPath, channelList.get(i));
            System.out.println("*********修改清單文件,替換清單文件成功 ***********");

            // 重新打包
            String cmdPack = String.format(
                    "cmd.exe /C java -jar apktool.jar b %s %s", decodeDir,
                    apkName);

            runCmd(cmdPack);

            System.out.println("*********4,打包成功,開始重新簽名 ***********");

            // 簽名文件地址
            String keyStorePath = basePath + "\\" + keystoreName;

            // 未簽名的apk 地址
            String unsign_apk_path = decodeDir + "\\dist\\" + apkName;

            // 簽名後的apk
            String sign_apk_path = basePath + "\\" + channelList.get(i)
                    + ".apk";

            String signCmd = jarsignerPath
                    + " -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore "
                    + keyStorePath + " -storepass " + storepass + " -keypass "
                    + keypass + " -signedjar " + sign_apk_path + " "
                    + unsign_apk_path + " " + keyName;

            runCmd(signCmd);

            System.out.println("*********5," + channelList.get(i)
                    + "簽名成功***********");
        }
    }

    /**
     * 修改渠道值
     * 
     * @param sourcePath
     *            備份清單文件地址
     * @param targetPath
     *            目標清單文件地址
     * @param channelStr
     *            要求該的渠道值
     */
    public void updateChannel(String sourcePath, String targetPath,
            String channelStr) {

        BufferedReader br = null;
        FileReader fr = null;
        FileWriter fw = null;
        try {
            // 從備份中讀取內容
            fr = new FileReader(sourcePath);
            br = new BufferedReader(fr);
            String line = null;
            StringBuffer sb = new StringBuffer();
            while ((line = br.readLine()) != null) {
                // 如果某一行存在qwertyy 則替換該值
                if (line.contains("qwertyy")) {
                    line = line.replaceAll("qwertyy", channelStr);
                }
                sb.append(line + "\n");
            }
            // 寫到目標清單文件
            fw = new FileWriter(targetPath);
            fw.write(sb.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fr != null) {
                    fr.close();
                }
                if (br != null) {
                    br.close();
                }
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 獲取渠道字段
     */
    private void getCannelFile() {
        File file = new File("channel.txt");
        // 如果文件不存在,則提示
        if (file.exists() && file.isFile()) {
            BufferedReader br = null;
            FileReader fr = null;
            try {

                // 獲取到渠道的輸入流
                br = new BufferedReader(new FileReader(file));

                String line = null;

                while ((line = br.readLine()) != null) {
                    // 獲取到渠道
                    channelList.add(line.trim());
                }

                System.out.println(channelList);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fr != null) {
                        fr.close();
                    }
                    if (br != null) {
                        br.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("*********獲取渠道成功 ***********");
        } else {
            System.err
                    .println("*********error: channel.txt文件不存在,請添加渠道文件***********");
        }
    }

    /**
     * 執行控制台指令
     * 
     * @param cmd
     */
    public void runCmd(String cmd) {
        Runtime rt = Runtime.getRuntime();
        BufferedReader br = null;
        InputStreamReader isr = null;
        try {
            // 執行
            Process p = rt.exec(cmd);
            // 獲取對應流,一遍打印控制台輸出的信息
            isr = new InputStreamReader(p.getInputStream());
            br = new BufferedReader(isr);
            String msg = null;
            while ((msg = br.readLine()) != null) {
                System.out.println(msg);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (isr != null) {
                    isr.close();
                }
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

工具類編寫好了,就看怎麼使用了


    public static void main(String[] args) {// 這裡用cmd傳入參數用

        // apk的名字
        String apkName = "test.apk";

        //簽名文件
        String keystoreName = "alex.keystore";

        // 初始密碼
        String storepass = "123456";

        // 名字
        String keyName = "test";

        // 密碼
        String keypass = "111111";



        // 簽名工具的地址    去jdk 中找 --- 需要修改
        String jarsignerPath = "C:\\Program Files\\Java\\jdk1.8.0_91\\bin\\jarsigner.exe";



        // 開始打包
        new SplitApk(apkName,keystoreName,storepass,keyName,keypass,jarsignerPath).mySplit();

    }

這樣就可以了,運行該方法之後,刷新工程,就可以看到在工程目錄下我們打好的包

這裡寫圖片描述

獲取渠道值

可能會存在,如果我們不用友盟的話,肯定我們自己要獲取到標簽值,以便做操作。在這裡仍用友盟舉例:

在這裡貼出清單文件的application的內容


        
            
                
                
            
        

        
        
    
在我們程序自定義的Application中獲取
        ApplicationInfo appInfo;
        try {
            appInfo = this.getPackageManager().getApplicationInfo(
                    getPackageName(), PackageManager.GET_META_DATA);

        msg = appInfo.metaData.getString("UMENG_CHANNEL");
        } catch (NameNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

OVER !!!!.

該工程源碼已經上傳到github,有需要者請移步。

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