Java獲取apk / ipa應用資訊的思考與實踐
阿新 • • 發佈:2019-01-27
讀完這篇文章,你可能會了解到以下幾點:
1. 蒲公英為什麼只上傳 ipa 檔案,就可以下載 app
2. Java 解析 ipa 檔案 (iOS 應用包)
3. Java 解析 apk 檔案 (Android 應用包)
4. 自己上傳 app 到伺服器,模擬蒲公英的效果
關於蒲公英的思考
蒲公英的作用(在工作中)
- 在我的實際工作中,蒲公英主要用於企業包(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<String, Object>
*/
public static Map<String, Object> analyzeIpa(InputStream is) {
Map<String, Object> 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 包(點我去下載頁)
主要程式碼
/**
* 解析 APK 檔案
* @param is 輸入流
* @return Map<String,Map<String,Object>>
*/
public static Map<String,Map<String,Object>> analyzeApk(InputStream is) {
Map<String,Map<String,Object>> 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<String,Object> 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<String,Object> manifest = resultMap.get("MANIFEST");
Map<String,Object> 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<String,Object> getError(Exception e) {
Map<String,Object> 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<String, Object> 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<String,Map<String,Object>> infoConfig = AppUtil.analyzeApk(is);
Map<String, Object> 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<String, Object> 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<String, Object>(){{
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 涉及到公司專案相關的內容,所以暫不上傳,日後整理後再貼出來下載連結。