1. 程式人生 > >一種動態寫入apk數據的方法(用於用戶關系綁定、添加渠道號等)

一種動態寫入apk數據的方法(用於用戶關系綁定、添加渠道號等)

val 遇到的問題 sig 無法 暫時 lac exception 每一個 tof

背景:

正在開發的APP需要記錄業務員與客戶的綁定關系。具體應用場景如下:

技術分享圖片

由流程圖可知,並沒有用戶填寫業務人員信息這一步,因此在用戶下載的APP中就已經攜帶了業務人員的信息。

由於業務人員眾多,不可能針對於每一個業務人員單獨生成一個安裝包,於是就有了動態修改APP安裝包的想法。

原理:

Android使用的apk包的壓縮方式是zip,與zip有相同的文件結構(zip文件結構見zip文件格式說明),在zip的EOCD區域中包含一個Comment區域。

如果我們能夠正確修改該區域,就可以在不破壞壓縮包、不重新打包的前提下快速給apk文件寫入自己想要的數據。

技術分享圖片

apk默認情況下沒有Comment,所以Comment length的short兩個字節為0,我們需要把這個值修改為我們的Comment長度,並把Comment追加到後面即可。

整體過程:

技術分享圖片

服務端實現:

實現下載接口:

 1 @RequestMapping(value = "/download", method = RequestMethod.GET)
 2 public void download(@RequestParam String token, HttpServletResponse response) throws Exception {
 3 
 4     // 獲取幹凈的apk文件
 5     Resource resource = new ClassPathResource("app-release.apk");
 6     File file = resource.getFile();
7 8 // 拷貝一份新文件(在新文件基礎上進行修改) 9 File realFile = copy(file.getPath(), file.getParent() + "/" + new Random().nextLong() + ".apk"); 10 11 // 寫入註釋信息 12 writeApk(realFile, token); 13 14 // 如果文件名存在,則進行下載 15 if (realFile != null && realFile.exists()) { 16 // 配置文件下載
17 response.setHeader("content-type", "application/octet-stream"); 18 response.setContentType("application/octet-stream"); 19 // 下載文件能正常顯示中文 20 response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(realFile.getName(), "UTF-8")); 21 22 // 實現文件下載 23 byte[] buffer = new byte[1024]; 24 FileInputStream fis = null; 25 BufferedInputStream bis = null; 26 try { 27 fis = new FileInputStream(realFile); 28 bis = new BufferedInputStream(fis); 29 OutputStream os = response.getOutputStream(); 30 int i = bis.read(buffer); 31 while (i != -1) { 32 os.write(buffer, 0, i); 33 i = bis.read(buffer); 34 } 35 System.out.println("Download successfully!"); 36 } catch (Exception e) { 37 System.out.println("Download failed!"); 38 } finally { 39 if (bis != null) { 40 try { 41 bis.close(); 42 } catch (IOException e) { 43 e.printStackTrace(); 44 } 45 } 46 if (fis != null) { 47 try { 48 fis.close(); 49 } catch (IOException e) { 50 e.printStackTrace(); 51 } 52 } 53 } 54 } 55 }

拷貝文件:

 1 private File copy(String source, String target) {
 2     Path sourcePath = Paths.get(source);
 3     Path targetPath = Paths.get(target);
 4 
 5     try {
 6         return Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING).toFile();
 7     } catch (IOException e) {
 8         e.printStackTrace();
 9     }
10     return null;
11 }

