背景:

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

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

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

原理:

Android使用的apk包的压缩方式是zip,与zip有相同的文件结构(zip文件结构见zip文件格式说明),在zip的EOCD区域中包含一个Comment区域。

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

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

整体过程:

服务端实现:

实现下载接口:

 @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;
}

往apk中写入信息:

 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();
}
}
}

其中:

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

查询谷歌官方文档可知

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

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

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

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

思考:

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

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

  1. 利用JavaScript数组动态写入HTML数据节点

    如果想要使用数组来写入HTML数据,绝对需要的是一个Key值,由Key来引导遍历数组各项:此外,使用DOM原生方法写入文档,用同一个CSS样式渲染它们,这样可以极大地减少开发时间和减少维护成本,此方法 ...

  2. MATLAB读取写入文本数据最佳方法 | Best Method for Loading & Saving Text Data Using MATLAB

    MATLAB读取文件有很多方法.然而笔者在过去进行数据处理中,由于函数太多,相互混杂,与C#,Python等语言相比,反而认为读取文本数据比较麻烦.C#和Python等高级语言中,对于大部分的文本数据 ...

  3. 按键精灵如何调用Excel及按键精灵写入Excel数据的方法教程---入门自动操作表格

    首先来建立一个新的Excel文档,在桌面上点击右键,选择[新建]-[Excel工作表],命名为[新手学员]. 现在这个新Excel文档是空白的,我们接下来会通过按键精灵的脚本来打开并写入一些数据.打开 ...

  4. OpenCV几种访问cv::Mat数据的方法

    一般来说,如果是遍历数据的话用指针ptr比用at要快.特别是在debug版本下.因为debug中,OpenCV会对at中的坐标检查是否有溢出,这是非常耗时的. 代码如下 #include <op ...

  5. thinkphp添加数据 add()方法

    thinkphpz内置的add()方法用于向数据库表添加数据,相当于SQL中的INSERT INTO 行为添加数据 add 方法是 CURD(Create,Update,Read,Delete / 创 ...

  6. C#中在WebClient中使用post发送数据实现方法

    很多时候,我们需要使用C#中的WebClient 来收发数据,WebClient 类提供向 URI 标识的任何本地.Intranet 或 Internet 资源发送数据以及从这些资源接收数据的公共方法 ...

  7. Python 使用 xlwings 往 excel 中写入一行数据的两种方法

    该方法跟上一篇写入一列的方法相反,代码如下: # -*- coding:utf-8 -*- import xlwings as xw list1 = [1,2,3,4,5] list2 = [[1], ...

  8. 用jquery解析JSON数据的方法以及字符串转换成json的3种方法

    用jquery解析JSON数据的方法,作为jquery异步请求的传输对象,jquery请求后返回的结果是 json对象,这里考虑的都是服务器返回JSON形式的字符串的形式,对于利用JSONObject ...

  9. iOS中常用的四种数据持久化方法简介

    iOS中常用的四种数据持久化方法简介 iOS中的数据持久化方式,基本上有以下四种:属性列表.对象归档.SQLite3和Core Data 1.属性列表涉及到的主要类:NSUserDefaults,一般 ...

随机推荐

  1. 从mysql中拿到的数据构造为列表

    最近测试接口遇到一个问题,用python2.7从mysql中取到的数据是元祖类型的,元祖内部的元素也是一个元祖(并且部分元素的编码格式是unicode的): 类似这样: ((10144, u''), ...

  2. HTTP/2 简介

    支撑现有 Web 服务的 HTTP 协议距离其发布时的 1997 年已经有些年月了,随后的 HTTP/1.1 版本发布自 1999 年.随着技术的进步和需求的进化,对于数据快速高效地传输,HTTP/1 ...

  3. SpringCloud学习系列之五-----配置中心(Config)和消息总线(Bus)完美使用版

    前言 在上篇中介绍了SpringCloud Config的使用,本篇则介绍基于SpringCloud(基于SpringBoot2.x,.SpringCloud Finchley版)中的分布式配置中心( ...

  4. .Net 反射学习

    Why?为什么使用反射 MVC ORM EF 都是用的反射.反射可以让程序的扩展性,灵活性得到加强.一起即可动态创建   what 反射原理    动态加载类库 ,先添加引用类库,或者复制debug里 ...

  5. SpaceSyntax【空间句法】之DepthMapX学习:第一篇 数据的输入 与 能做哪些分析

    两部分,1需要喂什么东西给软件,2它能干什么(输出什么东西在下一篇讲) 博客园/B站/知乎/CSDN @秋意正寒 转载请在头部附上源地址 目录:https://www.cnblogs.com/onsu ...

  6. 全球第一免费开源ERP Odoo Ubuntu最佳开发环境独家首发分享

    起源 近年来随着国内的互联网经济的快速腾飞,诞生了很多开源软件创造的市场价值以及企业价值神话,特别是对于企业ERP领域,一直以来都是高昂的国内外产品充实,国内的中小成长型企业越来越需要一套好看又能打, ...

  7. 企业自主可控免费开源ERP:Odoo采购管理解决方案

    供应商基础资料 1. 所有的供应商基础资料,Odoo开账启用时,期初的客户数据如果大于200条,可以批量导入: 2. 点“采购/采购/供应商”菜单可以查看.编辑修改.搜索所有的供应商基础资料: 3. ...

  8. 联发科Helio P90(mt6779),P70(mt6775),P60(MT6771),P35,P22(MT6762)芯片参数规格

    Helio P90(mt6779)是一款人工智能处理平台,集成了超级强大的AI专核APU 2.0,具有超强的AI性能和一系列基于人工智能的成像升级.该芯片将重新定义消费者对智能手机AI功能的体验.He ...

  9. lunix脚本进程挂掉时显示cpu和内存信息及挂掉的时间

    #!/bin/shwhile [ true ]; do #查询是否有8899正在运行的进程netstat -an|grep 8899if [ $? -ne 0 ]thennowtime=$(date ...

  10. 再谈AbstractQueuedSynchronizer1:独占模式

    关于AbstractQueuedSynchronizer JDK1.5之后引入了并发包java.util.concurrent,大大提高了Java程序的并发性能.关于java.util.concurr ...