注册

Android-多套环境的维护

记录一下项目中多套环境维护的一种思路。


一、多套环境要注意的问题


1、方便使用灵活配置

2、配置安全不会被覆写

3、扩展灵活

4、安装包可动态切换环境,方便测试人员使用


二、解决思路


1、Android中的Properties文件是只读的,打包后不可修改,所以用Properties文件维护所有的配置。

2、在一个安装包内动态切换环境,方便测试人员切换使用,这一点用MMKV来动态存储。为了防止打包时可能出现的错误,这一点也需要Properties文件来控制。


三、Properties文件的封装


package com.abc.kotlinstudio

import android.content.Context
import java.io.IOException
import java.util.*


object PropertiesUtil {

private var pros: Properties? = null

fun init(c: Context) {
pros = Properties()
try {
val input = c.assets.open("appConfig.properties")
pros?.load(input)
} catch (e: IOException) {
e.printStackTrace()
}
}

private fun getProperty(key: String, default: String): String {
return pros?.getProperty(key, default) ?: default
}

/**
* 判断是否是国内版本
*/
fun isCN(): Boolean {
return getProperty("isCN", "true").toBoolean()

}

/**
* 判断是否是正式环境
*/
fun isRelease(): Boolean {
return getProperty("isRelease", "false").toBoolean()
}

/**
* 获取版本的环境 dev test release
* 如果isRelease为true就读Properties文件,为false就读MMKV存储的值
*/
fun getEnvironment(): Int = if (isRelease()) {
when (getProperty("environment", "test")) {
"dev" -> {
GlobalUrlConfig.EnvironmentConfig.DEV.value
}
"test" -> {
GlobalUrlConfig.EnvironmentConfig.TEST.value
}
"release" -> {
GlobalUrlConfig.EnvironmentConfig.RELEASE.value
}
else -> {
GlobalUrlConfig.EnvironmentConfig.TEST.value
}
}

} else {
when (CacheUtil.getEnvironment(getProperty("environment", "test"))) {
"dev" -> {
GlobalUrlConfig.EnvironmentConfig.DEV.value
}
"test" -> {
GlobalUrlConfig.EnvironmentConfig.TEST.value
}
"release" -> {
GlobalUrlConfig.EnvironmentConfig.RELEASE.value
}

else -> {
GlobalUrlConfig.EnvironmentConfig.TEST.value
}
}
}


/**
* 获取国内外环境
*/
fun getCN(): Int = if (isRelease()) {
when (getProperty("isCN", "true")) {
"true" -> {
GlobalUrlConfig.CNConfig.CN.value
}
"false" -> {
GlobalUrlConfig.CNConfig.I18N.value
}

else -> {
GlobalUrlConfig.CNConfig.CN.value
}
}

} else {
when (CacheUtil.getCN(getProperty("isCN", "true"))) {
"true" -> {
GlobalUrlConfig.CNConfig.CN.value
}
"false" -> {
GlobalUrlConfig.CNConfig.I18N.value
}

else -> {
GlobalUrlConfig.CNConfig.CN.value
}
}
}


}

注意二点,打包时如果Properties文件isRelease为true则所有配置都读Properties文件,如果为false就读MMKV存储的值;如果MMKV没有存储值,默认值也是读Properties文件。


image.png


内容比较简单:


isCN = true   //是否国内环境 
isRelease = false //是否release,比如日志的打印也可以用这个变量控制
#dev test release //三种环境
environment = dev //环境切换

四、MMKV封装


package com.abc.kotlinstudio

import android.os.Parcelable
import com.tencent.mmkv.MMKV
import java.util.*

