美文网首页Android技术分享交流区别人家的Android开发经验Android
一套源码编译多个APP,不同的签名,包名,界面,字段...

一套源码编译多个APP,不同的签名,包名,界面,字段...

作者: 隔壁王较瘦 | 来源:发表于2017-10-12 10:32 被阅读1195次

    一套源码编译多个 APP,不同签名,不同接口等配置

    线上源码地址,结合源码看比较容易理解 源码地址

    作为一个刚入行一两年的 代码 Copy 者 来说,这么久没有深入了解 Gradle 真的是一大遗憾,熟悉了 Gradle 之后开发效率能够提高很多,当然,现在我还没有到那个地步,这篇文章就是我实际开发中的需求,刚开始的时候用的是最基本的开发方式,后来东西越来越多,渐渐的感觉这种开发方式太拖慢进度了,然后就搜索学习一番,没有理解,只是学习了基本的操作来解决开发中的需求,特写篇文章记录一下,怕忘记。

    emmm, 我可能有一个这样的需求,不知道可不可以...

    我的需求很简单...

    • 小张是一个四肢健全,各项功能正常的人,我需要根据这个人克隆出更多人

    我的目的很明确...

    • 我要跟别人长得不一样(界面不一样)
    • 我要身体会特殊的技能(额外功能)
    • 我要改名身份证上的名字(APP 名字)
    • 我要换身份证上的头像 (APP 桌面图标)
    • 我要换身份证号码 (APP 包名)
    • 我要换肤色 (不同的界面颜色,icon等)
    • 我要不同的签证 (不同的应用签名)

    ennn, 我可能有这样一种解决办法, 应该可以...

    1.首先,我们得有个小张这个人

    小张的基本信息是这样

    信息描述 基本信息
    身份证头像 (APP 名字) 小张
    身份证头像 (APP 头像)
    身份证号码 (APP 包名) com.persons.zhang

    小张是长这样的

    小张

    这是小张的源码

    package com.persons.zhang
    
    import android.os.Bundle
    import android.support.design.widget.BottomNavigationView
    import android.support.v4.app.Fragment
    import android.support.v4.view.ViewPager
    import android.support.v7.app.AppCompatActivity
    import android.util.Log
    import com.persons.zhang.adapter.MainViewPagerAdapter
    import com.persons.zhang.fragment.FootFragment
    import com.persons.zhang.fragment.HandFragment
    import com.persons.zhang.fragment.HeadFragment
    import kotlinx.android.synthetic.main.activity_main.*
    
    class MainActivity : AppCompatActivity() {
    
        private val mOnNavigationItemSelectedListener =              BottomNavigationView.OnNavigationItemSelectedListener { item ->
            when (item.itemId) {
                R.id.navigation_home -> {
                    viewPager.currentItem = 0
                    return@OnNavigationItemSelectedListener true
                }
                R.id.navigation_dashboard -> {
                    viewPager.currentItem = 1
                    return@OnNavigationItemSelectedListener true
                }
                R.id.navigation_notifications -> {
                    viewPager.currentItem = 2
                    return@OnNavigationItemSelectedListener true
                }
            }
            false
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
    
            var fragments : MutableList<Fragment> = mutableListOf()
    
            fragments.add(HeadFragment())
            fragments.add(HandFragment())
            fragments.add(FootFragment())
    
            viewPager.adapter = MainViewPagerAdapter(supportFragmentManager,fragments)
            viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{
    
                override fun onPageScrollStateChanged(state: Int) {
                }
    
                override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                }
    
                override fun onPageSelected(position: Int) {
                    navigation.menu.getItem(position)?.setChecked(true)
                }
    
            })
    
        }
    }
    

    这是小张的布局

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.persons.zhang.MainActivity">
    
        <android.support.v4.view.ViewPager
            android:id="@+id/viewPager"
            app:layout_constraintBottom_toTopOf="@id/navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    
        <android.support.design.widget.BottomNavigationView
            android:id="@+id/navigation"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="0dp"
            android:layout_marginStart="0dp"
            android:background="?android:attr/windowBackground"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:menu="@menu/navigation" />
    
    </android.support.constraint.ConstraintLayout>
    

    2.我们要在小张的基础上增加出来一个小李

    这是我们要增加的小李的大概需求

    • 名字要不一样 (APP 名字)
    • 身份证号码要不一样 (APP 包名)
    • 身份证头像要不一样 (APP 图标)
    • 肤色不一样 (界面颜色)
    • 头跟别人不一样 (主界面的头)

    生产之前的准备工作

    我们把 Android 切换到 Project
    模式.png

    1、新建关于包括小李一切的目录

    看着上图,对比着看啊,上图中的 zhang -> app -> src 目录上 右键 -> New -> Directory 新建一个小李的目录,我这里就叫做小李了,下面是图操作

    directory.png

    然后,我们对比着 src/main 这个目录里面的结构,在 li 这个目录下创建一模一样的结构,如下图

    这是li 目录下新建的 这是main 的结构
    li_directory.png

    好了,新建完成

    2. 新建针对小李的Gradle构建变体

    我也理解不清楚,大概就是把小李这个人当成备胎,这样这个人什么时候想换备胎了,小李这个人可以是其中一个。就是在编译的时候多一个小李的选项,可以单独针对小李进行编译,也就是备胎转正。

    我们在 app的gradle下 添加如下代码

    android {
        compileSdkVersion 26
        buildToolsVersion "26.0.2"
        defaultConfig {
            applicationId "com.persons.zhang"
            minSdkVersion 15
            targetSdkVersion 26
            versionCode 1
            versionName "1.0"
        }
        ...
      
      // 重点在这下面这些
        flavorDimensions "app"
    
      // 重点--------------------------
        productFlavors {
            li {
                dimension "app"
            }
    
            zhang {
                dimension "app"
            }
        }
    }
    

    注意看 productFlavors 里面的 lizhang ,他们里面都有 applicationIdversionCodeversionName 这些,这些就是针对他们不同的人,有不同的版本,不同的包名。

    这样写的话必须保证我们新建的那个目录要跟 productFlavors 里面的这个 li { ... } 保持一致 ,如果想使用不一样的名字可以按照以下操作来,至于上面的flavorDimensions 我也不太理解,没使用到。

    android {
        compileSdkVersion 26
        buildToolsVersion "26.0.2"
        ...
          
        sourceSets {
            li {
                java.srcDir('src/li/java')
                res.srcDir('src/li/res')
            }
        }
    }
    

    这样显示指定源码路径,名字不一致也无所谓了。

    这样我们就新建出来了一个小李 ,但是新建出来了我们怎么才能把默认点击 Run 安装的 debug 版本的app改成 小李呢,如果上面已经一步步配置好了之后,会有下图这些东西

    build_小李.png

    选择完成之后再点击 Run 安装的就是小李的 debug版本了

    此时的小李还是和小张一模一样的,下面我们要把小李 改成他该有的模样。

    3.不一样的名字

    既然我们这个项目都叫做小李了,那我们不能名字还是小张的名字吧,下面我们来给小李改改名字,一般情况下我们的名字都是在 strings.xml 里面的 app_name 字段来定义的,当然不排除你直接在 Manifest 里面写死,那你很棒棒哦

    我们的 strings.xml 现在只有 src/main/res/values 里面才有,我们要做的是在 src/li/res/values 里面新建一个 strings.xml ,里面新建一个 app_name 的字段,值为 小李 ,如下图

    小张(也就是main) 小李(也就是li)
    main_app_name.png li_app_name.png
    app_name_zhang.png app_name_li.png

    不知道你注意到没有,小张(main)strings.xml 里面还有 head 等字段, 小李(li) 却没有

    原因 :资源文件会自动合并。就这么简单,就比如说,现在 里面有 head 这些, 没有,等后面我们修改过,资源文件合并之后会变成以下状态

    li_strings.png

    相同的被替换,不同的被增加 ,相同的会被替换成当前目标的,比如说你现在小张有app_name小李

    也有,如果现在编译目标 是小李的话,app_name 的值就会是小李,好了我们来运行看一下效果

    小李.jpg

    好了,看来我们修改成功了,上面小张这个APP是我改了些东西保留下来的,为了作对比,正常情况下以前装的小张会被小李覆盖掉, 下面我们来看一下小李的主界面长什么样子

    小李_home.png

    可以看到我们的app_name 已经修改成功了,其他没修改的还是按照小张的样子来展示的,包括下面的头,手,脚,这些。

    3.身份证号码不一样(包名不一样)

    上面我说了,小张被保留下来是我改了一些东西才能保留下来和小李作对比的,正常情况下安装了小李之后,小张就会被覆盖掉,因为他们的包名一样的。

    下面我们来更改包名让 App 共存,我们的操作还是在 app/gradle 里面的 productFlavors 进行更改。

    android {
        compileSdkVersion 26
        buildToolsVersion "26.0.2"
        ...
          
        productFlavors {
            li {
                applicationId "com.persons.li"
                versionCode 2
                dimension "app"
                versionName "1.0.2"
            }
    
            zhang {
                applicationId "com.persons.zhang"
                versionCode 1
                dimension "app"
                versionName "1.0"
            }
        }
    }
    

    仔细看,我每个都加了 applicationId ,顺便还加了 versionCodeversionName, 这可以单独设置每个app不同的版本号,不同的版本名称,不同的包名,怕晕了,给个截图让你瞅瞅

    applicationId.png

    4.身份证头像不一样 (APP 图标)

    我们把版本号修改了,现在我们安装了之后就可以两个APP共存了,而且包名也是不相同的,这样基本上实现两个人就可以出现在一个手机里面了。

    但是只是名字改了,图标还是一样的,猛一看,很懵逼啊,这一不小心就点错了,多尴尬

    众所周知,我们的app图标是放在 res/mipmap 目录下的, 我们之前拷贝的 li 目录里面是和 小张(main) 的是一样的,我们在准备工作的时候还设置了 sourceSet ,而且,前面还说了相同名字的资源被覆盖,不同的被增加 ,我们要做的只是把 小李(li) 的图标放在 li/src/res/mipmap 下就可以了, 然后运行安装到手机

    li_mipmap.png

    下面看改过的效果

    li.png

    好了,替换完成了。

    5.不同的肤色

    我们现在小李的肤色还跟小张的肤色是一样的,下面我们来改一下,一切关于资源的要么被替换,要么被合并

    我们要改的是 li/res/values/colors.xml , 因为现在小李这个目录下面还没有 colors.xml 这个文件,我们直接从 小张(main) 的目录里面 copy 过来一个,改一下里面的色值就可以了。

    小张的肤色

    main_color.png

    小李的肤色

    li_color.png

    小李改过肤色的样子

    li_home_color.png

    好了,肤色也改完了。

    6.头界面不一样(重点)

    现在小李的头界面还是上面这张图的界面,什么也没有。我们要做的是在头这个界面里面加上眼睛,鼻子,小张是个无面男 ,我们要小李有个鼻子,点击鼻子的时候弹出一个提示。

    注意: 这个方法是通用的,就是说,只要是关于 java 代码的都按照这样的套路来做,把要修改的那个类拷贝出来,把基础包里面的删掉,给其他的目录里面也拷贝进去这个类,要不你在编译其他目录的apk的时候会报错的,找不到类。

    1.首先我们先把小李的头copy过来。

    为什么要拷贝过来? 不拷贝过来怎么会有自己的头,如果没有拷贝过来,默认使用的是小张的头。

    注意 :这时我们需要把源码大改一下了,我们要把原本小张的源码给改成通用的,就是把原来小张的源码当成一个基础包,我们要是加人就需要另外新建一个目录来做,就跟我们增加小李一样的。

    下面是我们改过的目录结构

    project.png

    可以看到,我们单独把 小张 给新建了一个目录,他的存在方式跟 小李 的是一样的。

    为了以防万一,我们还要在 app/gradle 里面修改一下 sourceSet 保证我们的APP源码指向是正确的。

        sourceSets {
            li {
                java.srcDir('src/li/java')
                res.srcDir('src/li/res')
                
                // assets.srcDir('src/li/assets')
                // manifest.srcFile('src/li/AndroidManifest.xml')
            }
    
            zhang {
                java.srcDir('src/zhang/java')
                res.srcDir('src/zhang/res')
                
                // assets.srcDir('src/zhang/assets')
                // manifest.srcFile('src/zhang/AndroidManifest.xml')
            }
        }
    

    怕看懵逼,加一张图

    zhang_sourceSet.png

    我们想要小李的头跟其他的不一样的话,需要单独把 copy 出来进行修改,这个跟res 下的文件不一样, res 下的文件相同了会覆盖合并,java 目录下面是会报错的,

    因为基础包目录下已经有了相同名字的类了。你在 li 的里面加了相同的 类, 李使用的源码是用 基础包里的来做基础的, 现在这个状态就相当于 你在同一个包里面,创建了两个相同名字的类

    head_diractory.png
    2. 现在我们给小李的头界面加上鼻子

    首先我们先把 基础包 里面 头 的布局文件 copy 到 li 的layout目录下。

    head_layout.png

    看清楚,是 li 目录下面的 layout 文件夹,拷贝到这里以后我们就跟平常写界面一样修改就可以了。

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:id="@+id/click"
            android:layout_centerInParent="true"
            android:text="我是鼻子"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
    </RelativeLayout>
    
    class HeadFragment : Fragment() {
    
        override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    
            val view = inflater?.inflate(R.layout.fragment_head, container, false)
    
            return view
        }
    
        override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
    
            click.setOnClickListener {
                Toast.makeText(activity,"我的鼻子被点了一下",Toast.LENGTH_LONG).show()
            }
    
        }
    
    }
    
    鼻子.jpg

    就是界面上有一个叫鼻子的按钮,设置点击之后弹出吐司,是不是还很骚气,而我们加上这个鼻子之后,小张还是原来的小张 ,鼻子只是被加在了小李的头上。

    3. 不同的签名文件

    这样好几个app在一块编译,我们在androidstudio里面点击打包还要选择不同的 签名文件,很麻烦,我们还是在 app/src/gradle 里面进行配置就可以了。

    android {
      ....
        
        
        signingConfigs {
    
            // 只是用来做演示,没有特地生成签名文件
    
            li {
                storeFile file("src/li/liKeystore.jks")
                storePassword "li123"
                keyAlias "li"
                keyPassword "li1234"
    
                // 开启 V2 签名
                v2SigningEnabled true
            }
    
            zhang {
                storeFile file("src/zhang/liKeystore.jks")
                storePassword "zhang123"
                keyAlias "zhang"
                keyPassword "zhang1234"
                v2SigningEnabled true
            }
        }
    
        productFlavors {
            li {
                applicationId "com.persons.li"
                versionCode 2
                dimension "app"
                versionName "1.0.2"
    
              // 注意看这里---------------------------------------------
                signingConfig signingConfigs.li
    
            }
    
            zhang {
                applicationId "com.persons.zhang"
                versionCode 1
                dimension "app"
                versionName "1.0"
    
                signingConfig signingConfigs.zhang
            }
        }
         
    }
    

    4. 其他操作

    1.批量打包和运行签名包测试第三方登录或分享,或支付。

    根据我们上面 gradle 的配置,签名打包很方便,打包使用这种方式,上面可以单独打一个,也可以一次打完,打一个就选择第四步其他的名字,比如 assembleZhang 这些。

    打包使用这种方式,上面可以单独打一个,也可以一次打完,打一个就选择第四步其他的名字,比如 assembleZhang 这些。

    打包.png

    2. Manifest value 值设置占位符

    在 Manifest 中

    Manifest.png

    在 Gradle 中

    Manifest_gradle.png

    3. 关于微信回调

    微信的回调是需要放在 包名/wxapi/ 目录下的,我们这样拷贝之后没有了包名都是不一样的,我们要为每个app的 src/java 目录下新建一个和包名一样的目录,比如小张, 我们为了方便区分,把小张的包名改成 cn.zhang ,下面是目录结构

    zhang_application.png wxapi_zhang.png

    到这里基本结束了。

    有什么没写的,写错的,评论告诉我,反正我也不一定改。。。

    相关文章

      网友评论

      • fffb4eaf11c2:留着备用,项目要做马甲包了,到时候试试
      • 科技探索者:android {
        signingConfigs{
        ydce{
        storeFile file("G:\\mapApp_apk\\movementin_some\\ydcc.jks")
        storePassword KEYSTORE_PASSWORD
        keyAlias "ydcc"
        keyPassword KEY_PASSWORD
        // 开启 V2 签名
        v2SigningEnabled true
        }
        ydtl{
        storeFile file("G:\\mapApp_apk\\ydtl\\ydtl.jks")
        storePassword KEYSTORE_PASSWORD
        keyAlias "key0"
        keyPassword KEY_PASSWORD
        // 开启 V2 签名
        v2SigningEnabled true
        }
        }

        compileSdkVersion 25
        buildToolsVersion '26.0.2'
        defaultConfig {
        minSdkVersion 16
        targetSdkVersion 22
        }

        // 重点在这下面这些
        flavorDimensions "app"
        // 重点--------------------------
        productFlavors {
        ydce {
        applicationId "com.movementinsome"
        versionCode 220
        versionName "移动采测V2.2.0"
        dimension "app"
        // 注意看这里---------------------------------------------
        signingConfig signingConfigs.ydce
        }
        ydtl {
        applicationId "com.gddst.leak"
        versionCode 103
        versionName "移动探漏V1.0.3"
        dimension "app"
        // 注意看这里---------------------------------------------
        signingConfig signingConfigs.ydtl
        }
        }
        sourceSets {
        ydce {
        java.srcDir('src/ydce/java')
        res.srcDir('src/ydce/res')
        }
        ydtl{
        java.srcDir('src/ydtl/java')
        res.srcDir('src/ydtl/res')
        }
        }
        隔壁王较瘦:@科技探索者 你是两个APP,基础包有吗
        科技探索者:@隔壁王较瘦 我的清单配置文件是这么写的,好像并没有设置applicationId呀

        package="com.movementinsome"
        android:sharedUserId="com.gddst"
        android:versionCode="169"
        android:versionName="mapApp1.6.9" xmlns:android="http://schemas.android.com/apk/res/android";
        隔壁王较瘦:@科技探索者 manifest里面applicationId删掉试试
      • 科技探索者:大神我按照你写的试了一下下,debug版两个App可以同时装在手机上,但是分别打出正式包之后就不能同时装在手机上了.我是把B项目的代码集成到A项目中来的,打出来的正式包A项目可以升级安装,B项目的不能升级,A,B两个项目也不可以同时装.配置文件如下,请大神帮我看看是什么问题
      • 程序猿男神:小李,新建文件夹下面都是空???这也不能编译啊!
        隔壁王较瘦:@程序猿男神 指定src路径么,图标名字跟存在的目录对应么
        程序猿男神:@隔壁王较瘦 图标替换的没起作用。
        隔壁王较瘦:@程序猿男神 没尝试,不好意思,既然有这个需求了文件夹都不会是空的,你是直接新建了一个APP名字的目录,里面java跟src目录都没有新建吗
      • 詹徐照:文章写得很好。我有个问题:
        两个flavor,有一份代码IDE无法解析:java code 无法变色,无法检查语法错误,无法自动导包。
        作者的截图中我好像也看到的相似的问题,li 目录下的java 包变蓝了,表示已经被ide解析了,zhang目录下的没有变蓝,里面的代码应该都是白的,ide的很多特性用不了。
        不知道作者有没有什么办法解决这个问题?
        詹徐照:IDE只会解析当前选中的flavor。
        当要修改某个flavor时,在屏幕左下角的BuildVariants中选中要修改的flavor,编译一次就能解析选中的flavor了。
      • dd25d4abf29c:智能啊,main里面放公用的,其他不同的放同级然后改改build。话说新建目录必须完全一样吗?我漏掉一两个目录他会用main中的不。。。看上去mainfest文件也是公用的,那可以分别新建不公用么,毕竟可能会新添加不少活动。。。
        dd25d4abf29c:@隔壁王较瘦 嗯嗯谢谢~
        隔壁王较瘦:漏掉的会使用main里面的,manifest也可以共用的,比如微信分享这种回调就可以再manifest里面注册,也可以分别新建各自的manifest,到编译的时候会和main里面的manifest文件合并的,所以注意一点多个manifest合并时候的冲突
      • PeterHart:改了每个application ID ,但还是不能同时存在一个手机是怎么回事啊
        flavorDimensions "app"
        productFlavors {
        daqi {
        applicationId "com.liang.gameapplication.daqi"
        versionCode 2
        dimension "app"
        versionName "1.0.2"
        }
        ant {
        applicationId "com.liang.gameapplication.ant"
        versionCode 1
        dimension "app"
        versionName "1.0"
        }
        }
        隔壁王较瘦:@Alan_e0f1 你manifest里面是不是写明了applicationId了
        PeterHart:@隔壁王较瘦 就把左下的Build Variant 改了 之前的是antdebug 后来是daqidebug
        隔壁王较瘦:@Alan_e0f1 怎么运行的,有切换云的哪个APP吗
      • icoo:微信回调那章 没看明白啊,为啥要改包名啊,直接在com.persons.zhang下再建立一个wxapi就行了啊?
        隔壁王较瘦:@icoo 本来我想在最后面加上这些的,再一想,反正占位符都讲过了,剩下的你们发挥脑洞就够了啊
        icoo:@隔壁王较瘦 博主666,学习了
        隔壁王较瘦:@icoo 还记得文章里面的manifest占位符那一段么,在基础包的manifest里面注册一个activity,里面的name一般情况不是写的activity的路径么,现在把activity路径的包名改成applicationId占位符代替包名,
        <activity android:name="com.persons.zhang.wxapi.WxActivity" />
        改成<activity android:name="${applicationId}.wxapi.WxActivity" />
        报错不用管,合并之后就是正确的了
      • 628f68d7f810:很好,非常适合我现在的场景.我现在有4个APP,就是根据一个APP为基准造出来的,功能基本一致.UI不一样.对于如何进行,版本迭代有没有,省力气的方法.我之前是修改了一个公共的bug后.用代码比对工具进行其他项目的同步.因为各个项目有些地方不一样
        隔壁王较瘦:@CSDNokmin 省力是对的
        628f68d7f810:@隔壁王较瘦 我想我应该在有不同的地方使用继承,新增功能.或者修改功能.在有公共bug修改时.就可以不用代码比对工具了.而是直接复制.不知道这样会不会省力气些了, :joy:
        隔壁王较瘦:这种方式也是无奈之举,不同的地方多了之后也跟新app差不多
      • ca87e08939e4:楼主想问下,怎么在gradle里面动态修改应用对应的繁体和英文app_name了,我们现在在弄自动化打包,支持客户自己定义app_name,简体的app_name在gradle里面已经实现动态更换了,繁体和英文不知道怎么弄。
        隔壁王较瘦:@Mr君陌 就跟正常的i18n国际化一样操作,没有什么区别,只不过是在编译哪个app的时候用哪个app的资源而已,跟普通一个app的多语言适配是一样的
        ca87e08939e4:@隔壁王较瘦 通过strings的资源合并替换,可以换对应的繁体和英文app_name么?
        隔壁王较瘦:@Mr君陌 这个我还真心没搞过,我的app_name是strings的资源合并替换的,没在gradle里面处理,抱歉哈
      • ZkJanus:很实用,以后会用到的,谢谢幺

      本文标题:一套源码编译多个APP,不同的签名,包名,界面,字段...

      本文链接:https://www.haomeiwen.com/subject/ecdgextx.html