文档章节

Python 后台基于 PackerNg 格式动态生成 APK 渠道包

lionets
 lionets
发布于 2016/07/27 13:05
字数 1097
阅读 101
收藏 0

本文代码的原理基于 git@github.com:mcxiaoke/packer-ng-plugin.git 项目。

该项目用于向打好的 APK 包快速写入渠道信息,因为是直接在 APK 的尾巴上加数据而没有解包打包的过程,故速度较快。本文实现的是另一种需求,即后台实时给 APK 加数据,然后返回给前端下载。

比如邀请码这样没法提前写入的数据,就适合实时生成。得到的好处就是用户在注册时可以免去填邀请码这一步。

但显然这会带来带宽的压力,依旧去实现它是考虑到了实际分享应用的场景:绝大多数是在 微信、QQ 等平台内传播,都会跳转到应用宝等市场上去。因此这种实时打包流量其实很低,除非有人非要别扭着在微信里使用【在浏览器中打开】按钮来下载,否则本接口是不会被访问到的。

所以这个实现其实一点实际用处都没有 →_→

原理

APK 使用的是 ZIP 的打包格式,所以它的结尾定义了两个字段 —— Comment Len, Comment,Comment 是个变长字段,长度写在 Comment Len 里,占两个字节。

ZIP 的详细解释参见:

https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html

APK 在安装后,还会在系统里留下一个副本,安装过的应用通过读取这个副本的 Comment,就可以获得这个数据。

PackerNg

开头的 github 项目便实现了这个功能,通过 Java 和 Python 两种语言。本文重写了 Python 的版本,使其可以更改 Comment,以便实现动态加尾巴的功能。

PackerNg 的 comment 格式 是 data + data length(2 bytes) + !ZXK!。最后的 !ZXK! 是个 magic 字段,用于判断该文件是否有有效的尾巴,再通过这个字段之前的两个字节来获取偏移量,读取尾巴。

但我感觉这个字段是可选的,如果直接以偏移量结尾,我们便可以通过检查最后两个字节是不是 "\x00\x00" 来判断该文件有没有加尾巴。可能的混淆情景仅为,该文件有一个非我们加的尾巴,而这可以通过人为约束来控制。

加尾巴代码

APK_COMM_LEN = 2  # bytes
APK_PACK_FMT = '<H'  # little endian unsigned short
APK_MAGIC_WD = b'!ZXK!'
APK_MAGIC_LEN = 5
# stream 相关
APK_BASE_CHUNK_SIZE = 1024 * 1024
APK_COMM_SAFE_LEN = 1024  # 保证最后一个 apk chunk 的尾部有这么多字节


# ------------------------ APK ------------------------ #
def get_apk_comment(f):
	"""获取 apk 包尾部的 comment 字节, 含 MAGIC
	"""
	# read magic
	f.seek(-APK_MAGIC_LEN, 2)
	tail = f.read(APK_MAGIC_LEN)
	if tail[-APK_COMM_LEN:] == b'\x00\x00':
		return b''
	if tail != APK_MAGIC_WD:
		raise ValueError("invalid apk comment format, it's not ended with MAGIC.")
	# read comment len
	f.seek(-APK_MAGIC_LEN - APK_COMM_LEN, 2)
	comm_len = struct.unpack(APK_PACK_FMT, f.read(APK_COMM_LEN))[0]
	f.seek(-APK_MAGIC_LEN - APK_COMM_LEN - comm_len, 2)
	# read comment
	comment = f.read(comm_len + APK_COMM_LEN + APK_MAGIC_LEN)
	return comment


def get_apk_comm_info(f):
	"""获取 apk 包尾部的 json 数据
	"""
	comment = get_apk_comment(f)
	if not comment:
		return {}
	comm_info = comment[:-APK_COMM_LEN - APK_MAGIC_LEN]
	if comm_info:
		return json.loads(comm_info.decode())


def gen_apk_comment(info={}):
	"""通过字典生成 comm_len 和 comment,均为 bytes 类型,含 MAGIC
	"""
	if not info:
		return b'\x00\x00', b''
	comment = json.dumps(info).encode()
	comment += struct.pack('<H', len(comment))
	comment += APK_MAGIC_WD
	return struct.pack('<H', len(comment)), comment


def set_apk_comment(f, cur_comm_len=b'\x00\x00', new_comm_len=b'\x00\x00', new_comment=b''):
	"""设置 apk 包尾部的 comment 字节,全量更新
	"""
	# check cur_comm_len
	cur_comm_len = struct.unpack(APK_PACK_FMT, cur_comm_len)[0]
	if cur_comm_len == 0:
		f.seek(-APK_COMM_LEN, 2)
		if f.read(APK_COMM_LEN) != b'\x00\x00':
			cur_comm = get_apk_comment(f)
			cur_comm_len = len(cur_comm)
	# write new
	f.seek(-cur_comm_len - APK_COMM_LEN, 2)
	f.write(new_comm_len)
	f.write(new_comment)
	f.truncate()
	f.seek(0, 0)
	return f

流式加尾巴代码

因为 Comment 信息位于 APK 的尾部,而我司渠道包又非常多且迭代较快,我没有选择在服务器本地保存 APK 包,而是实时从 cdn 取数据流,然后在最后一段上进行更改,即实现成了一个代理。

