前幾天接到個需求,如何根據一個基礎的Android App來產生100個或更多的App,要求App icon和App name都不一樣(可能還會有設定檔)。這個有點類似於為App貼上自己的標籤,但具體功能由別人提供,有點類似於OEM,下面來分析下如何? 仔細想一下其實這個就是apk的編譯和反編譯的應用,再加上個簽名(不簽名的話無法使用)。只不過是用代碼實現罷了準備工作 1、配置好Java開發環境 2、下載google提供的apk編譯和反編譯工具 (包含apktool.jar、apktool.bat、aapt.exe三個檔案) 3、下載google提供的簽名工具(包含sign.bat、signapk.jar兩個檔案)icon覆蓋和strings檔案修改 我們都知道,在Android應用中應用的icon和應用的名稱是在AndroidManifest.xml中指定的,應用程式名稱的話有可能直接寫死,但多數是這種情況
android:icon ="@drawable/ic_launcher" android:label ="@string/app_name"
我們只要覆蓋drawable-*下對應名字的icon圖片和修改values-*路徑下strings.xml中對應名字的屬性值就行了,為了簡單起見在這裡以drawable-hdpi和values-zh-rCN路徑來介紹
AndroidManifest.xml解析 通過上面的介紹,我們需要從 AndroidManifest.xml擷取icon和label兩個屬性的值,下面是一個簡單的解析類,該注意的地方都有注釋
/** * @author Tibib * */public class AndroidManifestParser { public String NS = "http://schemas.android.com/apk/res/android" ; public AppInfo parse(InputStream in) throws Exception { try { //使用pull解析庫 XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); NS = parser.getNamespace(); //設定使用 namespaces特性 parser.setFeature(XmlPullParser. FEATURE_PROCESS_NAMESPACES , true ); parser.setInput(in, "UTF-8" ); parser.nextTag(); return readAppInfo(parser); } catch (Exception e){ e.printStackTrace(); throw e; } finally { in.close(); } } private AppInfo readAppInfo(XmlPullParser parser) throws Exception{ AppInfo appInfo = new AppInfo(); while (parser.next() != XmlPullParser. END_TAG) { if (parser.getEventType() != XmlPullParser. START_TAG) { continue ; } String name = parser.getName(); // Starts by looking for the General tag if ("application" .equals(name)){ String attrLabelValue = parser.getAttributeValue( NS, "label" ); String attrIconValue = parser.getAttributeValue( NS, "icon" ); appInfo.setAppName(attrLabelValue.split( "/" )[1]); appInfo.setIconName(attrIconValue.split( "/" )[1]); } else { skip(parser); } } return appInfo; } // Skips tags the parser isn't interested in. Uses depth to handle nested tags. i.e., // if the next tag after a START_TAG isn't a matching END_TAG, it keeps going until it // finds the matching END_TAG (as indicated by the value of "depth" being 0). private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { if (parser.getEventType() != XmlPullParser. START_TAG) { throw new IllegalStateException(); } int depth = 1; while (depth != 0) { switch (parser.next()) { case XmlPullParser. END_TAG: depth--; break ; case XmlPullParser. START_TAG: depth++; break ; } } }}
修改strings.xml中name屬性為app_name(具體名稱看配置)的值
/** * @author Tibib * */public class XmlModifyUtil { /** * 使用的是 jdom庫 */ public static void modifyXML(File modifyXmlFile, String appNameAttrValue, String appNameText) { OutputStreamWriter bos = null ; try { SAXBuilder builder = new SAXBuilder(); if (modifyXmlFile.exists()) { Document document = (Document) builder.build(modifyXmlFile); Element root = document.getRootElement(); List<Element> stringChildList = root.getChildren( "string"); for (Element element : stringChildList) { String nameAttrValue = element.getAttribute("name" ) .getValue(); if (nameAttrValue.equals(appNameAttrValue)) { element.setText(appNameText); } } String xmlFileData = new XMLOutputter().outputString(document); // strings.xml預設是UTF-8格式 bos = new OutputStreamWriter( new FileOutputStream(modifyXmlFile), "UTF-8" ); bos.write(xmlFileData); bos.flush(); } else { System. out .println("File does not exist" ); } } catch (Exception ex) { ex.printStackTrace(); } finally { if (bos != null ) { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } } }}
執行編譯和簽名命令
我把反編譯和簽名工具都放在了同一目錄,並且事先把基礎apk反編譯好,現在只需要用代碼來執行編譯和簽名命令就行了。在Java中可以通過Runtime類來執行DOS命令
private static void createApk(String apkName) throws IOException, InterruptedException { File dir = new File(wpPath ); // 編譯命令,其中azbz是基礎apk反編譯後的檔案夾 String backCommand = "cmd /c apktool.bat b azbz " +apkName+".apk" ; // 簽名命令 String signCommand = "cmd /c java -jar signapk.jar platform.x509.pem platform.pk8 "+apkName+ ".apk " +apkName+"_signed.apk" ; // 這個命令執行完成會產生一個未簽名的 apk Runtime backR = Runtime. getRuntime(); Process backP = backR.exec(backCommand, null , dir); // 等待執行完再往下執行 backP.waitFor(); // 簽名 apk, 這裡使用的google提供的認證 Runtime signR = Runtime. getRuntime(); Process signP = signR.exec(signCommand, null , dir); signP.waitFor(); }
下面是隨手寫的一個產生兩個icon和名稱不同的Apk例子
public class ExecDosCommand { static String wpPath_app = "E:" +File. separator+ "decode apk"+File. separator+ "azbz" +File.separator ; static String iconPath = wpPath_app +"res" +File. separator+ "drawable-hdpi"+File. separator ; static String stringPath = wpPath_app +"res" +File. separator+ "values-zh-rCN"+File. separator +"strings.xml" ; static String manifestPath = wpPath_app+ "AndroidManifest.xml"; static String wpPath = "E:" + File. separator + "decode apk"+File. separator; public static void main(String[] args) throws Exception { AndroidManifestParser parser = new AndroidManifestParser(); AppInfo appInfo = parser.parse( new FileInputStream( manifestPath)); for (int i = 0; i < 2; i++) { coverIcon(appInfo, i); modifyAppName(appInfo, i); createApk( "修改"+(i+1)); } } private static void modifyAppName(AppInfo appInfo, int i) { XmlModifyUtil. modifyXML( new File( stringPath ), appInfo.getAppName(), "修改" +(i+1)); } private static void coverIcon(AppInfo appInfo, int i) throws FileNotFoundException, IOException { BufferedOutputStream bos = new BufferedOutputStream( new FileOutputStream(iconPath +appInfo.getIconName()+ ".png")); BufferedInputStream bis = new BufferedInputStream( new FileInputStream(wpPath +File. separator+ "image"+File. separator +"icon" +(i+1)+".png" )); byte [] buffer = new byte[1024]; int temp = 0; while ((temp = bis.read(buffer)) != -1 ){ bos.write(buffer, 0, temp); } bos.flush(); bos.close(); bis.close(); } private static void createApk(String apkName) throws IOException, InterruptedException { File dir = new File(wpPath ); // 編譯命令 String backCommand = "cmd /c apktool.bat b azbz " +apkName+".apk" ; // 簽名命令 String signCommand = "cmd /c java -jar signapk.jar platform.x509.pem platform.pk8 "+apkName+ ".apk " +apkName+"_signed.apk" ; // 這個命令執行完成會產生一個未簽名的 apk Runtime backR = Runtime .getRuntime(); Process backP = backR.exec(backCommand, null , dir); // 等待執行完再往下執行 backP.waitFor(); // 簽名 apk, 這裡使用的google提供的認證 Runtime signR = Runtime .getRuntime(); Process signP = signR.exec(signCommand, null , dir); signP.waitFor(); }}