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

本文介绍了一种在已编译的APK基础上批量生成不同渠道版本的方法,主要通过对AndroidManifest.xml中的metaData进行修改实现。该过程包括使用apktool反编译、修改metaData、重新编译并替换AndroidManifest.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;脚本代码如下:

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信息



<think>我们正在解决Unity中AndroidManifest.xml文件缺失的问题。根据用户描述,Unity安装目录下的AndroidPlayer/Apk文件夹中缺少AndroidManifest.xml文件。这个问题通常出现在Unity安装不完整或文件被误删的情况下。解决方案:1.重新安装Unity(最彻底,但耗时)2.从其他相同版本的Unity安装目录复制3.使用Unity默认模板生成(如果Unity编辑器支持)不过,根据引用[2]和[3]的提示,我们可以这样处理:引用[2]提到:Unity在打包时会合并多个AndroidManifest.xml(包括默认的和项目中Plugins/Android下的)。所以如果只是默认的AndroidManifest.xml缺失,我们可以通过在项目中的Assets/Plugins/Android目录下放置一个完整的AndroidManifest.xml来解决(这个文件会覆盖Unity默认的)。另外,引用[1]提到:UnityPlayerActivity的位置,这告诉我们Unity的核心类在哪里,虽然与AndroidManifest.xml无直接关系,但可以知道Unity的安装结构。具体步骤:方法1(推荐):在Unity项目内部创建AndroidManifest.xml-在项目的Assets目录下创建Plugins/Android文件夹(如果不存在的话)。-在Android文件夹中创建AndroidManifest.xml文件。-内容可以从以下来源获取:a.从Unity的安装目录的默认位置复制(如果其他相同版本的Unity有的话)。b.或者自己编写一个基本的AndroidManifest.xml(内容可参考Unity默认模板,下面会提供示例)。方法2:从其他相同版本的Unity复制-如果其他电脑上有相同版本的Unity,可以从它的安装目录的对应位置复制AndroidManifest.xml到你的Unity安装目录的相同位置。方法3:重新安装Unity(如果上述方法不奏效)。关于默认AndroidManifest.xml的示例内容:由于不同Unity版本可能内容不同,这里提供一个较通用的模板(适用于Unity2019以上),但请注意,最好还是从相同版本的Unity中获取。示例模板:```xml<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"package="com.unity3d.player"android:installLocation="preferExternal"android:versionCode="1"android:versionName="1.0"><uses-sdkandroid:minSdkVersion="19"android:targetSdkVersion="28"/><applicationandroid:icon="@drawable/app_icon"android:label="@string/app_name"android:debuggable="true"><activityandroid:name="com.unity3d.player.UnityPlayerActivity"android:label="@string/app_name"android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density"android:hardwareAccelerated="false"><intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter><meta-dataandroid:name="unityplayer.UnityActivity"android:value="true"/></activity></application><uses-permissionandroid:name="android.permission.INTERNET"/></manifest>```注意:上述模板中的targetSdkVersion可能需要根据你的需求修改(例如现在一般要求30+),其他权限也需要根据项目需要添加。但是,如果项目中的Plugins/Android目录下已经有一个AndroidManifest.xml,那么Unity就会使用这个而忽略默认的。所以我们可以通过创建这个文件来避免使用安装目录下的默认文件(这样即使默认文件缺失也没关系)。因此,总结处理步骤:1.在Unity项目中的路径:Assets/Plugins/Android/下创建AndroidManifest.xml文件。2.将上面的模板内容复制进去,并根据项目需求进行修改(例如包名、权限、Activity等)。3.重新构建项目,此时Unity会使用你提供的这个AndroidManifest.xml,而不会去寻找安装目录下的默认文件(即使它缺失了)。此外,如果项目中已经有了这个文件,那么可以检查一下它的内容是否正确,因为有时候可能是文件内容错误导致打包失败。如果按照以上步骤操作后,问题依旧,请检查Unity编辑器版本是否过低(引用[3]提到:旧版本Unity可能因为AndroidSDK更新而出现命令失效,需要降级AndroidSDKtools)。但这是另一个问题,和缺少AndroidManifest.xml没有直接关系。最后,如果还有打包错误,可以查看错误信息,并根据引用[3]中的提示,检查AndroidSDKtools的版本问题。生成相关问题:1.如何修改Unity项目的包名(PackageName)?2.如何在Unity中为Android应用添加自定义权限?3.解决Unity打包Android时出现的AndroidSDK工具版本兼容性问题?</think> ### 解决Unity中AndroidManifest.xml缺失问题的步骤 当Unity安装目录下缺少`AndroidPlayer/Apk/AndroidManifest.xml`文件时,可按以下步骤处理: #### 1. **验证文件路径** 默认路径应为(根据操作系统): - Windows: `C:\Program Files\Unity\Hub\Editor\<版本号>\Editor\Data\PlaybackEngines\AndroidPlayer\Apk\AndroidManifest.xml` - macOS: `/Applications/Unity/Hub/Editor/<版本号>/Unity.app/Contents/PlaybackEngines/AndroidPlayer/Apk/AndroidManifest.xml` --- #### 2. **解决方案** ##### 方法1:从Unity工程生成 ```bash # 在Unity编辑器中操作: 1. 打开: File > Build Settings > Android 2. 勾选: ✔ Export Project 3. 点击: Export 4. 在导出目录查看: unityLibrary/src/main/AndroidManifest.xml ``` 导出后可在`unityLibrary/src/main/`找到有效清单文件[^2] ##### 方法2:手动创建模板文件 创建`Assets/Plugins/Android/AndroidManifest.xml`并添加基础内容: ```xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.yourcompany.yourapp"> <application android:icon="@mipmap/app_icon" android:label="@string/app_name"> <activity android:name="com.unity3d.player.UnityPlayerActivity" android:launchMode="singleTask" android:configChanges="orientation|keyboardHidden|screenSize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.INTERNET" /> </manifest> ``` ##### 方法3:修复Unity安装 - 通过Unity Hub重装Android模块: ``` 1. 打开Unity Hub 2. 选择安装 > 对应版本 3. 勾选: ✔ Android Build Support 4. 点击Reinstall ``` --- #### 3. **验证修复结果** - ✅ 构建时检查日志:`Merging AndroidManifest...`应出现 - ✅ 解压APK确认:`assets/bin/Data/Managed/AndroidManifest.xml`存在 - ✅ 运行时检查:`adb logcat | grep -i manifest` > **关键提示**:项目中的`Assets/Plugins/Android/AndroidManifest.xml`优先级高于Unity默认清单[^2],缺失时主动创建即可正常工作。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值