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

在已经编译出一个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;脚本代码如下:

class ChannelBuildPlugin implements Plugin<Project> {

    String mSourceApkPath
    String mOutPutDir
    String mApkToolPath
    String mZip7ToolPath
    String mZipalignToolPath
    String mKeystore
    String mAlia
    String mStorepass
    String mSourceApkName
    String mProductName
    String mApplicationId
    void apply(Project project) {

        project.extensions.create("buildparam", ChannelBuildPluginExtension)

        project.task('autoBuildChannelProduct') << {

            println "autoBuildChannelProduct start "
            if (project.buildparam.sourceApkPath == null) {
                println "error !!!sourceApkPath == null"
                return
            }
            mSourceApkPath = project.buildparam.sourceApkPath
            File fp = new File(mSourceApkPath)
            if (!fp.exists()){
                throw new FileNotFoundException(mSourceApkPath)
            }
            mSourceApkName = fp.getName()
            mOutPutDir = project.buildparam.outPutDir
            File outDir = new File(mOutPutDir)
            if (!outDir.exists()){
                outDir.mkdirs()
            }
            mApkToolPath = project.buildparam.apkToolPath
            mZipalignToolPath = project.buildparam.zipalignToolPath
            mZip7ToolPath = project.buildparam.zip7ToolPath

            mKeystore = project.buildparam.keystore
            mAlia = project.buildparam.alia
            mStorepass = project.buildparam.storepass
            def signingConfigs

            project.copy {
                from "$mSourceApkPath"
                into "$mOutPutDir/workdir/sorceapk"
            }

            decodeApk()

            project.android.applicationVariants.all { variant -
                if (variant.name.contains("Release")){
                    mProductName = variant.flavorName;
                    signingConfigs = variant.getSigningConfig()


                    def metaConfig
                    mApplicationId = variant.productFlavors.applicationId[0]
                    println "applicationId:"+ mApplicationId


                    for (def item:variant.productFlavors.manifestPlaceholders){
                        metaConfig = item;
                    }


                    modifyMetaDataXML(metaConfig)
                    packageApk()
                    unzipAndroidManifest()
                    replaceApkAndroidManifest()
                    signCusApk(signingConfigs)
                    zipalign(project)


                    project.copy {
                        String targetApk = "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk"
                        if (mApkMd5 != null && !mApkMd5.equals("")){
                            targetApk = "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_$mApkMd5"+".apk"
                        }
                        from "$targetApk"
                        into "$mOutPutDir"
                    }
                }        
    }
        //重新签名
        project.task('signApk') << {
        }
    }

    public void zipalign(Project project) {
        def apkFile = new File("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
        if (apkFile.exists()){
            apkFile.delete()
        }
        apkFile = new File("$mOutPutDir/workdir/sorceapk/$mSourceApkName")
        if (apkFile.exists()) {
            def sdkDir
            Properties properties = new Properties()
            File localProps = project.rootProject.file("local.properties")
            if (localProps.exists()) {
                properties.load(localProps.newDataInputStream())
                sdkDir = properties.getProperty("sdk.dir")
            } else {
                sdkDir = System.getenv("ANDROID_HOME")
            }
            if (sdkDir) {


                Properties prop = System.getProperties();
                String os = prop.getProperty("os.name");
                def cmdExt = os.contains("Windows") ? '.exe' : ''

                def argv = []
                argv << '-f'    //overwrite existing outfile.zip
                // argv << '-z'    //recompress using Zopfli
                argv << '-v'    //verbose output
                argv << '4'     //alignment in bytes, e.g. '4' provides 32-bit alignment
                argv << "$mOutPutDir/workdir/sorceapk/$mSourceApkName"

                argv << "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk"  //output

                project.exec {
                    commandLine "${sdkDir}/build-tools/${project.android.buildToolsVersion}/zipalign${cmdExt}"
                    args argv
                }
                apkFile = new File("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
                if (!apkFile.exists()) {
                    throw new FileNotFoundException("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
                }
            } else {
                throw new InvalidUserDataException('$ANDROID_HOME is not defined')
            }
        }
    }
    //对齐
    void alignApk() {
        println "alignApk"
        def fp = new File("$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk")
        if (fp.exists()){
            fp.delete()
        }
        def args = [mZipalignToolPath,
                    '-f',
                    '-v',
                    '4',
                    "$mOutPutDir/workdir/sorceapk/$mSourceApkName",
                    "$mOutPutDir/workdir/sorceapk/"+mProductName +"_app-release_aligned"+".apk"]

        println("zipalign...");
        def proc = args.execute()
        println "${proc.text}"

    }

    //签名
    void signCusApk(def signingConfigs){
        println "signApk"
        println "delete META-INF start"
        def args = [mZip7ToolPath.replaceAll('/','\\\\'),
                    'd',
                    ("$mOutPutDir/workdir/sorceApk/"+mSourceApkName).replaceAll('/','\\\\'),
                    "META-INF"]
        def proc = args.execute()
        println "${proc.text}"
        println "delete META-INF end"

        args = [JavaEnvUtils.getJdkExecutable('jarsigner'),
                    '-verbose',
                    '-sigalg', 'MD5withRSA',
                    '-digestalg', 'SHA1',
                    '-sigfile', 'CERT',
                    '-tsa', 'http://timestamp.comodoca.com/authenticode',
                    '-keystore', signingConfigs.storeFile,
                    '-keypass', signingConfigs.keyPassword,
                    '-storepass', signingConfigs.storePassword,
                    "$mOutPutDir/workdir/sorceApk/$mSourceApkName",
                    signingConfigs.keyAlias]
        println("JavaEnvUtils.getJdkExecutable...");
        proc = args.execute()
        println "${proc.text}"
    }

    //替换原始的二进制化AndroidManifest
    void replaceApkAndroidManifest() {
        println "replaceApkAndroidManifest"
        def args = [mZip7ToolPath.replaceAll('/','\\\\'),
                    'u',
                    '-y',
                    ("$mOutPutDir/workdir/sorceApk/"+mSourceApkName).replaceAll('/','\\\\'),
                    ("$mOutPutDir/workdir/tempDir/AndroidManifest.xml").replaceAll('/','\\\\')]
        def proc = args.execute()
        println "${proc.text}"
    }

    //提取二进制化AndroidManifest
    void unzipAndroidManifest() {
        println "unzipAndroidManifest"

        String apkPath = "$mOutPutDir/workdir/tempDir/app-modify-temp.apk"; // apk文件路径
        ZipFile zf = new ZipFile(apkPath); // 建立zip文件
        InputStream is = zf.getInputStream(zf.getEntry("AndroidManifest.xml")); // 得到AndroidManifest.xml文件

        File targetFile = new File("$mOutPutDir/workdir/tempDir/AndroidManifest.xml");
        if (targetFile.exists()){
            targetFile.delete()
        }
        targetFile.createNewFile();

        FileOutputStream out = new FileOutputStream(targetFile);
        int length = 0;
        byte[] readByte =new byte[1024];
        try {
            while((length=is.read(readByte,0,1024))!=-1){
                out.write(readByte, 0, length);
            }
        } catch (Exception e2) {
            println "解压文件失败!"
            //  logger.error("解压文件失败!",e2);
        }finally {
            is.close();
            out.close();
            zf.close()
        }

        if (targetFile.length() <= 0){
            throw new Throwable("$mOutPutDir/workdir/tempDir/AndroidManifest.xml unzipAndroidManifest error!!!")
        }
    }

    //打包apk,主要是实现AndroidManifest二进制化
    void packageApk(){
        println "packageApk"
        def o = new File("$mOutPutDir/workdir/tempDir");
        o.deleteDir()
        o.mkdirs()

        Process p="$mApkToolPath b $mOutPutDir/workdir/decodeapk -o $mOutPutDir/workdir/tempDir/app-modify-temp.apk".execute()
        println "${p.text}"
        def fp = new File("$mOutPutDir/workdir/tempDir/app-modify-temp.apk")
        if (!fp.exists()){
            throw new Throwable("$mOutPutDir/workdir/tempDir/app-modify-temp.apk" + "not found !! packageApk error!!!")
        }
    }

    //修改AndroidManifest.xml的配置metaData
    boolean modifyMetaDataXML(Map<String,String> metaData) {
        println "modifyAMXML"
        println "metaData:"+metaData.toMapString()
        println "metaData:"+metaData.toMapString()
        if (metaData.size() <= 0) {
            println "mMetaSet size<= 0"
            return false;
        }

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        // 如果创建的解析器在解析XML文档时必须删除元素内容中的空格,则为true,否则为false
        dbf.setIgnoringElementContentWhitespace(false);
        try {
            /*
             * 创建文件对象
             */
            DocumentBuilder db = dbf.newDocumentBuilder();// 创建解析器,解析XML文档
            Document doc = db.parse("$mOutPutDir/workdir/decodeapk/AndroidManifest.xml");
            // 使用dom解析xml文件

            /*
             * 历遍列表,进行XML文件的数据提取
             */
            // 根据节点名称来获取所有相关的节点org.w3c.dom.
            org.w3c.dom.NodeList sonlist = doc.getElementsByTagName("meta-data");//
            println "sonlist:" + sonlist.length

            // println "getAttributeNode:" + doc.getElementsByTagName("meta-data").getAttributeNode("android:name");
            for (org.w3c.dom.Node ne : sonlist) {//org.w3c.dom.
                org.w3c.dom.NamedNodeMap nnm = ne.attributes
                org.w3c.dom.Node metaKey = nnm.getNamedItem("android:name")
                //  println "metaKey: $metaKey"
                if (metaKey != null) {
                    //  println "metaKey: "+metaKey.getNodeValue()
                    String value = metaData.get(metaKey.getNodeValue())
                    if (value == null){
                        value = metaData.get(metaKey.getNodeValue().toLowerCase())
                    }
                    // println "mMetaSet: $value"
                    if (value != null) {
                        org.w3c.dom.Node metaValue = nnm.getNamedItem("android:value")
                        metaValue.setNodeValue(value)
                        println "modify $metaKey to $value"
                    }
                }
            }

            try {

                TransformerFactory transformerFactory = TransformerFactory
                        .newInstance();
                javax.xml.transform.Transformer transformer = transformerFactory.newTransformer();
                DOMSource source = new DOMSource(doc);
                StreamResult streamResult = new StreamResult(new File(
                        "$mOutPutDir/workdir/decodeapk/AndroidManifest.xml"));
                transformer.transform(source, streamResult);

            } catch (Exception e) {
                e.printStackTrace();
                throw e;
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }

    }

    void decodeApk(){
        println "decodeApk"
        def outDir = new File("$mOutPutDir/workdir/decodeapk")
        outDir.deleteDir()

        Process p="$mApkToolPath d -f $mSourceApkPath -o $mOutPutDir/workdir/decodeapk".execute()
        println "${p.text}"
        File fp = new File("$mOutPutDir/workdir/decodeapk/AndroidManifest.xml")
        if (!fp.exists()){
            throw Exception("$mOutPutDir/workdir/decodeapk/AndroidManifest.xml not exist!!!error")
        }
    }
}


class ChannelBuildPluginExtension {
    String sourceApkPath
    String outPutDir
    String apkToolPath
    String zip7ToolPath
    String zipalignToolPath
    Map<String,String>  metaSet
    String keystore
    String alia
    String storepass
    String channelConfig
    void channel(Closure clos){
        closure = clos

    }
}

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

apply plugin:ChannelBuildPlugin


buildparam{
    sourceApkPath = "F:/svn/tv/app/app-release.apk"
    outPutDir = "F:/svn/tv/app"
    apkToolPath = "F:/svn/tv/app/apktool.bat"
    zip7ToolPath = "C:/Program Files/7-Zip/7z.exe"
}

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

说明: 

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

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

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

后续改进点:

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

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



  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值