Python: 另辟蹊径实现Android多渠道打包

python scott 582℃ 0评论

要先说明的是本文说的“渠道”单指在 AndroidManifest.xml定义的一个标识字符串(如友盟统计)。在代码或者通过其他文件定义的方式殊途同归。

说起 Android 多渠道打包,真是八仙过海各显神通:有手动一个个耐心打包的,有用 AntMaven重复跑编译任务的,有用 apktool解包后再修改重打包的,有在build.gradle定义一堆 flavor的,乃至有
通过apk里 META-INF/下的空文件来定义渠道的

上述方法各有优劣,在这里就不一一赘述了。

本文要介绍的是另一种方法:
直接修改APK中的 AndroidManifest.xml

上述种种,说白了都是围绕着如何修改 AndroidManifest.xml,如何重打包或是重编译。介绍的这个方法也不外如是,只是无需重打包重编译而已。

首先得知道一点,APK中的 AndroidMainfest.xml,解压出来用文本编辑器可是不能直接打开的,它是 aapt生成的一个二进制的xml格式(被称为 AXML),得用其他工具(如apktool)先解析出来。所以问题来了,如何直接修改这个 AXML 文件?

如何修改AXML中渠道名

想要修改一个文件,你得先了解它的格式。AXML文件格式其实早已有人研究,如: 《发布C语言的Android binary XML(AXML)解析代码》《AndroidManifest Ambiguity方案原理及代码》

这里就直接引用结论了: string在AXML中是存放在 StringChunk中的; string都是UTF-16编码的;如果需要往AXML中新增string是比较麻烦的(牵一发而动全身…);为了4字节对齐string数据块末尾可能被填充数个0x00…

综上结论,可知, 定义的渠道值是 UTF-16编码的string,并且可能 被填充数个0x00。

那么为了方便后期修改,我们可以先编译的一个特殊的“占位渠道包”,这个包的渠道名是一个 占位字符串,而这个字符串在AXML占的数据块长度能 适应所有渠道名的长度。假设一个占位字符串长度16,那么它自然可以被个数小于16的任意字符串所替代,如占位字符串’abcdefghijklmnop’,渠道有’xxxx’,’abcdef789’…

通过这个特殊的渠道包,我们就能够生成所有渠道包。

通过占位渠道包生成其他渠道包

大致步骤如下:

  1. 解压这个占位渠道包A中的 AndroidManifest.xml
  2. 用真正的渠道名替换 AndroidManifest.xml中的 占位字符串
  3. 拷贝一份新的占位渠道包B,删除掉 META-INF/*AndroidManifest.xml
  4. 将修改后的 AndroidManifest.xml重压缩到新的包B中
  5. 重命名渠道包B,并签名
  6. zipalign
  7. 完成一个渠道

如果你的渠道列表实在非常的多,你大概需要用多线程来优化这个步骤吧!

优劣

这个方法的优点在于:

1. 快,比所有需要重编译代码的方法快(包括apktool重打包、gradle定义flavor等);

2. 不依赖于第三方工具(都是自己写的实现脚本,算第三方不…)

缺点在于:

1. 要重新签名(倒也算不上什么缺点);

2. 需要注意占位字符串的长度不要太短了= =

实现脚本

我实现的版本这里就不献丑了,等你来完善吧!

核心代码如下:

python 替换AXML中的字符串:

(注:修改自 https://github.com/wanchouchou/playWithAXML)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def  replace_axml_string ( axml_data ,  old_string ,  new_string ):
  '''
     axml_data: the raw bytearray readed from AndroidManifest.xml
     '''

  new_string_pack  =  axml_utf16_pack ( new_string )
  old_string_pack  =  axml_utf16_pack ( old_string )
  new_string_pack_len  =  len ( new_string_pack )
  old_string_pack_len  =  len ( old_string_pack )
  if  old_string_pack_len  <  new_string_pack_len :
  raise  ValueError ( 'new_string cannot be larger than old_string! ' )
  pos  =  0
  while  True :
  pos  =  find_pack_in_axml ( axml_data ,  old_string_pack ,  pos )
  if  pos  <  0 :
  break
  axml_data [ pos  :  pos  +  new_string_pack_len ]  =  new_string_pack [  :  new_string_pack_len ]
  delta  =  old_string_pack_len  -  new_string_pack_len
  if  delta :
  axml_data [ pos  +  new_string_pack_len :  pos  +  old_string_pack_len ]  =  bytearray ( delta )
 
 def  axml_utf16_pack ( string ):
  pack  =  bytearray ( string . encode ( 'utf-16' ))
  str_len_pack  =  struct . pack ( '<I' ,  len ( string ))
  pack [  :  2 ]  =  struct . unpack ( 'BB' ,  str_len_pack [  :  2 ])
  return  pack
 
 def  find_pack_in_axml ( axml_data ,  pack ,  start ):
  pos  =  axml_data . find ( pack ,  start ,  - 1 )
  return  pos

签名APK命令形如下:

1
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/.android/debug.keystore -storepass android -keypass android path/to/channel.apk AndroidDebugKey

完。

本文思路源于@某因幡,向这只安静的兔子致敬~~

原文:http://www.yrom.net/blog/2015/05/25/the_other_way_to_package_multi_channel_apks/

转载请注明:osetc.com » Python: 另辟蹊径实现Android多渠道打包

喜欢 (0)or分享 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址