背景
随着业务的快速迭代增长,京东主站APP不断加入新的代码、图片资源、第三方SDK、React Native等等,直接导致APK体积不断增加。APK体积增长也会带来诸多问题,例如:推广费用增加,用户下载意愿降低,流量费用增加,下载及安装成功率降低,甚至可能会影响用户留存率;应用市场限制,Google Play规定安装包上限为100M。所以APK瘦身已经是迫在眉睫的事情。在尝试瘦身过程中,我们借鉴了很多业界其他同行的方案,比如资源混淆、图片压缩/转码等,同时针对自己需求发现了一些新的技巧。本文主要讲解如何使用Python对APK进行分析,统计基础数据,分析可优化的资源,为应用瘦身提供数据支持。
分析APK的前提条件就是要充分了解APK组成,所以下文将首先简单介绍APK组成。
APK文件目录
APK是一个压缩包,使用aapt l file.apk命令可以查看APK下所有文件,如下:
简单归类如下 :
当然还会有一些其他文件,例如org/,src/,aidl等等文件或文件夹,这些资源是Java Resources,具体详情可以了解下APK打包流程。
在充分了解APK组成部分后,下面来介绍下APK扫描实现主要工作。
APK分析主要工作
分析APK主要分为以下部分:
1)下载APK以及mapping文件。
2)AAPT获取APK信息。
3)读取APK在操作系统中大小(apk_file_size)以及APK真正大小(apk_download_size)。
4)还原混淆后资源ID。
5)根据文件MD5判断重复资源文件。
6)读取DEX头文件获取classes.dex的class_numbers和references_methods。
7)获取非alpha通道图以及图片尺寸,遍历出非透明通道图。
8)ZipFile解压APK的so文件并读取so文件内容,还原so混淆的资源ID,so中非透明通道图。
9)res/下无用资源。
以上工作主要使用Python进行实现,Python断点续传下载APK以及Mapping文件之后解压文件,为之后分析APK做好准备。这里关于Python下载实现不再赘述。
3.1 AAPT获取APK信息通过AAPT命令可以获取APK的package_name,version_name,version_code,launch_activity,min_sdk_version,target_sdk_version,application_label等信息
具体实现如下:
def get_apk_base_info(self): # 获取apk包的基本信息p = subprocess.Popen(self.aapt_path + " dump badging %s" % self.apkPath, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, shell=True) (output, err) = p.communicate() package_match = re.compile("package: name='(\S+)' versionCode='(\d+)' versionName='(\S+)'").match(output.decode()) if not package_match: raise Exception("can't get package,versioncode,version") package_name = package_match.group(1) version_code = package_match.group(2) version_name = package_match.group(3) launch_activity_match = re.compile("launchable-activity: name='(\S+)'").search(output.decode()) if not launch_activity_match: raise Exception("can't get launch_activity") launch_activity = launch_activity_match.group(1) sdk_version_match = re.compile("sdkVersion:'(\S+)'").search(output.decode()) if not sdk_version_match: raise Exception("can't get min_sdk_version") min_sdk_version = sdk_version_match.group(1) target_sdk_version_match = re.compile("targetSdkVersion:'(\S+)'").search(output.decode()) if not target_sdk_version_match: raise Exception("can't get target_sdk_version") target_sdk_version = target_sdk_version_match.group(1) application_label_match = re.compile("application-label:'([\u4e00-\u9fa5_a-zA-Z0-9-\S]+)'").search(output.decode()) if not application_label_match: raise Exception("can't get application_label") application_label = application_label_match.group(1) return package_name, version_name, version_code,launch_activity,min_sdk_version,target_sdk_version,application_label
3.2 apk_file_size & apk_download_size
apk_file_size是APK在操作系统中占据存储空间,可以通过os模块直接获取;apk_download_size是APK内实际大小,可以ZipFile获取每个文件压缩大小,实现如下:
def get_apk_size(self):# 得到apk的文件大小 size = round(os.path.getsize(self.apkPath) / (1024 * 1000), 2) # return str(size) + "M" return os.path.getsize(self.apkPath)def get_apk_download_size(apk_file_name): # 获取apk_download_size zip_file = zipfile.ZipFile(apk_file_name, 'r') zip_infos = zip_file.infolist() download_size = 0 for index in range(len(zip_infos)): zip_info = zip_infos[index] download_size += zip_info.compress_size return download_size
3.3 ZipFile读取APK文件
许多人多使用apktool.jar解压APK&