編輯:關於Android編程
最近看到公司IOS的同事做了一個app打包工具給QA使用,極大的方便了QA的工作,也給開發節省了不少精力,不需要頻繁的接收QA的要求給QA打包新app做測試,防止編程思路被打包這些瑣事給打斷。
為了編寫方便和跨平台應用,我使用了網頁版的交互方式,使用tomcat 8做服務器,這樣可以讓任意一台手機和電腦通過浏覽器就可以輕松的打包然後收到相應的.app文件,界面大概是這個樣子
主要的功能是這樣的
1、可以自由切換分支,分支號通過下拉列表的形式顯示在網頁上
2、可以自由切換服務器環境,比如測試服,開發服,正式服等等,上圖的staging,dev,live就屬於服務器
3、可以自動拉取git分支最新代碼
4、可以自動進行apk的簽名,apk對齊
大體思路是這樣:
1、關於切換分支
因為不同git分支的上的代碼不一樣,有些依賴庫也不一樣,通過git命令行直接克隆分支然後用gradle編譯是不行的,因為直接克隆下來的代碼,尤其是一些iml配置文件在本機是不能直接用的,而且有些依賴庫是以com.google.xxx.xxx這樣的方式寫在gradle.xml裡的,這些依賴庫本身需要聯網進行下載,另外不同的分支用的gradle版本也不一樣,需要下載對應的gradle,所以使用一個git 倉庫通過check分支來切換這種方式行不通。所以我使用的多個git倉庫,每個git倉庫放一個分支的代碼,切換分支實際上就是通過切換不同的git倉庫實現的。這樣做之後,下載依賴庫和重寫iml文件就交給Android Studio來進行,說白了就是克隆完一個分支之後,先用Android Studio先clean一遍,再用AS打一個apk,這樣這個git倉庫就和本機的配置契合,可以被命令行打包了。
2、關於切換環境
因為這個項目裡服務器的ip地址寫死在了.java文件裡面,所以只要修改相關java文件裡面的ip地址就可以實現服務器的切換。所以我在這裡是將各個寫有不同ip地址的java文件放在git倉庫之外,當打包時根據要打包的環境動態替換項目目錄中的.java文件實現環境的切換。
3、關於pull代碼,簽名,對齊,這些通過直接的git命令和gradle命令執行就好了
4、關於進度的提示:
因為gradle在編譯的時候對CPU和內存的開銷很大,所以一次只能有一個編譯進程執行,所以我就把進度直接用一個靜態保存了,獲取進度直接獲取這個靜態的變量的值就行。
在web端就做的很簡單,用http請求每隔兩秒進行輪詢,沒有采用高大尚的socket通訊。
先來復習一下gradle相關的命令行指令吧
打包指令:
首先cd到項目根目錄下,就是有gradlew.bat這個文件的那個目錄,然後執行(gradle命令比Eclipse打包容易多了)
gradle bulid 或 gradlew build 或 gradlew clean build
制作簽名文件指令:
keytool -genkeypair -alias mykeyName -keyalg RSA -validity 100 -keystore mydemo.keystoremyKeyName是生成簽名文件的別名,非常重要,100是有效期,mydemo.keystore是簽名文件的文件名,執行完這一條指令會讓你輸入一個密碼,注意區別密碼和前面“alias”(別名)的區別,不要搞混,當時我就搞混了然後浪費了很多時間,下面三張圖顯示了alias和password的在eclipse和Android Studio打包簽名時的截圖,幫助你區別alias和password
對apk進行簽名:
jarsigner -verbose -keystore E:\QA\file\key -signedjar app-signed.apk app-release-unsigned.apk mkeyName
首先cd到存放gradle編譯好的未簽名的apk的目錄下
E:\QA\file\key是簽名文件的文件路徑,app-signed.apk是簽好名之後生成apk的文件名,app-release-unsigned.apk是當前文件夾下未簽名apk的文件名mkeyName是簽名文件的別名,輸入換行符後,控制台會提示你輸入簽名文件的密碼,如果密碼正確就會開始一個文件一個文件的進行簽名
對簽名好的文件進行對齊,傳說對齊可以讓安卓系統訪問apk包裡的資源更快,具體怎樣沒試過,我對齊apk以後發現apk的大小變大了
首先cd到簽名好的apk文件的目錄下
zipalign -f -v 4 app-signed.apk app-publish.apkapp-signed.apk是簽名好的文件,app-publish.apk是對齊好後生成的新文件
代碼時間:
首先是最核心的文件,也就是執行具體命令行指令的java文件
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.SequenceInputStream; public class PackageNow { //記錄每個分支git倉庫的地址 public static final String[] BRANCH_0 = {"E:\\QA\\master\\app3","master"};//第0個分支的相關信息,第一個是位置,第二個是分支號,第三個是編譯器位置 public static final String[] BRANCH_1 = {"E:\\QA\\branch1\\app3","QRAII-6916"};//第0個分支的相關信息,第一個是位置,第二個是分支號,第三個是編譯器位置 public static final String[] BRANCH_2 = {"E:\\QA\\branch2\\app3","QRAII-sprint8"};//第0個分支的相關信息,第一個是位置,第二個是分支號,第三個是編譯器位置 public static final String[][] BRANCHES = {BRANCH_0,BRANCH_1,BRANCH_2}; public static final String GIT_ROOT = "http://username:[email protected]:7171/android/app3.git";//外網的git 地址 public static final String APK_PATH = "\\app\\build\\outputs\\apk";//進入項目目錄後,gradle編譯完成後輸出apk的目錄 //下面是編譯完後生成apk的文件名 public static final String APP_DEBUG = "app-debug.apk";//打了默認簽名的apk public static final String APP_SIGNED = "app-signed.apk";//打了正式簽名,但是沒有4k對齊的apk名字 public static final String APP_PUBLISH = "app-publish.apk";//打了正式簽名,且對齊了的apk名字 //用於簽名apk的簽名文件的路徑 public static final String KEY_PATH = "E:\\QA\\file\\key"; //要根據不同服務器環境替換文件的路徑 public static final String LIVE_FILE = "E:\\QA\\file\\live\\ApplicationConfigurationEntity.java";//記錄live環境的java文件的路徑,准備用於替換 public static final String STAGING_FILE = "E:\\QA\\file\\staging\\ApplicationConfigurationEntity.java"; public static final String DEV_FILE = "E:\\QA\\file\\dev\\ApplicationConfigurationEntity.java"; public static final String ALPHA1_FILE = "E:\\QA\\file\\alpha1\\ApplicationConfigurationEntity.java"; public static final String ALPHA2_FILE = "E:\\QA\\file\\alpha2\\ApplicationConfigurationEntity.java"; public static final String ALPHA3_FILE = "E:\\QA\\file\\alpha3\\ApplicationConfigurationEntity.java"; public static final String ALPHA4_FILE = "E:\\QA\\file\\alpha4\\ApplicationConfigurationEntity.java"; public static final String[] environmentNames = {"Staging","Dev","Live"};//獲取每個環境的名字,用於給文件命名的,下標是環境的代號 public static final String[] ENVIRONMENTS = {STAGING_FILE,DEV_FILE,LIVE_FILE};//要根據不同環境替換文件的路徑 public static final String ENV_FILE = "\\app\\src\\main\\java\\com\\xxxx\\xxxxx\\model\\ApplicationConfigurationEntity.java";//要根據環境不同來動態被的項目裡替換的java文件 public static final int totalCount = 997;//git編譯控制台輸出的總行數,用於判斷進度 public static String progress;//記錄當前的打包進度 public static boolean isPackaging = false;//記錄帶當前是否在打包,控制同一時刻只有一個打包進程,節省cpu,內存開銷 /** * 根據分支號進行編譯 * @param branch * @return 返回值是命令行命令 */ public static String[] packageNow(int branch) {//開始打包 return new String[]{ "cd "+BRANCHES[branch][0],//cd 到項目目錄 BRANCHES[branch][0].charAt(0)+":", "gradlew build"//正式進行編譯 }; }; /** * 對apk進行簽名 * @param branch * @return 返回值是命令行命令 */ public static String[] signKey(int branch){ return new String[]{ "cd "+BRANCHES[branch][0]+APK_PATH, BRANCHES[branch][0].charAt(0)+":", "jarsigner -verbose -keystore "+KEY_PATH+" -signedjar "+APP_SIGNED+" app-release-unsigned.apk keyPassword", "imaginato"//這個是簽名文件的密碼 }; } /** * 對app進行4k對齊 * @param branch * @return 返回值是命令行命令 */ public static String[] zipAlign(int branch){ return new String[]{ "cd "+BRANCHES[branch][0]+APK_PATH, BRANCHES[branch][0].charAt(0)+":", "zipalign -f -v 4 "+APP_SIGNED+" "+APP_PUBLISH, }; } /** * pull最新代碼 * @param branch * @return 返回值是命令行命令 */ public static String[] gitPull(int branch){//pull 一個分支的代碼 return new String[]{ "cd "+BRANCHES[branch][0],//cd 到項目目錄 BRANCHES[branch][0].charAt(0)+":", "git pull "+GIT_ROOT + " "+BRANCHES[branch][1] }; }; /** * 執行一條命令行指令 * @param orders 命令行命令 * @param callBack 控制台每輸出一條反饋,會調用一次回調 * @throws IOException */ public static void runOneRow(String[] orders,ReadLineCallBack callBack) throws IOException{ Process process = Runtime.getRuntime ().exec ("cmd"); SequenceInputStream sis = new SequenceInputStream (process.getInputStream (), process.getErrorStream ()); // next command OutputStreamWriter osw = new OutputStreamWriter (process.getOutputStream ()); InputStreamReader isr = new InputStreamReader (sis, "GBK"); BufferedReader br = new BufferedReader (isr); BufferedWriter bw = new BufferedWriter (osw); for(String s : orders){ System.out.println("待執行的語句是"+s); bw.write (s); bw.newLine (); } bw.flush (); bw.close (); osw.close (); // read String line = null; while (null != ( line = br.readLine () )) { System.out.println (line+"$$"); callBack.readLine(line); } br.close (); isr.close (); process.destroy (); } /** * 控制台每次返回文本後調用的回調接口 * @author Administrator * */ public interface ReadLineCallBack{ void readLine(String line); } /** * 開始打包函數 * @param appLocation 打包完成將apk發送到哪去的文件路徑 * @param branch //分支序號 * @param environment //服務器環境序號 * @param sign //是否進行自動簽名,如果進行簽名那麼簽名完會再執行一步對齊操作 * @throws IOException */ public static void buildPackage(String appLocation,int branch,int environment,final boolean sign) throws IOException{ System.out.println("即將打包的分支號是"+branch); progress ="正在進行編譯"; final int[]rowCount_progress = {0,0};//第0個記錄當前是第幾行,第1個記錄進度百分比 File targetEnvFile = new File(BRANCHES[branch][0]+ENV_FILE);//工作空間裡的環境配置文件 File sourceEnvFile = new File(ENVIRONMENTS[environment]);//寫好環境的外頭的配置文件 copyFile(sourceEnvFile, targetEnvFile); runOneRow(packageNow(branch), new ReadLineCallBack() { public void readLine(String line) { // TODO Auto-generated method stub rowCount_progress[0]++; if(!progress.equals("編譯完成") &&!progress.equals("error:編譯失敗"))progress = "Progress:"+String.valueOf(Math.round((float)rowCount_progress[0]/(float)totalCount*100)+"%"); if(line.startsWith(":")||line.startsWith("Reading")||line.startsWith("Note"))return; if(line.startsWith("BUILD SUCCESSFUL")){ if(!sign)isPackaging = false; System.out.println("編譯成功啦"); progress = "編譯完成";//如果不需要簽名,那麼現在已經成功了 }else if(line.startsWith("BUILD FAILED")){ progress = "error:編譯失敗"; isPackaging = false; } } }); if(!progress.equals("編譯完成")){ System.out.println("沒有編譯成功"+progress); progress = "error:沒有編譯成功"; return; } System.out.println("一共有"+rowCount_progress[0]+"行"); if(sign){//如果需要簽名 System.out.println("准備進行簽名"+BRANCHES[branch][0]); runOneRow(signKey(branch), new ReadLineCallBack() { public void readLine(String line) { // TODO Auto-generated method stub if(!line.startsWith("jar 已簽名"))return; System.out.println("簽名成功"); progress = "簽名成功!!"; } }); if(!progress.equals("簽名成功!!")){//檢測簽名是否添加成功 progress = "error:簽名失敗!!!"; return; } progress = "正在進行apk對齊"; //准備進行4k對齊 runOneRow(zipAlign(branch), new ReadLineCallBack() { public void readLine(String line) { // TODO Auto-generated method stub if(line.startsWith("Unable to open")){ System.out.println("4k對齊失敗"); progress = "4k對齊失敗!!!"; return; } if(!line.equals("Verification succesful"))return; System.out.println("4k對齊成功"); progress = "4k對齊成功"; } }); if(!progress.equals("4k對齊成功")){//檢測4k對齊是否成功 progress = "error:4k對齊失敗"; return; } } progress = "正在准備傳送文件"; File sourceFile = new File(BRANCHES[branch][0]+APK_PATH+"\\"+(sign?APP_PUBLISH:APP_DEBUG));//需要簽名和不需要簽名給出的帶key的app名字不一樣 System.out.println("源文件"+sourceFile.getAbsolutePath()+sourceFile.exists()); System.out.println("源文件的大小是"+sourceFile.length()); if(sourceFile.exists() && sourceFile.isFile() && sourceFile.length()>100000){ File targetFile = new File(appLocation); if(copyFile(sourceFile, targetFile))System.out.println("文件復制成功"+targetFile.length()); progress = "succeed";//在這裡算徹底的成功 }else { progress = "error:失敗"; } isPackaging = false; } /** * 一個簡單的復制文件函數 * @param sourceFile * @param targetFile * @return */ public static boolean copyFile(File sourceFile,File targetFile){ long beginTime = System.currentTimeMillis(); if(sourceFile==null||targetFile==null)return false; System.out.println("目標地址"+targetFile.getAbsolutePath()); try { if(!targetFile.exists()){//如果目標文件不存在就新建 targetFile.createNewFile(); }else {//如果目標文件存在就刪除,然後新建一個 targetFile.delete(); targetFile.createNewFile(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } FileInputStream fis; FileOutputStream fos; try { fis = new FileInputStream(sourceFile); fos = new FileOutputStream(targetFile); } catch (FileNotFoundException e1) { // TODO Auto-generated catch block e1.printStackTrace(); return false; } byte[] b = new byte[1024]; int len = 0; try { while ((len = fis.read(b)) != -1) { fos.write(b, 0, len); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } try { fos.flush(); fis.close(); fos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } long endTime = System.currentTimeMillis(); System.out.println("采用傳統IO FileInputStream 讀取,耗時:"+ (endTime - beginTime)); return true; } }
import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.imaginato.tools.PackageNow; /** * Servlet implementation class PackageOL */ public class PackageOL extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public PackageOL() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub System.out.println("有人訪問在線打包servlet"+PackageNow.isPackaging); int branch = 0;//分支選擇 int environment = 0;//環境選擇 boolean needSign = false; try { branch = new Integer(request.getParameter("branch")); environment = new Integer(request.getParameter("environment")); if(request.getParameter("needSign")!=null && request.getParameter("needSign").length()>0)needSign = true; } catch (Exception e) { // TODO: handle exception e.printStackTrace(); System.out.println("沒有傳來分支號!!!"); return; } System.out.println("當前選擇分支"+branch+" 環境"+environment+" 是否簽名"+needSign); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd"); String fileName = "AppII"+PackageNow.environmentNames[environment]+"_"+dateFormat.format(new Date(System.currentTimeMillis())).concat(".apk");//QravedIIStaging_20160508.apk String appLocation = getServletConfig().getServletContext().getRealPath("/").concat("release\\").concat(fileName);//將最終要給客戶的文件的服務器路徑發給打包類,讓打包類打包完以後將文件復制到這個目錄下 PrintWriter out = response.getWriter(); if(PackageNow.isPackaging){ System.out.println("現在正在打包"); out.write("packaging"); out.flush(); out.close(); return; }else { out.write(fileName);//將准備放過去的文件名返回 } out.flush(); out.close(); PackageNow.isPackaging = true; PackageNow.buildPackage(appLocation,branch,environment,needSign); } }下面貼出web頁面的HTML代碼,裡面包含了布局和ajax請求,這裡發給服務器的分支號是發的序號,環境也是序號,這個序號就是上面PackageNow.java裡面靜態字符串數組的下標
<!DOCTYPE html> <html> <head> <title>index.html</title> <meta name="keywords" content="keyword1,keyword2,keyword3"> <meta name="description" content="this is my page"> <meta name="content-type" content="text/html"; charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="./js/bootstrap.min.js"></script> <link rel="stylesheet" type="text/css" href="./css/bootstrap.min.css">--> </head> <body> <div class="container-fluid"> <div class="jumbotron"> <h1>歡迎使用Qraved Android HTML5 自動打包工具!</h1> <p></p> <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a></p> </div> <div style="margin-left: auto;margin-right: auto"> <div class="checkbox"> <label> <input id = "autoSign" type="checkbox"> 自動簽名 </label> </div> <div class="dropdown"> <button id=currentBranch class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> 當前分支:sprint8 </button> <ul class="dropdown-menu" aria-labelledby="dropdownMenu1"> <li class=branch code=0><a href="#">master</a></li> <li class=branch code=1><a href="#">sprint7</a></li> <li class=branch code=2><a href="#">sprint8</a></li> </ul> </div> <div class="btn-group" data-toggle="buttons"> <label class="btn btn-primary active env"code="0"> <input type="radio" name="options" id="option1" autocomplete="off" checked> staging </label> <label class="btn btn-primary env"code="1"> <input type="radio" name="options" id="option2" autocomplete="off"> dev </label> <label class="btn btn-primary env" code="6"> <input type="radio" name="options" id="option4" autocomplete="off"> live </label> </div> <div> <span id="process">准備打包</span> </div> <div> <button type="button" class="btn btn-primary" id=pull>拉取最新代碼</button> <button type="button" class="btn btn-success" id="startPackage">開始打包</button> <button type="button" class="btn btn-info" id="download">下載apk</button> </div> </div> </div> </body> <script type="text/javascript"> var showDialog = false;//這個是當前頁面是否已經顯示過一遍打板成功 var choosedBranch = "2";//這個記錄當前選定的分支號,有三種選擇,0,1,2,默認為2 var environment = "0"; var autoSign = false; var isPulling = false; $("#startPackage").click(function(){ console.log("點擊了開始打包"); $.get("./PackageOL", "branch="+choosedBranch+"&environment="+environment+(autoSign?"&needSign=true":""), function(data, textStatus, req) { console.log("發來的data=="+data); if(data=="packaging"){ $("#process").text("正在打包中,請稍等"); window.alert("當前有其他用戶正在打板,請稍候") return; }else if(data=="0"){ $("#process").text("准備就緒"); }else{ $("#process").text("開始打包"); showDialog = false; $("#download").unbind("click").click(function() { console.log("點擊了下載"); window.open("./release/"+data);//正常的話,返回的data是文件名 }); } }); var task = window.setInterval(function() { console.log("發送獲取進度的請求"); $.get("./PackageProcess", "", function(data, textStatus, req) { console.log("ajax返回data是"+data); if(data=="succeed"){ $("#process").text("打包成功,請點擊下載"); if(showDialog==false){ window.alert("打包成功,請點擊下載"); showDialog = true; } window.clearInterval(task); return; }else{ if(data.substr(0, 5)=="error"){ window.clearInterval(task); showDialog = true; window.alert("打包失敗,原因"+data); } $("#process").text(data); } }); }, 2500); }); //下面是控制選擇分支的下啦菜單 $(".branch").click(function() { var li = this; $("#currentBranch").text("當前分支:"+$(li).text()); choosedBranch = $(li).attr("code"); console.log("當前選定"+choosedBranch); }); $("#pull").click(function() { console.log("准備pull最新代碼"+choosedBranch); if(isPulling){ window.alert("上次代碼還沒有pull完"); return; } $("#process").text("正在拉取代碼,請稍等"); isPulling = true; $.get("./PullCodeServlet", "branch="+choosedBranch, function(data, textStatus, req) { console.log("pull代碼的data是"+data); if(data=="succeed")window.alert("pull 代碼成功"); else window.alert("pull 代碼失敗"); $("#process").text("代碼拉取完畢"); isPulling = false; }); }); $(".env").click(function() { var button = this; environment = $(button).attr("code"); console.log("environment是"+environment); }); $("#autoSign").click(function() { autoSign = !autoSign; }); </script> </html>
1.問題是如何發生的,會在什麼情況下發生此類問題?當用戶運用手機清理助手或後台回收我們的應用造成我們應用程序進程被殺死的時候就有可能出現這種空指針的問題,下面舉個例子我們
效果圖: 開源項目用的是Studio 開發的 ,如果用Eclipse自己手動導入就可以了,常用的方法: public sta
Fragment相當於一個小型activity,因為Fragment可以實現activity中所有的功能,不同的是Fragment可以嵌入activity,一個activ
本實例通過MediaPlayer播放一首音樂並通過AudioManager控制手機音頻,關於AudioManager的詳解可參照:Android開發之AudioManag