object CacheUtil {

private var userId: Long = 0

//公共存储区的ID
private const val STORAGE_PUBLIC_ID = "STORAGE_PUBLIC_ID"

//------------------------公共区的键------------------
//用户登录的Token
const val KEY_PUBLIC_TOKEN = "KEY_PUBLIC_TOKEN"

//------------------------私有区的键------------------
//用户是否第一次登录
const val KEY_USER_IS_FIRST = "KEY_USER_IS_FIRST"


/**
* 设置用户的ID,根据用户ID做私有化分区存储
*/
fun setUserId(userId: Long) {
this.userId = userId
}

/**
* 获取MMKV对象
* @param isStoragePublic true 公共存储空间 false 用户私有空间
*/
fun getMMKV(isStoragePublic: Boolean): MMKV = if (isStoragePublic) {
MMKV.mmkvWithID(STORAGE_PUBLIC_ID)
} else {
MMKV.mmkvWithID("$userId")
}


/**
* 设置登录后token
*/
fun setToken(token: String) {
put(KEY_PUBLIC_TOKEN, token, true)
}


/**
* 获取登录后token
*/
fun getToken(): String = getString(KEY_PUBLIC_TOKEN)


/**
* 设置MMKV存储的环境
*/
fun putEnvironment(value: String) {
put("environment", value, true)
}

/**
* 获取MMKV存储的环境
*/
fun getEnvironment(defaultValue: String): String {
return getString("environment", true, defaultValue)
}

/**
* 设置MMKV存储的国内外环境
*/
fun putCN(value: String) {
put("isCN", value, true)
}

/**
* 获取MMKV存储的国内外环境
*/
fun getCN(defaultValue: String): String {
return getString("isCN", true, defaultValue)
}


//------------------------------------------基础方法区-----------------------------------------------

/**
* 基础数据类型的存储
* @param key 存储的key
* @param value 存储的值
* @param isStoragePublic 是否存储在公共区域 true 公共区域 false 私有区域
*/
fun put(key: String, value: Any?, isStoragePublic: Boolean): Boolean {
val mmkv = getMMKV(isStoragePublic)
return when (value) {
is String -> mmkv.encode(key, value)
is Float -> mmkv.encode(key, value)
is Boolean -> mmkv.encode(key, value)
is Int -> mmkv.encode(key, value)
is Long -> mmkv.encode(key, value)
is Double -> mmkv.encode(key, value)
is ByteArray -> mmkv.encode(key, value)
else -> false
}
}


/**
* 这里使用安卓自带的Parcelable序列化,它比java支持的Serializer序列化性能好些
* @param isStoragePublic 是否存储在公共区域 true 公共区域 false 私有区域
*/
fun <T : Parcelable> put(key: String, t: T?, isStoragePublic: Boolean): Boolean {
if (t == null) {
return false
}
return getMMKV(isStoragePublic).encode(key, t)
}

/**
* 存Set集合的数据
* @param isStoragePublic 是否存储在公共区域 true 公共区域 false 私有区域
*/
fun put(key: String, sets: Set<String>?, isStoragePublic: Boolean): Boolean {
if (sets == null) {
return false
}
return getMMKV(isStoragePublic).encode(key, sets)
}

/**
* 取数据,因为私有存储区用的多,所以这里给了默认参数为私有区域,如果公共区域取要记得改成true.下同
*/
fun getInt(key: String, isStoragePublic: Boolean = false, defaultValue: Int = 0): Int {
return getMMKV(isStoragePublic).decodeInt(key, defaultValue)
}

fun getDouble(
key: String,
isStoragePublic: Boolean = false,
defaultValue: Double = 0.00
): Double {
return getMMKV(isStoragePublic).decodeDouble(key, defaultValue)
}

fun getLong(key: String, isStoragePublic: Boolean = false, defaultValue: Long = 0L): Long {
return getMMKV(isStoragePublic).decodeLong(key, defaultValue)
}

fun getBoolean(
key: String,
isStoragePublic: Boolean = false,
defaultValue: Boolean = false
): Boolean {
return getMMKV(isStoragePublic).decodeBool(key, defaultValue)
}

fun getFloat(key: String, isStoragePublic: Boolean = false, defaultValue: Float = 0F): Float {
return getMMKV(isStoragePublic).decodeFloat(key, defaultValue)
}

fun getByteArray(key: String, isStoragePublic: Boolean = false): ByteArray? {
return getMMKV(isStoragePublic).decodeBytes(key)
}

fun getString(
key: String,
isStoragePublic: Boolean = false,
defaultValue: String = ""
): String {
return getMMKV(isStoragePublic).decodeString(key, defaultValue) ?: defaultValue
}

/**
* getParcelable<Class>("")
*/
inline fun <reified T : Parcelable> getParcelable(
key: String,
isStoragePublic: Boolean = false
): T? {
return getMMKV(isStoragePublic).decodeParcelable(key, T::class.java)
}

fun getStringSet(key: String, isStoragePublic: Boolean = false): Set<String>? {
return getMMKV(isStoragePublic).decodeStringSet(key, Collections.emptySet())
}

fun removeKey(key: String, isStoragePublic: Boolean = false) {
getMMKV(isStoragePublic).removeValueForKey(key)
}

fun clearAll(isStoragePublic: Boolean = false) {
getMMKV(isStoragePublic).clearAll()
}

}