往apk中寫入信息:

 1 public static void writeApk(File file, String comment) {
 2     ZipFile zipFile = null;
 3     ByteArrayOutputStream outputStream = null;
 4     RandomAccessFile accessFile = null;
 5     try {
 6         zipFile = new ZipFile(file);
 7 
 8         // 如果已有comment,則不進行寫入操作(其實可以先擦除再寫入)
 9         String zipComment = zipFile.getComment();
10         if (zipComment != null) {
11             return;
12         }
13 
14         byte[] byteComment = comment.getBytes();
15         outputStream = new ByteArrayOutputStream();
16 
17         // comment內容
18         outputStream.write(byteComment);
19         // comment長度(方便讀取)
20         outputStream.write(short2Stream((short) byteComment.length));
21 
22         byte[] data = outputStream.toByteArray();
23 
24         accessFile = new RandomAccessFile(file, "rw");
25         accessFile.seek(file.length() - 2);
26 
27         // 重寫comment實際長度
28         accessFile.write(short2Stream((short) data.length));
29         // 寫入comment內容
30         accessFile.write(data);
31     } catch (IOException e) {
32         e.printStackTrace();
33     } finally {
34         try {
35             if (zipFile != null) {
36                 zipFile.close();
37             }
38             if (outputStream != null) {
39                 outputStream.close();
40             }
41             if (accessFile != null) {
42                 accessFile.close();
43             }
44         } catch (Exception e) {
45             e.printStackTrace();
46         }
47     }
48 }

其中:

1 private static byte[] short2Stream(short data) {
2     ByteBuffer buffer = ByteBuffer.allocate(2);
3     buffer.order(ByteOrder.LITTLE_ENDIAN);
4     buffer.putShort(data);
5     buffer.flip();
6     return buffer.array();
7 }

客戶端實現:

獲取comment信息並寫入TextView:

 1 @Override
 2 protected void onCreate(Bundle savedInstanceState) {
 3     super.onCreate(savedInstanceState);
 4     setContentView(R.layout.activity_main);
 5 
 6     TextView textView = findViewById(R.id.tv_world);
 7 
 8     // 獲取包路徑(安裝包所在路徑)
 9     String path = getPackageCodePath();
10     // 獲取業務員信息
11     String content = readApk(path);
12 
13     textView.setText(content);
14 }

讀取comment信息:

 1 public String readApk(String path) {
 2     byte[] bytes = null;
 3     try {
 4         File file = new File(path);
 5         RandomAccessFile accessFile = new RandomAccessFile(file, "r");
 6         long index = accessFile.length();
 7 
 8         // 文件最後兩個字節代表了comment的長度
 9         bytes = new byte[2];
10         index = index - bytes.length;
11         accessFile.seek(index);
12         accessFile.readFully(bytes);
13 
14         int contentLength = bytes2Short(bytes, 0);
15 
16         // 獲取comment信息
17         bytes = new byte[contentLength];
18         index = index - bytes.length;
19         accessFile.seek(index);
20         accessFile.readFully(bytes);
21 
22         return new String(bytes, "utf-8");
23     } catch (FileNotFoundException e) {
24         e.printStackTrace();
25     } catch (IOException e) {
26         e.printStackTrace();
27     }
28     return null;
29 }

其中:

1 private static short bytes2Short(byte[] bytes, int offset) {
2     ByteBuffer buffer = ByteBuffer.allocate(2);
3     buffer.order(ByteOrder.LITTLE_ENDIAN);
4     buffer.put(bytes[offset]);
5     buffer.put(bytes[offset + 1]);
6     return buffer.getShort(0);
7 }

遇到的問題:

修改完comment之後無法安裝成功:

最開始遇到的就是無法安裝的問題,一開始以為是下載接口寫的有問題,經過多次調試之後發現是修改完comment之後apk就無法安裝了。

查詢谷歌官方文檔可知

技術分享圖片

因此,只需要打包的時候簽名方式只選擇V1不選擇V2就行。

多人同時下載搶占文件導致的線程安全問題:

這個問題暫時的考慮方案是每當有下載請求就會先復制一份,將復制的文件進行修改,客戶端下載成功再刪除。

但是未做測試,不知是否會產生問題。

思考:

  • 服務端和客戶端不一樣,服務端的任何請求都需要考慮線程同步問題;
  • 既然客戶端可以獲取到安裝包,則其實也可以通過修改包名來進行業務人員信息的傳遞;
  • 利用該方法可以傳遞其他數據用來實現其他一些功能,不局限於業務人員的信息。

一種動態寫入apk數據的方法(用於用戶關系綁定、添加渠道號等)