Android多套环境的维护思路详解
TimeFine 人气:0一、多套环境要注意的问题
记录一下项目中多套环境维护的一种思路。
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文件。
内容比较简单:
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,则发布的包就不会有问题,这个一般都不会忘记,风险很小。相比存文件或者其他形式安全很多。
写的比较匆忙,代码略粗糙,主要体现思路。
加载全部内容