欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

一种动态写入apk数据的方法(用于用户关系绑定、添加渠道号等)

程序员文章站 2022-04-08 17:50:28
背景: 正在开发的APP需要记录业务员与客户的绑定关系。具体应用场景如下: 由流程图可知,并没有用户填写业务人员信息这一步,因此在用户下载的APP中就已经携带了业务人员的信息。 由于业务人员众多,不可能针对于每一个业务人员单独生成一个安装包,于是就有了动态修改APP安装包的想法。 原理: Andro ......

背景:

正在开发的app需要记录业务员与客户的绑定关系。具体应用场景如下:

一种动态写入apk数据的方法(用于用户关系绑定、添加渠道号等)

由流程图可知,并没有用户填写业务人员信息这一步,因此在用户下载的app中就已经携带了业务人员的信息。

由于业务人员众多,不可能针对于每一个业务人员单独生成一个安装包,于是就有了动态修改app安装包的想法。

原理:

android使用的apk包的压缩方式是zip,与zip有相同的文件结构(zip文件结构见),在zip的eocd区域中包含一个comment区域。

如果我们能够正确修改该区域,就可以在不破坏压缩包、不重新打包的前提下快速给apk文件写入自己想要的数据。

一种动态写入apk数据的方法(用于用户关系绑定、添加渠道号等)

apk默认情况下没有comment,所以comment length的short两个字节为0,我们需要把这个值修改为我们的comment长度,并把comment追加到后面即可。

整体过程:

一种动态写入apk数据的方法(用于用户关系绑定、添加渠道号等)

服务端实现:

实现下载接口:

 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就无法安装了。

查询可知

一种动态写入apk数据的方法(用于用户关系绑定、添加渠道号等)

因此,只需要打包的时候签名方式只选择v1不选择v2就行。

多人同时下载抢占文件导致的线程安全问题:

这个问题暂时的考虑方案是每当有下载请求就会先复制一份,将复制的文件进行修改,客户端下载成功再删除。

但是未做测试,不知是否会产生问题。

思考:

  • 服务端和客户端不一样,服务端的任何请求都需要考虑线程同步问题;
  • 既然客户端可以获取到安装包,则其实也可以通过修改包名来进行业务人员信息的传递;
  • 利用该方法可以传递其他数据用来实现其他一些功能,不局限于业务人员的信息。