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


    背景:

    正在开发的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就行。

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

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

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

    思考:

    • 服务端和客户端不一样,服务端的任何请求都需要考虑线程同步问题;
    • 既然客户端可以获取到安装包,则其实也可以通过修改包名来进行业务人员信息的传递;
    • 利用该方法可以传递其他数据用来实现其他一些功能,不局限于业务人员的信息。
  • 相关阅读:
    图片像素与大小
    压缩概念及常见图片格式
    王强推荐的创业者的知识架构
    Python学习笔记
    个人成效提升方法之遗愿清单
    基于Jws的WebService项目
    使用XSSFWork创建的xlsx后缀Excel文件无法打开
    notepad++每行首尾添加内容
    数据抓取的艺术(一):Selenium+Phantomjs数据抓取环境配置
    使用PhantomJS实现网页截图服务
  • 原文地址:https://www.cnblogs.com/lanxingren/p/10656647.html
Copyright © 2020-2023  润新知