一种动态写入apk数据的方法(用于用户关系绑定、添加渠道号等)
程序员文章站
2022-04-08 17:50:28
背景: 正在开发的APP需要记录业务员与客户的绑定关系。具体应用场景如下: 由流程图可知,并没有用户填写业务人员信息这一步,因此在用户下载的APP中就已经携带了业务人员的信息。 由于业务人员众多,不可能针对于每一个业务人员单独生成一个安装包,于是就有了动态修改APP安装包的想法。 原理: Andro ......
背景:
正在开发的app需要记录业务员与客户的绑定关系。具体应用场景如下:
由流程图可知,并没有用户填写业务人员信息这一步,因此在用户下载的app中就已经携带了业务人员的信息。
由于业务人员众多,不可能针对于每一个业务人员单独生成一个安装包,于是就有了动态修改app安装包的想法。
原理:
android使用的apk包的压缩方式是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就行。
多人同时下载抢占文件导致的线程安全问题:
这个问题暂时的考虑方案是每当有下载请求就会先复制一份,将复制的文件进行修改,客户端下载成功再删除。
但是未做测试,不知是否会产生问题。
思考:
- 服务端和客户端不一样,服务端的任何请求都需要考虑线程同步问题;
- 既然客户端可以获取到安装包,则其实也可以通过修改包名来进行业务人员信息的传递;
- 利用该方法可以传递其他数据用来实现其他一些功能,不局限于业务人员的信息。