1. 程式人生 > >Java獲取apk / ipa應用資訊的思考與實踐

Java獲取apk / ipa應用資訊的思考與實踐

讀完這篇文章,你可能會了解到以下幾點:

1. 蒲公英為什麼只上傳 ipa 檔案,就可以下載 app
2. Java 解析 ipa 檔案 (iOS 應用包)
3. Java 解析 apk 檔案 (Android 應用包)
4. 自己上傳 app 到伺服器,模擬蒲公英的效果

關於蒲公英的思考

蒲公英的作用(在工作中)

  • 在我的實際工作中,蒲公英主要用於企業包(In-House證書打的包)的分發,方便 QA 和其他使用者測試
  • 如果是自己做應用分發(下載),比如是把 .ipainfo.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

demo.gif

Tips

關於 jar 包下載

  • 之前 CSDN 上可以上傳零積分下載的資源,現在至少是1積分,所以積分不足的同學,可以留下聯絡方式,私發。

關於 demo

  • 由於完整的 demo 涉及到公司專案相關的內容,所以暫不上傳,日後整理後再貼出來下載連結。