当前位置:   article > 正文

android 原apk替换androidManifest.xml的metaData的多渠道自动打包_apk替换xml资源

apk替换xml资源

在已经编译出一个apk的情况下,其他的渠道只是改变androidManifest.xml的metaData信息,在这个情况下不需要再编译apk,只需要修改androidManifest.xml;

实现的思路如下:

1.获取源androidManifest.xml;因为apk里的androidManifest.xml是已经编译为二进制的文件,不好修改;可以使用apktool把源apk反编译得到androidManifest.xml的文本;

  当然上面可以二进制的可以通过AXMLEditor.jar来修改,但这个修改metadata有点吃力,先简单开始直接使用apktool。

2.修改metaData:反编译得到androidManifest.xml的文本修改metaData信息;

3.得到二进制的androidManifest.xml:通过apktool再次编译为apk,解压androidManifest.xml出来即可;

3.替换原apk的二进制的androidManifest.xml,这样得到是全新的apk;

4.签名:删除apk的META-INF,使用jarsigner进行签名;

5.字节对齐:通过zipalign进行字节对齐;

利用android studio的product多渠道脚本、签名等信息可实现修改androidManifest.xml;脚本代码如下:

  1. class ChannelBuildPlugin implements Plugin<Project> {
  2. String mSourceApkPath
  3. String mOutPutDir
  4. String mApkToolPath
  5. String mZip7ToolPath
  6. String mZipalignToolPath
  7. String mKeystore
  8. String mAlia
  9. String mStorepass
  10. String mSourceApkName
  11. String mProductName
  12. String mApplicationId
  13. void apply(Project project) {
  14. project.extensions.create("buildparam", ChannelBuildPluginExtension)
  15. project.task('autoBuildChannelProduct') << {
  16. println "autoBuildChannelProduct start "
  17. if (project.buildparam.sourceApkPath == null) {
  18. println "error !!!sourceApkPath == null"
  19. return
  20. }
  21. mSourceApkPath = project.buildparam.sourceApkPath
  22. File fp = new File(mSourceApkPath)
  23. if (!fp.exists()){
  24. throw new FileNotFoundException(mSourceApkPath)
  25. }
  26. mSourceApkName = fp.getName()
  27. mOutPutDir = project.buildparam.outPutDir
  28. File outDir = new File(mOutPutDir)
  29. if (!outDir.exists()){
  30. outDir.mkdirs()
  31. }
  32. mApkToolPath = project.buildparam.apkToolPath
  33. mZipalignToolPath = project.buildparam.zipalignToolPath
  34. mZip7ToolPath = project.buildparam.zip7ToolPath
  35. mKeystore = project.buildparam.keystore
  36. mAlia = project.buildparam.alia
  37. mStorepass = project.buildparam.storepass
  38. def signingConfigs
  39. project.copy {
  40. from "$mSourceApkPath"
  41. into "$mOutPutDir/workdir/sorceapk"
  42. }
  43. decodeApk()
  44. project.android.applicationVariants.all { variant -
  1.                 if (variant.name.contains("Release")){
  2.                     mProductName = variant.flavorName;
  3.                     signingConfigs = variant.getSigningConfig()
  4.                     def metaConfig
  5.                     mApplicationId = variant.productFlavors.applicationId[0]
  6.                     println "applicationId:"+ mApplicationId
  7.                     for (def item:variant.productFlavors.manifestPlaceholders){
  8.                         metaConfig = item;
  9.                     }
  10.                     modifyMetaDataXML(metaConfig)
  11.                     packageApk()
  12.                     unzipAndroidManifest()
  13.                     replaceApkAndroidManifest()
  14.                     signCusApk(signingConfigs)
  15.                     zipalign(project)
  16.                     project.copy {
  17.                         String targetApk = "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk"
  18.                         if (mApkMd5 != null && !mApkMd5.equals("")){
  19.                             targetApk = "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_$mApkMd5"+".apk"
  20.                         }
  21.                         from "$targetApk"
  22.                         into "$mOutPutDir"
  23.                     }
  24.                 }
  1.     }
  2. //重新签名
  3. project.task('signApk') << {
  4. }
  5. }
  6. public void zipalign(Project project) {
  7. def apkFile = new File("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
  8. if (apkFile.exists()){
  9. apkFile.delete()
  10. }
  11. apkFile = new File("$mOutPutDir/workdir/sorceapk/$mSourceApkName")
  12. if (apkFile.exists()) {
  13. def sdkDir
  14. Properties properties = new Properties()
  15. File localProps = project.rootProject.file("local.properties")
  16. if (localProps.exists()) {
  17. properties.load(localProps.newDataInputStream())
  18. sdkDir = properties.getProperty("sdk.dir")
  19. } else {
  20. sdkDir = System.getenv("ANDROID_HOME")
  21. }
  22. if (sdkDir) {
  23. Properties prop = System.getProperties();
  24. String os = prop.getProperty("os.name");
  25. def cmdExt = os.contains("Windows") ? '.exe' : ''
  26. def argv = []
  27. argv << '-f' //overwrite existing outfile.zip
  28. // argv << '-z' //recompress using Zopfli
  29. argv << '-v' //verbose output
  30. argv << '4' //alignment in bytes, e.g. '4' provides 32-bit alignment
  31. argv << "$mOutPutDir/workdir/sorceapk/$mSourceApkName"
  32. argv << "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk" //output
  33. project.exec {
  34. commandLine "${sdkDir}/build-tools/${project.android.buildToolsVersion}/zipalign${cmdExt}"
  35. args argv
  36. }
  37. apkFile = new File("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
  38. if (!apkFile.exists()) {
  39. throw new FileNotFoundException("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
  40. }
  41. } else {
  42. throw new InvalidUserDataException('$ANDROID_HOME is not defined')
  43. }
  44. }
  45. }
  46. //对齐
  47. void alignApk() {
  48. println "alignApk"
  49. def fp = new File("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
  50. if (fp.exists()){
  51. fp.delete()
  52. }
  53. def args = [mZipalignToolPath,
  54. '-f',
  55. '-v',
  56. '4',
  57. "$mOutPutDir/workdir/sorceapk/$mSourceApkName",
  58. "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk"]
  59. println("zipalign...");
  60. def proc = args.execute()
  61. println "${proc.text}"
  62. }
  63. //签名
  64. void signCusApk(def signingConfigs){
  65. println "signApk"
  66. println "delete META-INF start"
  67. def args = [mZip7ToolPath.replaceAll('/','\\\\'),
  68. 'd',
  69. ("$mOutPutDir/workdir/sorceApk/"+mSourceApkName).replaceAll('/','\\\\'),
  70. "META-INF"]
  71. def proc = args.execute()
  72. println "${proc.text}"
  73. println "delete META-INF end"
  74. args = [JavaEnvUtils.getJdkExecutable('jarsigner'),
  75. '-verbose',
  76. '-sigalg', 'MD5withRSA',
  77. '-digestalg', 'SHA1',
  78. '-sigfile', 'CERT',
  79. '-tsa', 'http://timestamp.comodoca.com/authenticode',
  80. '-keystore', signingConfigs.storeFile,
  81. '-keypass', signingConfigs.keyPassword,
  82. '-storepass', signingConfigs.storePassword,
  83. "$mOutPutDir/workdir/sorceApk/$mSourceApkName",
  84. signingConfigs.keyAlias]
  85. println("JavaEnvUtils.getJdkExecutable...");
  86. proc = args.execute()
  87. println "${proc.text}"
  88. }
  89. //替换原始的二进制化AndroidManifest
  90. void replaceApkAndroidManifest() {
  91. println "replaceApkAndroidManifest"
  92. def args = [mZip7ToolPath.replaceAll('/','\\\\'),
  93. 'u',
  94. '-y',
  95. ("$mOutPutDir/workdir/sorceApk/"+mSourceApkName).replaceAll('/','\\\\'),
  96. ("$mOutPutDir/workdir/tempDir/AndroidManifest.xml").replaceAll('/','\\\\')]
  97. def proc = args.execute()
  98. println "${proc.text}"
  99. }
  100. //提取二进制化AndroidManifest
  101. void unzipAndroidManifest() {
  102. println "unzipAndroidManifest"
  103. String apkPath = "$mOutPutDir/workdir/tempDir/app-modify-temp.apk"; // apk文件路径
  104. ZipFile zf = new ZipFile(apkPath); // 建立zip文件
  105. InputStream is = zf.getInputStream(zf.getEntry("AndroidManifest.xml")); // 得到AndroidManifest.xml文件
  106. File targetFile = new File("$mOutPutDir/workdir/tempDir/AndroidManifest.xml");
  107. if (targetFile.exists()){
  108. targetFile.delete()
  109. }
  110. targetFile.createNewFile();
  111. FileOutputStream out = new FileOutputStream(targetFile);
  112. int length = 0;
  113. byte[] readByte =new byte[1024];
  114. try {
  115. while((length=is.read(readByte,0,1024))!=-1){
  116. out.write(readByte, 0, length);
  117. }
  118. } catch (Exception e2) {
  119. println "解压文件失败!"
  120. // logger.error("解压文件失败!",e2);
  121. }finally {
  122. is.close();
  123. out.close();
  124. zf.close()
  125. }
  126. if (targetFile.length() <= 0){
  127. throw new Throwable("$mOutPutDir/workdir/tempDir/AndroidManifest.xml unzipAndroidManifest error!!!")
  128. }
  129. }
  130. //打包apk,主要是实现AndroidManifest二进制化
  131. void packageApk(){
  132. println "packageApk"
  133. def o = new File("$mOutPutDir/workdir/tempDir");
  134. o.deleteDir()
  135. o.mkdirs()
  136. Process p="$mApkToolPath b $mOutPutDir/workdir/decodeapk -o $mOutPutDir/workdir/tempDir/app-modify-temp.apk".execute()
  137. println "${p.text}"
  138. def fp = new File("$mOutPutDir/workdir/tempDir/app-modify-temp.apk")
  139. if (!fp.exists()){
  140. throw new Throwable("$mOutPutDir/workdir/tempDir/app-modify-temp.apk" + "not found !! packageApk error!!!")
  141. }
  142. }
  143. //修改AndroidManifest.xml的配置metaData
  144. boolean modifyMetaDataXML(Map<String,String> metaData) {
  145. println "modifyAMXML"
  146. println "metaData:"+metaData.toMapString()
  147. println "metaData:"+metaData.toMapString()
  148. if (metaData.size() <= 0) {
  149. println "mMetaSet size<= 0"
  150. return false;
  151. }
  152. DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  153. // 如果创建的解析器在解析XML文档时必须删除元素内容中的空格,则为true,否则为false
  154. dbf.setIgnoringElementContentWhitespace(false);
  155. try {
  156. /*
  157. * 创建文件对象
  158. */
  159. DocumentBuilder db = dbf.newDocumentBuilder();// 创建解析器,解析XML文档
  160. Document doc = db.parse("$mOutPutDir/workdir/decodeapk/AndroidManifest.xml");
  161. // 使用dom解析xml文件
  162. /*
  163. * 历遍列表,进行XML文件的数据提取
  164. */
  165. // 根据节点名称来获取所有相关的节点org.w3c.dom.
  166. org.w3c.dom.NodeList sonlist = doc.getElementsByTagName("meta-data");//
  167. println "sonlist:" + sonlist.length
  168. // println "getAttributeNode:" + doc.getElementsByTagName("meta-data").getAttributeNode("android:name");
  169. for (org.w3c.dom.Node ne : sonlist) {//org.w3c.dom.
  170. org.w3c.dom.NamedNodeMap nnm = ne.attributes
  171. org.w3c.dom.Node metaKey = nnm.getNamedItem("android:name")
  172. // println "metaKey: $metaKey"
  173. if (metaKey != null) {
  174. // println "metaKey: "+metaKey.getNodeValue()
  175. String value = metaData.get(metaKey.getNodeValue())
  176. if (value == null){
  177. value = metaData.get(metaKey.getNodeValue().toLowerCase())
  178. }
  179. // println "mMetaSet: $value"
  180. if (value != null) {
  181. org.w3c.dom.Node metaValue = nnm.getNamedItem("android:value")
  182. metaValue.setNodeValue(value)
  183. println "modify $metaKey to $value"
  184. }
  185. }
  186. }
  187. try {
  188. TransformerFactory transformerFactory = TransformerFactory
  189. .newInstance();
  190. javax.xml.transform.Transformer transformer = transformerFactory.newTransformer();
  191. DOMSource source = new DOMSource(doc);
  192. StreamResult streamResult = new StreamResult(new File(
  193. "$mOutPutDir/workdir/decodeapk/AndroidManifest.xml"));
  194. transformer.transform(source, streamResult);
  195. } catch (Exception e) {
  196. e.printStackTrace();
  197. throw e;
  198. }
  199. } catch (Exception e) {
  200. e.printStackTrace();
  201. throw e;
  202. }
  203. }
  204. void decodeApk(){
  205. println "decodeApk"
  206. def outDir = new File("$mOutPutDir/workdir/decodeapk")
  207. outDir.deleteDir()
  208. Process p="$mApkToolPath d -f $mSourceApkPath -o $mOutPutDir/workdir/decodeapk".execute()
  209. println "${p.text}"
  210. File fp = new File("$mOutPutDir/workdir/decodeapk/AndroidManifest.xml")
  211. if (!fp.exists()){
  212. throw Exception("$mOutPutDir/workdir/decodeapk/AndroidManifest.xml not exist!!!error")
  213. }
  214. }
  215. }
  216. class ChannelBuildPluginExtension {
  217. String sourceApkPath
  218. String outPutDir
  219. String apkToolPath
  220. String zip7ToolPath
  221. String zipalignToolPath
  222. Map<String,String> metaSet
  223. String keystore
  224. String alia
  225. String storepass
  226. String channelConfig
  227. void channel(Closure clos){
  228. closure = clos
  229. }
  230. }

 下面是在主工程的脚本配置:

  1. apply plugin:ChannelBuildPlugin
  2. buildparam{
  3. sourceApkPath = "F:/svn/tv/app/app-release.apk"
  4. outPutDir = "F:/svn/tv/app"
  5. apkToolPath = "F:/svn/tv/app/apktool.bat"
  6. zip7ToolPath = "C:/Program Files/7-Zip/7z.exe"
  7. }

这样可以直接使用autoBuildChannelProduct这个任务即可编译所有的渠道的包。

说明: 

   1.AndroidManifest.xml的metaData的key与manifestPlaceholders的key要对应,可以大小写不同;

   2.android studio配置了自动签名,不然需要手动配置签名信息。

使用的场景特别是需要热修复的情况,在多渠道的基准包中,必须要保持基准包的类及资源除AndroidManifest外都必须一样的环境,这样能保证所有渠道包均能热修复;

后续改进点:

  1.不能修改applicationId、版本号等

  2.不能使用默认配置的,每个渠道都必须配置完所有的metaData信息



声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/253055
推荐阅读
相关标签
  

闽ICP备14008679号