五、URL的配置


假设有国内外以及host、h5_host环境 :


object GlobalUrlConfig {

private val BASE_HOST_CN_DEV = "https://cn.dev.abc.com"
private val BASE_HOST_CN_TEST = "https://cn.test.abc.com"
private val BASE_HOST_CN_RELEASE = "https://cn.release.abc.com"

private val BASE_HOST_I18N_DEV = "https://i18n.dev.abc.com"
private val BASE_HOST_I18N_TEST = "https://i18n.test.abc.com"
private val BASE_HOST_I18N_RELEASE = "https://i18n.release.abc.com"

private val BASE_HOST_H5_CN_DEV = "https://cn.dev.h5.abc.com"
private val BASE_HOST_H5_CN_TEST = "https://cn.test.h5.abc.com"
private val BASE_HOST_H5_CN_RELEASE = "https://cn.release.h5.abc.com"

private val BASE_HOST_H5_I18N_DEV = "https://i18n.dev.h5.abc.com"
private val BASE_HOST_H5_I18N_TEST = "https://i18n.test.h5.abc.com"
private val BASE_HOST_H5_I18N_RELEASE = "https://i18n.release.h5.abc.com"

private val baseHostList: List<List<String>> = listOf(
listOf(
BASE_HOST_CN_DEV,
BASE_HOST_CN_TEST,
BASE_HOST_CN_RELEASE
), listOf(
BASE_HOST_I18N_DEV,
BASE_HOST_I18N_TEST,
BASE_HOST_I18N_RELEASE
)
)

private val baseHostH5List: List<List<String>> = listOf(
listOf(
BASE_HOST_H5_CN_DEV,
BASE_HOST_H5_CN_TEST,
BASE_HOST_H5_CN_RELEASE
), listOf(
BASE_HOST_H5_I18N_DEV,
BASE_HOST_H5_I18N_TEST,
BASE_HOST_H5_I18N_RELEASE
)
)

//base
var BASE_HOST: String =
baseHostList[PropertiesUtil.getCN()][PropertiesUtil.getEnvironment()]
//base_h5
var BASE_H5_HOST: String =
baseHostH5List[PropertiesUtil.getCN()][PropertiesUtil.getEnvironment()]


enum class CNConfig(var value: Int) {
CN(0), I18N(1)
}

enum class EnvironmentConfig(var value: Int) {
DEV(0), TEST(1), RELEASE(2)
}

六、测试人员可在打好的App动态切换


可以弹Dialog动态切换环境,下面为测试代码:


//初始化
PropertiesUtil.init(this)
MMKV.initialize(this)
CacheUtil.setUserId(1000L)

val btSetCn = findViewById<AppCompatButton>(R.id.bt_set_cn)
val btSeti18n = findViewById<AppCompatButton>(R.id.bt_set_i8n)
val btSetDev = findViewById<AppCompatButton>(R.id.bt_set_dev)
val btSetTest = findViewById<AppCompatButton>(R.id.bt_set_test)
val btSetRelease = findViewById<AppCompatButton>(R.id.bt_set_release)

//App内找个地方弹一个Dialog动态修改下面的参数即可。

btSetCn.setOnClickListener {
CacheUtil.putCN("true")
//重启App(AndroidUtilCode工具类里面的方法)
AppUtils.relaunchApp(true)
}

btSeti18n.setOnClickListener {
CacheUtil.putCN("false")
AppUtils.relaunchApp(true)
}

btSetDev.setOnClickListener {
CacheUtil.putEnvironment("dev")
AppUtils.relaunchApp(true)
}

btSetTest.setOnClickListener {
CacheUtil.putEnvironment("test")
AppUtils.relaunchApp(true)
}

btSetRelease.setOnClickListener {
CacheUtil.putEnvironment("release")
AppUtils.relaunchApp(true)
}

总结


一般会有4套环境: 开发环境,测试环境,预发布环境,正式环境。如果再区分国内外则乘以2。除了base的主机一般还会引入其他主机,比如h5的主机,这样会导致整个环境复杂多变。


刚开始是给测试打多渠道包,测试抱怨切环境,频繁卸载安装App很麻烦,于是做了这个优化。上线时记得把Properties文件isRelease设置为true,则发布的包就不会有问题,这个一般都不会忘记,风险很小。相比存文件或者其他形式安全很多。


写的比较匆忙,代码略粗糙,主要体现思路。以上!


作者:TimeFine
链接:https://juejin.cn/post/7168497103516205069
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册