×

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

96
隔壁王较瘦
2017.10.12 10:32* 字数 3182

一套源码编译多个 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

到这里基本结束了。

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

Android
Web note ad 1