读完这篇文章,你可能会了解到以下几点:
1. 蒲公英为什么只上传 ipa 文件,就可以下载 app
2. Java 解析 ipa 文件 (iOS 应用包)
3. Java 解析 apk 文件 (Android 应用包)
4. 自己上传 app 到服务器,模拟蒲公英的效果
************************************* 我是一条朴素的分割线 *************************************
20200512更新日志
问题
有些apk包,用之前的jar包解析,报错:Expected chunk of type 0x80003, read 0x80001.
解决办法
使用aapt命令解析apk文件
关于aapt
一、aapt定义:
aapt即Android Asset Packaging Tool, 在SDK的build-tools目录下。该工具可以查看、创建、更新ZIP格式的文档附件(zip,jar,apk)。也可将资源文件编译成二进制文件,尽管你可能没有直接使用aapt工具,但是build scripts和IDE插件会使用这个工具打包apk文件构成一个Android应用程序。在使用aapt之前需要在环境变量里面配置SDK-tools路径,或者是路径+aapt的方式进入aapt。
二、aapt文件位置
安卓sdk目录下:
/Users/yong.chen/Library/Android/sdk/build-tools/28.0.3/aapt
三、常用命令
AAPT常用命令
AAPT用法
选项 | 说明 | 例如 |
---|---|---|
badging | 查看apk包的packageName、versionCode、applicationLabel、launcherActivity、permission等各种详细信息 | aapt dump badging |
permissions | 查看权限 | aapt dump resources |
resources | 查看资源列表 | aapt dump resources |
configurations | 查看apk配置信息 | aapt dump configurations |
xmltree | 以树形结构输出的xml信息。 | aapt dump xmltree |
xmlstrings | 查看指定apk的指定xml文件。 | aapt dump xmlstrings |
TIP:由于我们工作中需要使用badging参数来查看versioncode,因此可以使用命令aapt dump badging | findstr “versionCode”
来查看
上代码
private ProcessBuilder mBuilder;
private static final String SPLIT_REGEX = "(: )|(=')|(' )|'";
public AppAnalyzeUtils() {
mBuilder = new ProcessBuilder();
mBuilder.redirectErrorStream(true);
}
private Map analyzeApk(String filePath, String aaptPath) {
Map apkInfoMap = new HashMap<>();
Process process = null;
BufferedReader br = null;
String tmp;
InputStream is = null;
try {
process = mBuilder.command(aaptPath, "d", "badging", filePath).start();
is = process.getInputStream();
br = new BufferedReader(new InputStreamReader(is, "utf8"));
tmp = br.readLine();
if (tmp == null || !tmp.startsWith("package"))
throw new Exception("参数不正确,无法正常解析APK包。输出结果为:\n" + tmp);
do {
if (tmp.startsWith("package")) splitPackageInfo(apkInfoMap, tmp);
} while ((tmp = br.readLine()) != null);
return apkInfoMap;
} catch (Exception e) {
logger.error("[AppAnalyzeUtils analyzeApk]" + e.getMessage());
} finally {
if (process != null) {
process.destroy();
}
closeIO(is);
closeIO(br);
return apkInfoMap;
}
}
private void splitPackageInfo(Map apkInfoMap, String packageSource) {
String[] packageInfo = packageSource.split(SPLIT_REGEX);
apkInfoMap.put("package", packageInfo[2]) ;
apkInfoMap.put("buildVersion", packageInfo[4]) ;
apkInfoMap.put("versionName", packageInfo[6]) ;
}
private void closeIO(Closeable io) {
if (io != null) {
try {
io.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
存在的问题
本地启动服务,可以正常执行aapt命令,并解析apk文件。上传服务器后,因aapt文件没有执行权限,故报错,需要运维工程师协助配合。
************************************* 我是一条朴素的分割线 *************************************
关于蒲公英的思考
蒲公英的作用(在工作中)
- 在我的实际工作中,蒲公英主要用于企业包(In-House证书打的包)的分发,方便 QA 和其他用户测试
- 如果是自己做应用分发(下载),比如把
.ipa
和info.plist
文件 上传到七牛服务器,然后自己制作一个下载页面
为什么蒲公英那么方便?
- 我的想法是:在我们上传
ipa
文件的同时,蒲公英会根据 ipa 文件,读取应用对应的配置文件,获取必要信息 (比如bundleId
),生成对应的info.plist
文件,然后同时上传到服务器,就相当于我们自己手动上传那两个文件一样的效果。
思考:如何获取 ipa 或者 apk 文件的应用的配置文件信息呢?
获取 ipa 文件的配置文件信息
准备工作
- iOS 安装包(.ipa 文件)
- 解析
info.plist
文件所需的 jar 包(点我去下载页)
主要代码
// AppUtil.java 文件
import com.dd.plist.NSDictionary;
import com.dd.plist.NSNumber;
import com.dd.plist.NSObject;
import com.dd.plist.NSString;
import com.dd.plist.PropertyListParser;
/**
* 解析 IPA 文件
* @param is 输入流
* @return Map
*/
public static Map analyzeIpa(InputStream is) {
Map resultMap = new HashMap<>();
try {
ZipInputStream zipIs = new ZipInputStream(is);
ZipEntry ze;
InputStream infoIs = null;
while ((ze = zipIs.getNextEntry()) != null) {
if (!ze.isDirectory()) {
String name = ze.getName();
// 读取 info.plist 文件
// FIXME: 包里可能会有多个 info.plist 文件!!!
if (name.contains(".app/Info.plist")) {
ByteArrayOutputStream abos = new ByteArrayOutputStream();
int chunk = 0;
byte[] data = new byte[256];
while(-1 != (chunk = zipIs.read(data))) {
abos.write(data, 0, chunk);
}
infoIs = new ByteArrayInputStream(abos.toByteArray());
break;
}
}
}
NSDictionary rootDict = (NSDictionary) PropertyListParser.parse(infoIs);
String[] keyArray = rootDict.allKeys();
for (String key : keyArray) {
NSObject value = rootDict.objectForKey(key);
if (key.equals("CFBundleSignature")) {
continue;
}
if (value.getClass().equals(NSString.class) || value.getClass().equals(NSNumber.class)) {
resultMap.put(key, value.toString());
}
}
zipIs.close();
is.close();
} catch (Exception e) {
resultMap.put("error", e.getStackTrace());
}
return resultMap;
}
获取 apk 文件的配置文件信息
准备工作
- Android 安装包(.apk 文件)
- 解析
AndroidManifest.xml
文件所需的 jar 包(点我去下载页)
主要代码
// AppUtil.java 文件
import org.apkinfo.api.util.AXmlResourceParser;
import org.apkinfo.api.util.XmlPullParser;
import org.apkinfo.api.util.XmlPullParserException;
/**
* 解析 APK 文件
* @param is 输入流
* @return Map>
*/
public static Map> analyzeApk(InputStream is) {
Map> resultMap = new HashMap<>();
try {
ZipInputStream zipIs = new ZipInputStream(is);
zipIs.getNextEntry();
AXmlResourceParser parser = new AXmlResourceParser();
parser.open(zipIs);
boolean flag = true;
while(flag) {
int type = parser.next();
if (type == XmlPullParser.START_TAG) {
int count = parser.getAttributeCount();
String action = parser.getName().toUpperCase();
if(action.equals("MANIFEST") || action.equals("APPLICATION")) {
Map tempMap = new HashMap<>();
for (int i = 0; i < count; i++) {
String name = parser.getAttributeName(i);
String value = parser.getAttributeValue(i);
value = (value == null) ? "" : value;
tempMap.put(name, value);
}
resultMap.put(action, tempMap);
} else {
Map manifest = resultMap.get("MANIFEST");
Map application = resultMap.get("APPLICATION");
if(manifest != null && application != null) {
flag = false;
}
continue;
}
}
}
zipIs.close();
is.close();
} catch (ZipException e) {
resultMap.put("error", getError(e));
} catch (IOException e) {
resultMap.put("error", getError(e));
} catch (XmlPullParserException e) {
resultMap.put("error", getError(e));
}
return resultMap;
}
private static Map getError(Exception e) {
Map errorMap = new HashMap<>();
errorMap.put("cause", e.getCause());
errorMap.put("message", e.getMessage());
errorMap.put("stack", e.getStackTrace());
return errorMap;
}
注:以上代码,部分参考自网络。整合之后,亲测,可以正常使用。【20170903】
模拟蒲公英上传 ipa 文件
主要代码
@ResponseBody
@RequestMapping(value = "app/upload", method = RequestMethod.POST)
public Object upload(@RequestParam MultipartFile file, HttpServletRequest request) {
Log.info("上传开始");
int evn = 4;
// 文件上传成功后,返回给前端的 appInfo 对象
AppInfoModel appInfo = new AppInfoModel();
appInfo.setEvn(evn);
String path = request.getSession().getServletContext().getRealPath("upload");
Date now = new Date();
String[] extensions = file.getOriginalFilename().split("\\.");
long time = now.getTime();
String fileNameWithoutExtension = "app_" + evn + "_" + time;
String fileExtension = extensions[extensions.length - 1];
String fileName = fileNameWithoutExtension + "." + fileExtension;
Log.info(path);
try {
File targetFile = new File(path, fileName);
if(!targetFile.exists()) {
targetFile.mkdirs();
}
file.transferTo(targetFile);
InputStream is = new FileInputStream(targetFile);
boolean isIOS = fileExtension.toLowerCase().equals("ipa");
if (isIOS) {
// 获取配置文件信息
Map infoPlist = AppUtil.analyzeIpa(is);
// 获取包名
String packageName = (String) infoPlist.get("CFBundleIdentifier");
appInfo.setBundleId(packageName);
appInfo.setVersionCode((String) infoPlist.get("CFBundleShortVersionString"));
// 这是个私有方法,根据包名获取特定的 app 信息,并设置 appInfo
setupAppInfo(packageName, true, appInfo);
} else if (fileExtension.toLowerCase().equals("apk")) {
// 获取配置文件信息
Map> infoConfig = AppUtil.analyzeApk(is);
Map manifestMap = infoConfig.get("MANIFEST");
String packageName = (String) manifestMap.get("package");
appInfo.setBundleId(packageName);
appInfo.setVersionCode((String) manifestMap.get("versionName"));
setupAppInfo(packageName, false, appInfo);
} else {
Map map = new HashMap<>();
map.put("code", NetError.BadRequest.getCode());
map.put("message", "文件格式错误,请重新上传!");
return map;
}
// 上传 FTP
FTPUtil ftp = new FTPUtil(FtpHostName, FtpHostPort, FtpUserName, FtpPassword);
String ftpPath = "/app/" + appInfo.getAppId() + "/" + appInfo.getVersionCode();
FileInputStream in = new FileInputStream(targetFile);
ftp.uploadFile(ftpPath, fileName, in);
targetFile.delete();
String url = ftpPath + "/" + fileName;
if (isIOS) { // iOS 创建 plist 文件
String plistFilName = fileNameWithoutExtension + ".plist";
String plistUrl = path + "/" + plistFilName;
// 创建 info.plist 文件
boolean result = appUploadService.createPlist(plistUrl, nfo.getBundleId(), appInfo.getName(), appInfo.getVersionCode(), url, est.getLocalAddr(), request.getLocalPort());
if (result == false) {
NetError error = NetError.BadRequest;
error.setMessage("创建Plist文件失败");
throw new NetException(error);
}
File targetPlistFile = new File(path, plistFilName);
in = new FileInputStream(targetPlistFile);
ftp.uploadFile(ftpPath, plistFilName, in);
url = ftpPath + "/" + plistFilName;
targetPlistFile.delete();
}
Log.info("上传完成");
final String uploadedUrl = url;
return getResult(new HashMap(){{
put("url", uploadedUrl);
put("appInfo", appInfo);
}});
} catch (Exception e) {
e.printStackTrace();
NetError error = NetError.BadRequest;
error.setMessage(e.toString());
throw new NetException(error);
}
}
Demo
Tips
关于 jar 包下载
- 之前 csdn 上可以上传零积分下载的资源,现在至少是1积分,所以积分不足的同学,可以留下联系方式,私发。
关于 demo
- 由于完整的 demo 涉及到公司项目相关的内容,所以暂不上传,日后整理后再贴出来下载链接。