正在開發的APP須要記錄業務員與客戶的綁定關係。具體應用場景以下: html
由流程圖可知,並無用戶填寫業務人員信息這一步,所以在用戶下載的APP中就已經攜帶了業務人員的信息。java
因爲業務人員衆多,不可能針對於每個業務人員單獨生成一個安裝包,因而就有了動態修改APP安裝包的想法。android
Android
使用的apk
包的壓縮方式是zip
,與zip
有相同的文件結構(zip
文件結構見zip
文件格式說明),在zip
的EOCD
區域中包含一個Comment
區域。安全
若是咱們可以正確修改該區域,就能夠在不破壞壓縮包、不從新打包的前提下快速給apk
文件寫入本身想要的數據。app
apk
默認狀況下沒有Comment
,因此Comment length
的short
兩個字節爲0
,咱們須要把這個值修改成咱們的Comment
長度,並把Comment
追加到後面便可。dom
@RequestMapping(value = "/download", method = RequestMethod.GET)
public void download(@RequestParam String token, HttpServletResponse response) throws Exception {
// 獲取乾淨的apk文件
Resource resource = new ClassPathResource("app-release.apk");
File file = resource.getFile();
// 拷貝一份新文件(在新文件基礎上進行修改)
File realFile = copy(file.getPath(), file.getParent() + "/" + new Random().nextLong() + ".apk");
// 寫入註釋信息
writeApk(realFile, token);
// 若是文件名存在,則進行下載
if (realFile != null && realFile.exists()) {
// 配置文件下載
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream");
// 下載文件能正常顯示中文
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(realFile.getName(), "UTF-8"));
// 實現文件下載
byte[] buffer = new byte[1024];
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(realFile);
bis = new BufferedInputStream(fis);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
System.out.println("Download successfully!");
} catch (Exception e) {
System.out.println("Download failed!");
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
複製代碼
private File copy(String source, String target) {
Path sourcePath = Paths.get(source);
Path targetPath = Paths.get(target);
try {
return Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING).toFile();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
複製代碼
public static void writeApk(File file, String comment) {
ZipFile zipFile = null;
ByteArrayOutputStream outputStream = null;
RandomAccessFile accessFile = null;
try {
zipFile = new ZipFile(file);
// 若是已有comment,則不進行寫入操做(其實能夠先擦除再寫入)
String zipComment = zipFile.getComment();
if (zipComment != null) {
return;
}
byte[] byteComment = comment.getBytes();
outputStream = new ByteArrayOutputStream();
// comment內容
outputStream.write(byteComment);
// comment長度(方便讀取)
outputStream.write(short2Stream((short) byteComment.length));
byte[] data = outputStream.toByteArray();
accessFile = new RandomAccessFile(file, "rw");
accessFile.seek(file.length() - 2);
// 重寫comment實際長度
accessFile.write(short2Stream((short) data.length));
// 寫入comment內容
accessFile.write(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (zipFile != null) {
zipFile.close();
}
if (outputStream != null) {
outputStream.close();
}
if (accessFile != null) {
accessFile.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
其中:ide
private static byte[] short2Stream(short data) {
ByteBuffer buffer = ByteBuffer.allocate(2);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort(data);
buffer.flip();
return buffer.array();
}
複製代碼
comment
信息並寫入TextView
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.tv_world);
// 獲取包路徑(安裝包所在路徑)
String path = getPackageCodePath();
// 獲取業務員信息
String content = readApk(path);
textView.setText(content);
}
複製代碼
comment
信息public String readApk(String path) {
byte[] bytes = null;
try {
File file = new File(path);
RandomAccessFile accessFile = new RandomAccessFile(file, "r");
long index = accessFile.length();
// 文件最後兩個字節表明了comment的長度
bytes = new byte[2];
index = index - bytes.length;
accessFile.seek(index);
accessFile.readFully(bytes);
int contentLength = bytes2Short(bytes, 0);
// 獲取comment信息
bytes = new byte[contentLength];
index = index - bytes.length;
accessFile.seek(index);
accessFile.readFully(bytes);
return new String(bytes, "utf-8");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
複製代碼
其中:測試
private static short bytes2Short(byte[] bytes, int offset) {
ByteBuffer buffer = ByteBuffer.allocate(2);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(bytes[offset]);
buffer.put(bytes[offset + 1]);
return buffer.getShort(0);
}
複製代碼
comment
以後沒法安裝成功最開始遇到的就是沒法安裝的問題,一開始覺得是下載接口寫的有問題,通過屢次調試以後發現是修改完comment
以後apk
就沒法安裝了。spa
查詢谷歌官方文檔可知 線程
所以,只須要打包的時候簽名方式只選擇V1不選擇V2就行。
這個問題暫時的考慮方案是每當有下載請求就會先複製一份,將複製的文件進行修改,客戶端下載成功再刪除。
可是未作測試,不知是否會產生問題。