def inv_apk_wrapper(url, inv_code):
	r = requests.get(url, timeout=300, stream=True)
	# 生成符合要求的 chunk_size
	content_length = json.loads(r.headers['content-length'])
	tail_length = content_length % APK_BASE_CHUNK_SIZE
	if tail_length < APK_COMM_SAFE_LEN:
		less = APK_COMM_SAFE_LEN - tail_length
		chunk_num = int(math.ceil(content_length / float(APK_BASE_CHUNK_SIZE)))
		chunk_size = APK_BASE_CHUNK_SIZE - int(math.ceil(less / (chunk_num - 1.0)))
	else:
		chunk_size = APK_BASE_CHUNK_SIZE
	new_apk_gen = inv_apk_generator(r.iter_content(chunk_size), inv_code)
	# update content-length
	new_headers = dict(r.headers)
	new_headers['content-length'] = str(content_length + len(', "inv_code": "{inv_code}"'.format(inv_code=inv_code)))
	return Response(new_apk_gen, headers=new_headers)


def inv_apk_generator(apk_iterator, inv_code):
	data = []
	while True:
		try:
			data.append(next(apk_iterator))
		except StopIteration:  # 确认到尾部, 添加 inv_code
			if data:
				content = BytesIO(data[0])
				comm_info = get_apk_comm_info(content)
				comm_info['inv_code'] = inv_code
				comm_len, comment = gen_apk_comment(comm_info)
				content = set_apk_comment(content, new_comm_len=comm_len, new_comment=comment)
				content.seek(0, 0)
				yield content.read()
			raise StopIteration
		if len(data) >= 2:
			yield data[0]
			data = data[1:]

注意这种接口因为太占 I/O,最好不要和正常业务部署在一起,或者,最好不要部署。。。

© 著作权归作者所有

共有 人打赏支持
lionets
粉丝 92
博文 101
码字总数 135303
作品 0
朝阳
程序员
私信 提问
Android渠道打包工具--packer-ng-plugin

packer-ng-plugin 是下一代Android渠道打包工具Gradle插件,支持极速打包,1000个渠道包只需要5秒钟,速度是 gradle-packer-plugin 的1000倍以上,可方便的用于CI系统集成,支持自定义输出目...

sikkx
2015/12/31
3.4K
3
Android-V1、V2签名包和快速集成美团多渠道打包(原创)

本文已独家授权 郭霖 ( guolin_blog) 公众号发布! 今天的文章主要介绍的是Android V1、V2签名包的基本概念和使用场景以及如何十分钟快速掌握美团多渠道打包。 内容概览: 1:美团多渠道打包...

小猪看流星
2018/01/29
0
0
在Linux下的批量打包apk方法

在Android项目需要发布的时候,经常需要发布多个渠道,这写渠道信息一般写在minifest.xml文件当中,这样的话,每次发布一个渠道的时候,需要去更改minifest.xml文件,这样显然不是一个程序员...

蜗牛TT
2012/08/28
0
4
Android批量打包-如何一秒内打完几百个apk渠道包

在国内Android常用渠道可能多达几十个,如: 谷歌市场、腾讯应用宝、百度手机助手、91手机商城、360应用平台、豌豆荚、安卓市场、小米、魅族商店、oppo手机、联想乐商、中兴汇天地、华为、安...

SuShine
2017/10/23
0
0
Android最新打包方式-支持V2

本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 签名机制: V1:7.0以前默认,使用META-INF目录下三个文件,MANIFEST.MF,CERT.MF,CERT.RSA来保证apk不被修改。 MANIFEST...

liuzxgeek
2017/11/09
0
0

没有更多内容

加载失败,请刷新页面

加载更多

【Mysql技术内幕】第2章 InnoDB存储引擎

2.6 InnoDB关键特性 插入缓冲 两次写 自适应哈希索引 异步IO 刷新邻接页 2.6.1 插入缓冲 通常应用程序中行记录的插入顺序是按照主键的递增顺序进行插入的,因此插入聚集索引(Primary Key)一...

HOT_POT
59分钟前
2
0
Java8 如何正确使用 Optional

原文链接:https://blog.kaaass.net/archives/764 Optional是Java8提供的为了解决null安全问题的一个API。善用Optional可以使我们代码中很多繁琐、丑陋的设计变得十分优雅。这篇文章是建立在...

大灰狼时间
今天
3
0
富兰克林的人生信条

春节假期期间读了富兰克林自传,这位饱经风霜的老人出身贫寒,只读过两年书,但是通过刻苦自学和不懈奋斗还是取得了令人难以置信的成就,他的一生可以作为我们普通人的励志典范。 富兰克林 ...

春哥大魔王的博客
今天
1
0
不用中间变量交换 a ,b(三种方法)

1、加减法:该方法可以交换整型和浮点型数值的变量,但在处理浮点型的时候有可能出现精度的损失。 a = a + b; b = a - b; a = a - b; 2、异或法:可以完成对整型变量的交换,对于浮点型变量它...

robslove
今天
6
0
一文了解 OutOfMemory 及解决方案

1. Java 堆空间 发生频率 5颗星 造成原因 无法在 Java 堆中分配对象 吞吐量增加 应用程序无意中保存了对象引用,对象无法被 GC 回收 应用程序过度使用 finalizer。finalizer 对象不能被 GC 立...

java菜分享
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部