Java读取apk、ipa包名、版本名、版本号、图标等信息
当我们上传apk或者ipa文件的时候是要读取到里面的一些信息的,而对于未知的apk或者ipa,我们无法直接获取其中的包名、应用名、图标等,这时候通过工具类来获取信息就很有必要了。在这里,我总结了一下读取apk,ipa文件的代码和使用方法,大家可以参考下,取其精华、去其糟粕,以便适用于自己的需求。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 首先看一下使用到的工具
环境:Linux
apktool是google提供的apk编译工具,需要Java运行环境,推荐使用JDK1.6或者JDK1.7;dd-plist-1.16这个jar包需要JDK1.5以上。
所以,建议JDK环境1.6以上。
解析apk:使用windows开发环境的时候直接下载aapt.exe即可
(1)aapt
(2)apktool
(3)apktool.jar (google提供)
解析ipa:
(1)dd-plist-1.16.jar(google提供)
(2)python 2.6
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2 解析apk
对于apk的解析,我第一个想到的是导入相应的jar包,然后用一些代码就可以读取出来,这样的话就比较方便简单。事实上,这样做的确是可以读取到一些信息的,比如说apk的包名、版本号、版本名,但是对于apk的名称和图标就读取不到了,显然这个不能满足于我们的要求。所以,我希望可以找到一种能够读取尽量多信息的方法,通过在网上查询比较,我选择了aapt这个工具。尽管aapt工具很烦人,比我之前说的导jar包的方式繁琐很多,但是我们需要解析未知apk的图标,名称,不得不使用这个aapt工具了。
2.1 在Linux下安装aapt工具
apktool和aapt各种版本的下载地址:http://connortumbleson.com/apktool/
下载apktool
# wget https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool
下载apktool_2.2.1.jar并且重命名为apktool.jar
# wget http://connortumbleson.com/apktool/apktool_2.2.1.jar
# mv apktool_2.2.1.jar apktool.jar
下载aapt
http://connortumbleson.com/apktool/aapt/linux/aapt
新建/usr/local/apktool文件夹,将apktool,apktool.jar和aapt移进来
# mv apktool apktool.jar aapt /usr/local/apktool
赋予apktool,apktool.jar和aapt可执行权限
# chmod +x apktool apktool.jar aapt
将apktool加入环境变量,修改/etc/profile,在最后一行添加如下内容
export PATH="$PATH:/usr/local/apktool"
使环境变量立即生效
# source /etc/profile
使用aapt工具时可能报如下错误
aapt: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by aapt)
这是因为缺少glibc-2.14,安装一下就好了
# wget http://ftp.gnu.org/gnu/glibc/glibc-2.14.tar.gz
# tar zxvf glibc-2.14.tar.gz
# cd glibc-2.14
# mkdir build
# cd build
# ../configure --prefix=/opt/glibc-2.14
# make -j4
# make install
修改/etc/profile,在最后一行添加如下内容
export LD_LIBRARY_PATH=/opt/glibc-2.14/lib:$LD_LIBRARY_PATH
使环境变量立即生效
# source /etc/profile
给aapt相应权限
# chmod 755 aapt
apktool和aapt简单使用方法
# apktool d /data/test.apk //反编译apk
# aapt dump badging /data/test.apk //查看apk包详细信息
2.2 建两个实体类
这两个实体类封装了要解析apk的一些信息字段,比如说apk的包名、版本号、版本名、所需设备特性等。
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * apk实体信息 */ public class ApkInfo { public static final String APPLICATION_ICON_120 = "application-icon-120"; public static final String APPLICATION_ICON_160 = "application-icon-160"; public static final String APPLICATION_ICON_240 = "application-icon-240"; public static final String APPLICATION_ICON_320 = "application-icon-320"; /** * apk内部版本号 */ private String versionCode = null; /** * apk外部版本号 */ private String versionName = null; /** * apk的包名 */ private String packageName = null; /** * 支持的android平台最低版本号 */ private String minSdkVersion = null; /** * apk所需要的权限 */ private List<String> usesPermissions = null; /** * 支持的SDK版本。 */ private String sdkVersion; /** * 建议的SDK版本 */ private String targetSdkVersion; /** * 应用程序名 */ private String applicationLable; /** * 各个分辨率下的图标的路径。 */ private Map<String, String> applicationIcons; /** * 程序的图标。 */ private String applicationIcon; /** * 暗指的特性。 */ private List<ImpliedFeature> impliedFeatures; /** * 所需设备特性。 */ private List<String> features; /** * 启动界面 */ private String launchableActivity; public ApkInfo() { this.usesPermissions = new ArrayList<String>(); this.applicationIcons = new HashMap<String, String>(); this.impliedFeatures = new ArrayList<ImpliedFeature>(); this.features = new ArrayList<String>(); } /** * 返回版本代码。 * * @return 版本代码。 */ public String getVersionCode() { return versionCode; } /** * @param versionCode * the versionCode to set */ public void setVersionCode(String versionCode) { this.versionCode = versionCode; } /** * 返回版本名称。 * * @return 版本名称。 */ public String getVersionName() { return versionName; } /** * @param versionName * the versionName to set */ public void setVersionName(String versionName) { this.versionName = versionName; } /** * 返回支持的最小sdk平台版本。 * * @return the minSdkVersion */ public String getMinSdkVersion() { return minSdkVersion; } /** * @param minSdkVersion * the minSdkVersion to set */ public void setMinSdkVersion(String minSdkVersion) { this.minSdkVersion = minSdkVersion; } /** * 返回包名。 * * @return 返回的包名。 */ public String getPackageName() { return packageName; } public void setPackageName(String packageName) { this.packageName = packageName; } /** * 返回sdk平台版本。 * * @return */ public String getSdkVersion() { return sdkVersion; } public void setSdkVersion(String sdkVersion) { this.sdkVersion = sdkVersion; } /** * 返回所建议的SDK版本。 * * @return */ public String getTargetSdkVersion() { return targetSdkVersion; } public void setTargetSdkVersion(String targetSdkVersion) { this.targetSdkVersion = targetSdkVersion; } /** * 返回所需的用户权限。 * * @return */ public List<String> getUsesPermissions() { return usesPermissions; } public void setUsesPermissions(List<String> usesPermission) { this.usesPermissions = usesPermission; } public void addToUsesPermissions(String usesPermission) { this.usesPermissions.add(usesPermission); } /** * 返回程序的名称标签。 * * @return */ public String getApplicationLable() { return applicationLable; } public void setApplicationLable(String applicationLable) { this.applicationLable = applicationLable; } /** * 返回应用程序的图标。 * * @return */ public String getApplicationIcon() { return applicationIcon; } public void setApplicationIcon(String applicationIcon) { this.applicationIcon = applicationIcon; } /** * 返回应用程序各个分辨率下的图标。 * * @return */ public Map<String, String> getApplicationIcons() { return applicationIcons; } public void setApplicationIcons(Map<String, String> applicationIcons) { this.applicationIcons = applicationIcons; } public void addToApplicationIcons(String key, String value) { this.applicationIcons.put(key, value); } public void addToImpliedFeatures(ImpliedFeature impliedFeature) { this.impliedFeatures.add(impliedFeature); } /** * 返回应用程序所需的暗指的特性。 * * @return */ public List<ImpliedFeature> getImpliedFeatures() { return impliedFeatures; } public void setImpliedFeatures(List<ImpliedFeature> impliedFeatures) { this.impliedFeatures = impliedFeatures; } /** * 返回应用程序所需的特性。 * * @return */ public List<String> getFeatures() { return features; } public void setFeatures(List<String> features) { this.features = features; } public void addToFeatures(String feature) { this.features.add(feature); } @Override public String toString() { return "ApkInfo [versionCode=" + versionCode + ",\n versionName=" + versionName + ",\n packageName=" + packageName + ",\n minSdkVersion=" + minSdkVersion + ",\n usesPermissions=" + usesPermissions + ",\n sdkVersion=" + sdkVersion + ",\n targetSdkVersion=" + targetSdkVersion + ",\n applicationLable=" + applicationLable + ",\n applicationIcons=" + applicationIcons + ",\n applicationIcon=" + applicationIcon + ",\n impliedFeatures=" + impliedFeatures + ",\n features=" + features + ",\n launchableActivity=" + launchableActivity + "\n]"; } public String getLaunchableActivity() { return launchableActivity; } public void setLaunchableActivity(String launchableActivity) { this.launchableActivity = launchableActivity; } }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * 特性实体 */ public class ImpliedFeature { /** * 所需设备特性名称。 */ private String feature; /** * 表明所需特性的内容。 */ private String implied; public ImpliedFeature() { super(); } public ImpliedFeature(String feature, String implied) { super(); this.feature = feature; this.implied = implied; } public String getFeature() { return feature; } public void setFeature(String feature) { this.feature = feature; } public String getImplied() { return implied; } public void setImplied(String implied) { this.implied = implied; } @Override public String toString() { return "Feature [feature=" + feature + ", implied=" + implied + "]"; } }
2.3 apk的工具类
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * apk工具类。封装了获取Apk信息的方法。 * 包名/版本号/版本名/应用程序名/支持的android最低版本号/支持的SDK版本/建议的SDK版本/所需设备特性等 */ public class ApkUtil { public static final String VERSION_CODE = "versionCode"; public static final String VERSION_NAME = "versionName"; public static final String SDK_VERSION = "sdkVersion"; public static final String TARGET_SDK_VERSION = "targetSdkVersion"; public static final String USES_PERMISSION = "uses-permission"; public static final String APPLICATION_LABEL = "application-label"; public static final String APPLICATION_ICON = "application-icon"; public static final String USES_FEATURE = "uses-feature"; public static final String USES_IMPLIED_FEATURE = "uses-implied-feature"; public static final String SUPPORTS_SCREENS = "supports-screens"; public static final String SUPPORTS_ANY_DENSITY = "supports-any-density"; public static final String DENSITIES = "densities"; public static final String PACKAGE = "package"; public static final String APPLICATION = "application:"; public static final String LAUNCHABLE_ACTIVITY = "launchable-activity"; private ProcessBuilder mBuilder; private static final String SPLIT_REGEX = "(: )|(=')|(' )|'"; private static final String FEATURE_SPLIT_REGEX = "(:')|(',')|'"; /** * aapt所在的目录。 */ //windows环境下直接指向appt.exe //比如你可以放在src下 private String mAaptPath = "src/main/java/aapt"; //linux下 //private String mAaptPath = "/usr/local/apktool/aapt"; public ApkUtil() { mBuilder = new ProcessBuilder(); mBuilder.redirectErrorStream(true); } /** * 返回一个apk程序的信息。 * * @param apkPath apk的路径。 * @return apkInfo 一个Apk的信息。 */ public ApkInfo getApkInfo(String apkPath) throws Exception { System.out.println("================================开始执行命令========================="); //通过命令调用aapt工具解析apk文件 Process process = mBuilder.command(mAaptPath, "d", "badging", apkPath) .start(); InputStream is = null; is = process.getInputStream(); BufferedReader br = new BufferedReader( new InputStreamReader(is, "utf8")); String tmp = br.readLine(); try { if (tmp == null || !tmp.startsWith("package")) { throw new Exception("参数不正确,无法正常解析APK包。输出结果为:\n" + tmp + "..."); } ApkInfo apkInfo = new ApkInfo(); do { setApkInfoProperty(apkInfo, tmp); } while ((tmp = br.readLine()) != null); return apkInfo; } catch (Exception e) { throw e; } finally { process.destroy(); closeIO(is); closeIO(br); } } /** * 设置APK的属性信息。 * * @param apkInfo * @param source */ private void setApkInfoProperty(ApkInfo apkInfo, String source) { if (source.startsWith(PACKAGE)) { splitPackageInfo(apkInfo, source); } else if (source.startsWith(LAUNCHABLE_ACTIVITY)) { apkInfo.setLaunchableActivity(getPropertyInQuote(source)); } else if (source.startsWith(SDK_VERSION)) { apkInfo.setSdkVersion(getPropertyInQuote(source)); } else if (source.startsWith(TARGET_SDK_VERSION)) { apkInfo.setTargetSdkVersion(getPropertyInQuote(source)); } else if (source.startsWith(USES_PERMISSION)) { apkInfo.addToUsesPermissions(getPropertyInQuote(source)); } else if (source.startsWith(APPLICATION_LABEL)) { //windows下获取应用名称 apkInfo.setApplicationLable(getPropertyInQuote(source)); } else if (source.startsWith(APPLICATION_ICON)) { apkInfo.addToApplicationIcons(getKeyBeforeColon(source), getPropertyInQuote(source)); } else if (source.startsWith(APPLICATION)) { String[] rs = source.split("( icon=')|'"); apkInfo.setApplicationIcon(rs[rs.length - 1]); //linux下获取应用名称 apkInfo.setApplicationLable(rs[1]); } else if (source.startsWith(USES_FEATURE)) { apkInfo.addToFeatures(getPropertyInQuote(source)); } else if (source.startsWith(USES_IMPLIED_FEATURE)) { apkInfo.addToImpliedFeatures(getFeature(source)); } else { // System.out.println(source); } } private ImpliedFeature getFeature(String source) { String[] result = source.split(FEATURE_SPLIT_REGEX); ImpliedFeature impliedFeature = new ImpliedFeature(result[1], result[2]); return impliedFeature; } /** * 返回出格式为name: 'value'中的value内容。 * * @param source * @return */ private String getPropertyInQuote(String source) { int index = source.indexOf("'") + 1; return source.substring(index, source.indexOf('\'', index)); } /** * 返回冒号前的属性名称 * * @param source * @return */ private String getKeyBeforeColon(String source) { return source.substring(0, source.indexOf(':')); } /** * 分离出包名、版本等信息。 * * @param apkInfo * @param packageSource */ private void splitPackageInfo(ApkInfo apkInfo, String packageSource) { String[] packageInfo = packageSource.split(SPLIT_REGEX); apkInfo.setPackageName(packageInfo[2]); apkInfo.setVersionCode(packageInfo[4]); apkInfo.setVersionName(packageInfo[6]); } /** * 释放资源。 * * @param c 将关闭的资源 */ private final void closeIO(Closeable c) { if (c != null) { try { c.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { try { String demo = "D:\\Java\\idea_app\\ReadApkAndIpa1\\src\\main\\java\\150211100441.apk"; ApkInfo apkInfo = new ApkUtil().getApkInfo(demo); System.out.println(apkInfo); } catch (Exception e) { e.printStackTrace(); } } public String getmAaptPath() { return mAaptPath; } public void setmAaptPath(String mAaptPath) { this.mAaptPath = mAaptPath; } }
如上所示,我做了个测试,我使用的是神庙逃亡的apk,然后解析到如下信息:
从上可以看到解析到的图标信息是以字符串的格式存在的,所以我们还需要一个工具类,通过解析这个字符串解压出icon图片并且存放到磁盘上
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * 通过ApkInfo 里的applicationIcon从APK里解压出icon图片并存放到磁盘上 */ public class ApkIconUtil { /** * 从指定的apk文件里获取指定file的流 * * @param apkPath * @param fileName * @return */ public static InputStream extractFileFromApk(String apkPath, String fileName) { try { ZipFile zFile = new ZipFile(apkPath); ZipEntry entry = zFile.getEntry(fileName); entry.getComment(); entry.getCompressedSize(); entry.getCrc(); entry.isDirectory(); entry.getSize(); entry.getMethod(); InputStream stream = zFile.getInputStream(entry); return stream; } catch (IOException e) { e.printStackTrace(); } return null; } /** * 从指定的apk文件里解压出icon图片并存放到指定的磁盘上 * * @param apkPath apk文件路径 * @param fileName apk的icon * @param outputPath 指定的磁盘路径 * @throws Exception */ public static void extractFileFromApk(String apkPath, String fileName, String outputPath) throws Exception { InputStream is = extractFileFromApk(apkPath, fileName); File file = new File(outputPath); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file), 1024); byte[] b = new byte[1024]; BufferedInputStream bis = new BufferedInputStream(is, 1024); while (bis.read(b) != -1) { bos.write(b); } bos.flush(); is.close(); bis.close(); bos.close(); } /** * demo 获取apk文件的icon并写入磁盘指定位置 * * @param args */ public static void main(String[] args) { try { String apkPath = "D:\\Java\\idea_app\\ReadApkAndIpa1\\src\\main\\java\\shenmiaotaowang_966.apk"; /* if (args.length > 0) { apkPath = args[0]; }*/ ApkInfo apkInfo = new ApkUtil().getApkInfo(apkPath); System.out.println(apkInfo); long now = System.currentTimeMillis(); extractFileFromApk(apkPath, apkInfo.getApplicationIcon(), "D:\\image\\apkIcon" + now + ".png"); } catch (Exception e) { e.printStackTrace(); } } }
经测试,此方案可行,所以如果不需要解析图标的话,ApkUtil就够用了;如果需要解析图标到本地的话,就需要用ApkIconUtil。
这是我解析到的图标
apk的解析到这里就可以告一段落了!
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3 解析ipa
解析ipa基本属性需要用到dd-plist-1.16.jar,有了这个jar包,我们解析ipa基本属性就相对来说简单一点了,可以通过NSDictionary根据相应的字段就可以解析到一些基本信息,比如包名之类的;
而对于icon的解析,首先我们先来看一下plist的结构:
我们可以层层递进来解析图标,先解析CFBundleIcons,然后CFBundlePrimaryIcon,再CFBundleIconFiles。
3.1 ipa工具类
完整代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * 解析ipa的工具类 * 包名/版本名/版本号/应用名称/应用展示名称/所需IOS最低版本 */ public class IpaUtil { /** * * @param ipaURL 安装包的绝对路径 * @param path 指定图标的存放位置 * @return */ public static Map<String, Object> readIPA(String ipaURL,String path) { Map<String, Object> map = new HashMap<String, Object>(); try { File file = new File(ipaURL); InputStream is = new FileInputStream(file); InputStream is2 = new FileInputStream(file); ZipInputStream zipIns = new ZipInputStream(is); ZipInputStream zipIns2 = new ZipInputStream(is2); ZipEntry ze; ZipEntry ze2; InputStream infoIs = null; NSDictionary rootDict = null; String icon = null; while ((ze = zipIns.getNextEntry()) != null) { if (!ze.isDirectory()) { String name = ze.getName(); if (null != name && name.toLowerCase().contains("info.plist")) { ByteArrayOutputStream _copy = new ByteArrayOutputStream(); int chunk = 0; byte[] data = new byte[1024]; while (-1 != (chunk = zipIns.read(data))) { _copy.write(data, 0, chunk); } infoIs = new ByteArrayInputStream(_copy.toByteArray()); rootDict = (NSDictionary) PropertyListParser.parse(infoIs); NSDictionary iconDict = (NSDictionary) rootDict.get("CFBundleIcons"); //获取图标名称 while (null != iconDict) { if (iconDict.containsKey("CFBundlePrimaryIcon")) { NSDictionary CFBundlePrimaryIcon = (NSDictionary) iconDict.get("CFBundlePrimaryIcon"); if (CFBundlePrimaryIcon.containsKey("CFBundleIconFiles")) { NSArray CFBundleIconFiles = (NSArray) CFBundlePrimaryIcon.get("CFBundleIconFiles"); icon = CFBundleIconFiles.getArray()[0].toString(); if (icon.contains(".png")) { icon = icon.replace(".png", ""); } System.out.println("获取icon名称:" + icon); break; } } } break; } } } //根据图标名称下载图标文件到指定位置 while ((ze2 = zipIns2.getNextEntry()) != null) { if (!ze2.isDirectory()) { String name = ze2.getName(); if (icon!=null){ if (name.contains(icon.trim())) { //图片下载到指定的地方 FileOutputStream fos = new FileOutputStream(new File(path)); int chunk = 0; byte[] data = new byte[1024]; while (-1 != (chunk = zipIns2.read(data))) { fos.write(data, 0, chunk); } fos.close(); System.out.println("=================下载图片成功"); break; } } } } //如果想要查看有哪些key ,可以把下面注释放开 // for (String string : dictionary.allKeys()) { // System.out.println(string + ":" + dictionary.get(string).toString()); // } // 应用包名 NSString parameters = (NSString) rootDict.get("CFBundleIdentifier"); map.put("package", parameters.toString()); // 应用版本名 parameters = (NSString) rootDict.objectForKey("CFBundleShortVersionString"); map.put("versionName", parameters.toString()); //应用版本号 parameters = (NSString) rootDict.get("CFBundleVersion"); map.put("versionCode", parameters.toString()); //应用名称 parameters = (NSString) rootDict.objectForKey("CFBundleName"); map.put("name", parameters.toString()); //应用展示的名称 parameters = (NSString) rootDict.objectForKey("CFBundleDisplayName"); map.put("displayName", parameters.toString()); //应用所需IOS最低版本 //parameters = (NSString) rootDict.objectForKey("MinimumOSVersion"); //map.put("minIOSVersion", parameters.toString()); infoIs.close(); is.close(); zipIns.close(); } catch (Exception e) { e.printStackTrace(); map.put("code", "fail"); map.put("error", "读取ipa文件失败"); } return map; } public static void main(String[] args) { String ipaUrl = "D:\\Java\\idea_app\\ReadApkAndIpa1\\src\\main\\java\\com.baosight.securityCloudHD.ipa"; String imgPath = "D:\\image\\ipaIcon" + System.currentTimeMillis() + ".png"; Map<String, Object> mapIpa = IpaUtil.readIPA(ipaUrl,imgPath); for (String key : mapIpa.keySet()) { System.out.println(key + ":" + mapIpa.get(key)); } } }
经测试,可以得到如下信息:
但是这时候,因为我使用的是windows环境,下载的图标就出现问题了(linux环境也有此问题,在mac环境下无此问题),可能是IOS对ipa进行压缩的原因,图标是黑色的。
如下所示:
网上找了很久,发现用ipin.py(python脚本)对图片进行反序列化就可以恢复正常图片,而且这个办法也很简单。
3.2 安装python环境
首先下载python http://www.runoob.com/python/python-install.html
建议下载python2.x系列的,版本太高的话执行ipin.py可能会出问题,我下载的是python2.6。
下载好之后,解压目录,然后在环境变量上配置python的根目录。
3.3 ipin.py展示
这个是我直接在网上下载的,并且做出了一点修改:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#--- # iPIN - iPhone PNG Images Normalizer v1.0 # Copyright (C) 2007 # # Author: # Axel E. Brzostowski # http://www.axelbrz.com.ar/ # axelbrz@gmail.com # # References: # http://iphone.fiveforty.net/wiki/index.php/PNG_Images # http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # #--- from struct import * from zlib import * import stat import sys import os import shutil import glob def getNormalizedPNG(filename): pngheader = "\x89PNG\r\n\x1a\n" file = open(filename, "rb") oldPNG = file.read() file.close() if oldPNG[:8] != pngheader: return None newPNG = oldPNG[:8] chunkPos = len(newPNG) idatAcc = "" breakLoop = False # For each chunk in the PNG file while chunkPos < len(oldPNG): skip = False # Reading chunk chunkLength = oldPNG[chunkPos:chunkPos+4] chunkLength = unpack(">L", chunkLength)[0] chunkType = oldPNG[chunkPos+4 : chunkPos+8] chunkData = oldPNG[chunkPos+8:chunkPos+8+chunkLength] chunkCRC = oldPNG[chunkPos+chunkLength+8:chunkPos+chunkLength+12] chunkCRC = unpack(">L", chunkCRC)[0] chunkPos += chunkLength + 12 # Parsing the header chunk if chunkType == "IHDR": width = unpack(">L", chunkData[0:4])[0] height = unpack(">L", chunkData[4:8])[0] # Parsing the image chunk if chunkType == "IDAT": # Store the chunk data for later decompression idatAcc += chunkData skip = True # Removing CgBI chunk if chunkType == "CgBI": skip = True # Add all accumulated IDATA chunks if chunkType == "IEND": try: # Uncompressing the image chunk bufSize = width * height * 4 + height chunkData = decompress( idatAcc, -15, bufSize) except Exception, e: # The PNG image is normalized print e return None chunkType = "IDAT" # Swapping red & blue bytes for each pixel newdata = "" for y in xrange(height): i = len(newdata) newdata += chunkData[i] for x in xrange(width): i = len(newdata) newdata += chunkData[i+2] newdata += chunkData[i+1] newdata += chunkData[i+0] newdata += chunkData[i+3] # Compressing the image chunk chunkData = newdata chunkData = compress( chunkData ) chunkLength = len( chunkData ) chunkCRC = crc32(chunkType) chunkCRC = crc32(chunkData, chunkCRC) chunkCRC = (chunkCRC + 0x100000000) % 0x100000000 breakLoop = True if not skip: newPNG += pack(">L", chunkLength) newPNG += chunkType if chunkLength > 0: newPNG += chunkData newPNG += pack(">L", chunkCRC) if breakLoop: break return newPNG def updatePNG(filename): data = getNormalizedPNG(filename) if data != None: file = open(filename, "wb") file.write(data) file.close() return True return data def getFiles(base): global _dirs global _pngs if base == ".": _dirs = [] _pngs = [] if base in _dirs: return files = os.listdir(base) for file in files: filepath = os.path.join(base, file) try: st = os.lstat(filepath) except os.error: continue if stat.S_ISDIR(st.st_mode): if not filepath in _dirs: getFiles(filepath) _dirs.append( filepath ) elif file[-4:].lower() == ".png": if not filepath in _pngs: _pngs.append( filepath ) if base == ".": return _dirs, _pngs print "-----------------------------------" print " iPhone PNG Images Normalizer v1.0" print "-----------------------------------" print " " print "[+] Searching PNG files...", dirs, pngs = getFiles(".") print "ok" if len(pngs) == 0: print " " print "[!] Alert: There are no PNG files found. Move this python file to the folder that contains the PNG files to normalize." exit() print " " print " - %d PNG files were found at this folder (and subfolders)." % len(pngs) print " " while True: normalize = "y" if len(normalize) > 0 and (normalize[0] == "y" or normalize[0] == "n"): break normalized = 0 if normalize[0] == "y": for ipng in xrange(len(pngs)): perc = (float(ipng) / len(pngs)) * 100.0 print "%.2f%% %s" % (perc, pngs[ipng]) if updatePNG(pngs[ipng]): normalized += 1 print " " print "[+] %d PNG files were normalized." % normalized for filename in glob.glob(r'/F:/image/*.png'): shutil.move(filename,"/F:/image2") print filename
最后三行代码是自己加上的,意思是把反序列的正常图片全部移到另一个文件夹里,这样做的好处是如果我们上传解析的很多ipa文件,那么也会生成很多图标,如果都放在反序列化的文件夹,而不移走的话,那么,程序每次都会去查找有哪些图片需要反序列化,这些都是耗时的;还有一个好处就是及时将反序列好的图片移到另一个文件夹,防止再次反序列化损坏图片。
3.4 反序列化图标
将ipin.fy文件放在image目录下,并且没有将ipa图标进行反序列化是这样的:
点shift+右键,然后点击在此处打开窗口,在打开的命令窗口上输入python ipin.py命令
然后就可以将上面黑色的图标进行反序列化,得到下面这样的:
但是,这边得注意的是:
1.windows环境下如果得到的ipa图标是黑色的,输入一次命令后,会得到正常的,这时候不能再输入上述的命令了,不然会损坏文件!!!
2.mac环境下不需要进行这些操作
ipa的解析到这里也可以告一段落了。
4.工具类的调用方法
4.1 apk工具类的调用
-
首先传入一个apkPath(apk安装包的绝对路径:可以存放在项目根目录,然后通过这个相对路径获取绝对路径,再加上安装包名即可),这边分享一个处理上传文件的工具类UploadUtil
public class UploadUtil { /** * 处理文件上传 * * @param file * @param basePath * 存放文件的目录的绝对路径 servletContext.getRealPath("/upload") * @return */ public static String upload(MultipartFile file, String basePath) { String orgFileName = file.getOriginalFilename(); String fileName = System.currentTimeMillis() + "." + FilenameUtils.getExtension(orgFileName); try { File targetFile = new File(basePath, fileName); FileUtils.writeByteArrayToFile(targetFile, file.getBytes()); } catch (IOException e) { e.printStackTrace(); } return fileName; } }
-
然后通过这个apkPath调用ApkUtil的getApkInfo方法就可以得到apkInfo,这个apkInfo里面有包名,版本名,版本号,图标等信息
- 最后调用ApkIconUtil的extractFileFromApk方法就可以将apk的图标存放到指定的位置
-
伪代码示例:
@Autowired private ServletContext context; public void do(MultipartFile file){ //根据项目根路径得到这个根路径的绝对路径 String absolutePath = context.getRealPath(""); //项目根路径的绝对路径+安装包名就是这个安装包名的绝对路径 String apkPath = absolutePath + "/" + UploadUtil.upload(file , absolutePath); String imaPath = System.currentTimeMillis() + ".png"; //得到apkInfo ApkInfo apkInfo = new ApkUtil.getApkInfo(apkPath); //将解析出来的图标存放到项目根路径下 ApkIconUtil.extractFileFromApk(apkPath, apkInfo.getApplicationIcon(), absolutePath + "/" + imaPath); //包名、版本名、版本号、应用名称 String packageName = apkInfo.getPackageName(); String versionName = apkInfo.getVersionName(); String versionCode = apkInfo.getVersionCode(); String displayName = apkInfo.getApplicationLable(); }
4.2 ipa工具类的调用
- 调用IpaUtil的readIpa方法,这个方法有两个参数,第一个参数是安装包的绝对路径,第二个参数是指定解析出来的图标存放位置,得到的是一个map
- 然后就可以从这个map中取出包名、版本名、版本号、应用名称
-
伪代码示例
//接着上面伪代码使用的do方法来写,实际上可以根据apkPath包含".apk"或".ipa"来判断是apk或者ipa public void do(MultipartFile file){ Map<String, Object> map = IpaUtil.readIPA(apkPath, absolutePath + "/" + imaPath); String packageName = (String) map.get("package"); String versionName = (String) map.get("versionName"); String versionCode = (String) map.get("versionCode"); String displayName = (String) map.get("displayName"); }