在項目中用到了百度SDK統計,沒用過別的統計工具,只用了百度的感覺還不錯,最新版本新增了Fragment統計功能。應用上線三天,用各種流氓辦法下載安裝量已經超過了2800,但是留存率只有10%左右。主要原因還是產品同質化比較嚴重,沒有什麼亮點。
用到統計工具基本上就會用到渠道,分渠道打包真是件很頭疼的事情,渠道一多了之後手動打包效率非常低,而且容易出錯。所以今天花了半天時間研究了一下多渠道自動打包的方法,這樣節省了不少時間,主要不會在打包的過程中出錯了!
下面我就一步步的告訴大家怎麼自己寫一個多渠道打包工具,為什麼我不提供一個寫好的給大家下載呢?因為每個人的項目、編譯環境等等諸多因素都不相同,主要原因也是我很忙,沒有時間寫一個擴充性更好的工具,所以就在這裡講一講實現原理吧。希望有人可以看到這篇文章後寫個通用性更廣的打包工具出來。
言歸正傳。
apk打包有兩種方式ant & apktool,我看網上很多人都用ant的打包方式,但是研究了一下感覺有點小複雜,不是半天就能搞定的,所以換用apktool的方式實現自動打包。apktool是外國人寫的工具,很多反編譯軟體會用到它解包,也有一些山寨應用會用它解包打包,官方網址是http://code.google.com/p/android-apktool/,最新版本是1.5.2。apktool底層原理就是用sdk工具中的aapt實現的。
我們需要用到的工具
jdk 一般開發都有這個吧
sdk 一般開發都有這個吧(主要用到裡面的aapt,我的路徑是:sdk\build-tools\android-4.2.2\aapt.exe)
apktool 去官網下載(http://code.google.com/p/android-apktool/downloads/detail?name=apktool1.5.2.tar.bz2&can=2&q=)
有了工具就可以開始寫代碼了,實現自動打包的原理是這樣的:
1.先得到apk檔案(我是用Eclipse產生的,可以從bin檔案夾裡直接獲得,也可以打簽名包和未簽名包,只要有apk就行)
2.用apktool 解包 (java -jar apktool.jar d -f -s xxx.apk),通過這個指令就會在apktool目錄下產生一個apk同名的檔案夾,其中就包括我們要修改的AndroidManifest.xml
3.寫代碼去修改AndroidManifest.xml中對應Channel_Id的地方
4.用apktool 打包 (java -jar apktool.jar b xxx.ap xxx_us.apk),通過這個指令會產生一個未簽名的apk,注意,此指令需要依賴aapt,請在系統內容變數中引入aapt!
5.用jdk的jarsigner工具給apk簽名(指令有很多,我用的是jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore abc.keystore -signedjar xxx_s.apk xxx_us.apk abc.keystore -storepass)
好的,原理知道後,剩下的就非常簡單了,一步步去實現就可以了!
為了避免大家走彎路,我告訴大家一個方法。在寫剩下的代碼之前,請大家用apktool指令Run一遍解包、打包和簽名的一整套動作,如果可以順利跑下來,你後面寫的工具才是有意義的。我在寫工具過程中遇到一些問題都是因為這幾個指令都不能完全執行導致的,特別是因為aapt和jarsigner沒有配置環境變數。
正式開始了 1.開啟Eclipse建立Java工程,起一個自己喜歡的工程名字和包名。
2.建立一個程式入口Main.java
public class Main { public static void main(String[] args) {// 這裡用cmd傳入參數用 System.out.println("====**====By H3c=====**======"); if (args.length != 3) {// 傳入3個參數 apk報名、簽名檔案、簽名密碼 System.out .println("==ERROR==usage:java -jar rePack.jar apkName keyFile keyPasswd======"); System.out .println("==INFO==Example: java -jar rePack.jar test.apk android.keystore 123456======"); return; } String apk = args[0]; String keyFile = args[1]; String keyPasswd = args[2]; SplitApk sp = new SplitApk(apk, keyFile, keyPasswd); sp.mySplit(); }}
3.建立工具類SplitApk.java
import java.io.BufferedReader;import java.io.File;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;import java.io.InputStreamReader;import java.util.HashMap;import java.util.Map;public class SplitApk { HashMap<String, String> qudao = new HashMap<String, String>();// 渠道號,渠道名 String curPath;// 當前檔案夾路徑 String apkName; String keyFile; String keyPasswd; public SplitApk(String apkName, String keyFile, String keyPasswd) {// 建構函式接受參數 this.curPath = new File("").getAbsolutePath(); this.apkName = apkName; this.keyFile = keyFile; this.keyPasswd = keyPasswd; } public void mySplit() { getCannelFile();// 獲得自訂的渠道號 modifyXudao();// 解包 - 打包 - 簽名 } /** * 獲得渠道號 */ private void getCannelFile() { File f = new File("channel.txt");// 讀取當前檔案夾下的channel.txt if (f.exists() && f.isFile()) { BufferedReader br = null; FileReader fr = null; try { fr = new FileReader(f); br = new BufferedReader(fr); String line = null; while ((line = br.readLine()) != null) { String[] array = line.split("\t");// 這裡是Tab分割 if (array.length == 2) { qudao.put(array[0].trim(), array[1].trim());// 講渠道號和渠道名存入HashMap中 } } } 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("==INFO 1.==擷取渠道成功,一共有" + qudao.size() + "個渠道======"); } else { System.out.println("==ERROR==channel.txt檔案不存在,請添加渠道檔案======"); } } /** * apktool解壓apk,替換渠道值 * * @throws Exception */ private void modifyXudao() { // 解壓 /C 執行字串指定的命令然後終斷 String cmdUnpack = "cmd.exe /C java -jar apktool.jar d -f -s " + apkName; runCmd(cmdUnpack); System.out.println("==INFO 2.==解壓apk成功,準備移動======"); // 備份AndroidManifest.xml // 擷取解壓的apk檔案名稱 String[] apkFilePath = apkName.split("\\\\"); String shortApkName = apkFilePath[apkFilePath.length - 1]; String dir = shortApkName.split(".apk")[0]; File packDir = new File(dir);// 獲得解壓的apk目錄 String f_mani = packDir.getAbsolutePath() + "\\AndroidManifest.xml"; String f_mani_bak = curPath + "\\AndroidManifest.xml"; File manifest = new File(f_mani); File manifest_bak = new File(f_mani_bak); // 拷貝檔案 -- 此方法慎用,詳見http://xiaoych.iteye.com/blog/149328 manifest.renameTo(manifest_bak); for (int i = 0; i < 10; i++) { if (manifest_bak.exists()) { break; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } if (manifest_bak.exists()) { System.out.println("==INFO 3.==移動檔案成功======"); } else { System.out.println("==ERROR==移動檔案失敗======"); } // 建立產生結果的目錄 File f = new File("apk"); if (!f.exists()) { f.mkdir(); } /* * 遍曆map,複製manifese進來,修改後打包,簽名,儲存在對應檔案夾中 */ for (Map.Entry<String, String> entry : qudao.entrySet()) { String id = entry.getKey(); System.out.println("==INFO 4.1. == 正在產生包: " + entry.getValue() + " ======"); BufferedReader br = null; FileReader fr = null; FileWriter fw = null; try { fr = new FileReader(manifest_bak); br = new BufferedReader(fr); String line = null; StringBuffer sb = new StringBuffer(); while ((line = br.readLine()) != null) { if (line.contains("\"ads-2.0\"")) { line = line.replaceAll("ads-2.0", id); } sb.append(line + "\n"); } // 寫迴文件 fw = new FileWriter(f_mani); 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(); } } System.out.println("==INFO 4.2. == 準備打包: " + entry.getValue() + " ======"); // 打包 - 產生未簽名的包 String unsignApk = id + "_" + dir + "_un.apk"; String cmdPack = String.format( "cmd.exe /C java -jar apktool.jar b %s %s", dir, unsignApk); runCmd(cmdPack); System.out.println("==INFO 4.3. == 開始簽名: " + entry.getValue() + " ======"); // 簽名 String signApk = "./apk/" + id + "_" + dir + ".apk"; String cmdKey = String .format("cmd.exe /C jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore %s -signedjar %s %s %s -storepass %s", keyFile, signApk, unsignApk, keyFile, keyPasswd); runCmd(cmdKey); System.out.println("==INFO 4.4. == 簽名成功: " + entry.getValue() + " ======"); // 刪除未簽名的包 File unApk = new File(unsignApk); unApk.delete(); } // 刪除中途檔案 String cmdKey = String.format("cmd.exe /C rd /s/q %s", dir); runCmd(cmdKey); manifest_bak.delete(); System.out.println("==INFO 5 == 完成 ======"); } /** * 執行指令 * * @param cmd */ public void runCmd(String cmd) { Runtime rt = Runtime.getRuntime(); BufferedReader br = null; InputStreamReader isr = null; try { Process p = rt.exec(cmd); // p.waitFor(); 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(); } } }}
4.代碼寫好後就該產生jar包了: 右擊工程選擇菜單中的Export - Java - Runnable JAR file,選擇匯出路徑後就可以輸出jar了。 5.最後一步,建立一個檔案夾,放入剛編譯出的jar、apktool.jar和channel.txt,最好還有android.keystore channel.txt 格式如下,注意是tab分割,不是空格
123 外部推廣124 軟體盒子125 內部網頁top126 官方包
為什麼這裡建議放入androdi.keystore,因為如果引入外部的會報一個簽名不一致的錯誤。
6.在cmd中進入這個檔案夾後,輸入java -jar rePack.jar 檔案名稱 android.keystore 簽名密碼,就可以自動換渠道打包了,如果中途出現問題,請自己檢查apktool解打包過程和jarsigner是否會報錯,去google上搜出錯原因。
7.為了更簡單,可以寫個批處理
@echo offset /p var=請拖入apk:java -jar rePack.jar %var% android.keystore 55775577echo.&echo 請按任意鍵退出...&pause>nulexit
全文完