Android多套環(huán)境的維護(hù)思路詳解
一、多套環(huán)境要注意的問(wèn)題
記錄一下項(xiàng)目中多套環(huán)境維護(hù)的一種思路。
1、方便使用靈活配置
2、配置安全不會(huì)被覆寫(xiě)
3、擴(kuò)展靈活
4、安裝包可動(dòng)態(tài)切換環(huán)境,方便測(cè)試人員使用
二、解決思路
1、Android中的Properties文件是只讀的,打包后不可修改,所以用Properties文件維護(hù)所有的配置。
2、在一個(gè)安裝包內(nèi)動(dòng)態(tài)切換環(huán)境,方便測(cè)試人員切換使用,這一點(diǎn)用MMKV來(lái)動(dòng)態(tài)存儲(chǔ)。為了防止打包時(shí)可能出現(xiàn)的錯(cuò)誤,這一點(diǎn)也需要Properties文件來(lái)控制。
三、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
}
/**
* 判斷是否是國(guó)內(nèi)版本
*/
fun isCN(): Boolean {
return getProperty("isCN", "true").toBoolean()
}
/**
* 判斷是否是正式環(huán)境
*/
fun isRelease(): Boolean {
return getProperty("isRelease", "false").toBoolean()
}
/**
* 獲取版本的環(huán)境 dev test release
* 如果isRelease為true就讀Properties文件,為false就讀MMKV存儲(chǔ)的值
*/
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
}
}
}
/**
* 獲取國(guó)內(nèi)外環(huán)境
*/
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
}
}
}
}
注意二點(diǎn),打包時(shí)如果Properties文件isRelease為true則所有配置都讀Properties文件,如果為false就讀MMKV存儲(chǔ)的值;如果MMKV沒(méi)有存儲(chǔ)值,默認(rèn)值也是讀Properties文件。

內(nèi)容比較簡(jiǎn)單:
isCN = true //是否國(guó)內(nèi)環(huán)境 isRelease = false //是否release,比如日志的打印也可以用這個(gè)變量控制 #dev test release //三種環(huán)境 environment = dev //環(huán)境切換
四、MMKV封裝
package com.abc.kotlinstudio
import android.os.Parcelable
import com.tencent.mmkv.MMKV
import java.util.*
object CacheUtil {
private var userId: Long = 0
//公共存儲(chǔ)區(qū)的ID
private const val STORAGE_PUBLIC_ID = "STORAGE_PUBLIC_ID"
//------------------------公共區(qū)的鍵------------------
//用戶(hù)登錄的Token
const val KEY_PUBLIC_TOKEN = "KEY_PUBLIC_TOKEN"
//------------------------私有區(qū)的鍵------------------
//用戶(hù)是否第一次登錄
const val KEY_USER_IS_FIRST = "KEY_USER_IS_FIRST"
/**
* 設(shè)置用戶(hù)的ID,根據(jù)用戶(hù)ID做私有化分區(qū)存儲(chǔ)
*/
fun setUserId(userId: Long) {
this.userId = userId
}
/**
* 獲取MMKV對(duì)象
* @param isStoragePublic true 公共存儲(chǔ)空間 false 用戶(hù)私有空間
*/
fun getMMKV(isStoragePublic: Boolean): MMKV = if (isStoragePublic) {
MMKV.mmkvWithID(STORAGE_PUBLIC_ID)
} else {
MMKV.mmkvWithID("$userId")
}
/**
* 設(shè)置登錄后token
*/
fun setToken(token: String) {
put(KEY_PUBLIC_TOKEN, token, true)
}
/**
* 獲取登錄后token
*/
fun getToken(): String = getString(KEY_PUBLIC_TOKEN)
/**
* 設(shè)置MMKV存儲(chǔ)的環(huán)境
*/
fun putEnvironment(value: String) {
put("environment", value, true)
}
/**
* 獲取MMKV存儲(chǔ)的環(huán)境
*/
fun getEnvironment(defaultValue: String): String {
return getString("environment", true, defaultValue)
}
/**
* 設(shè)置MMKV存儲(chǔ)的國(guó)內(nèi)外環(huán)境
*/
fun putCN(value: String) {
put("isCN", value, true)
}
/**
* 獲取MMKV存儲(chǔ)的國(guó)內(nèi)外環(huán)境
*/
fun getCN(defaultValue: String): String {
return getString("isCN", true, defaultValue)
}
//------------------------------------------基礎(chǔ)方法區(qū)-----------------------------------------------
/**
* 基礎(chǔ)數(shù)據(jù)類(lèi)型的存儲(chǔ)
* @param key 存儲(chǔ)的key
* @param value 存儲(chǔ)的值
* @param isStoragePublic 是否存儲(chǔ)在公共區(qū)域 true 公共區(qū)域 false 私有區(qū)域
*/
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 是否存儲(chǔ)在公共區(qū)域 true 公共區(qū)域 false 私有區(qū)域
*/
fun <T : Parcelable> put(key: String, t: T?, isStoragePublic: Boolean): Boolean {
if (t == null) {
return false
}
return getMMKV(isStoragePublic).encode(key, t)
}
/**
* 存Set集合的數(shù)據(jù)
* @param isStoragePublic 是否存儲(chǔ)在公共區(qū)域 true 公共區(qū)域 false 私有區(qū)域
*/
fun put(key: String, sets: Set<String>?, isStoragePublic: Boolean): Boolean {
if (sets == null) {
return false
}
return getMMKV(isStoragePublic).encode(key, sets)
}
/**
* 取數(shù)據(jù),因?yàn)樗接写鎯?chǔ)區(qū)用的多,所以這里給了默認(rèn)參數(shù)為私有區(qū)域,如果公共區(qū)域取要記得改成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的配置
假設(shè)有國(guó)內(nèi)外以及host、h5_host環(huán)境 :
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)
}
六、測(cè)試人員可在打好的App動(dòng)態(tài)切換
可以彈Dialog動(dòng)態(tài)切換環(huán)境,下面為測(cè)試代碼:
//初始化
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內(nèi)找個(gè)地方彈一個(gè)Dialog動(dòng)態(tài)修改下面的參數(shù)即可。
btSetCn.setOnClickListener {
CacheUtil.putCN("true")
//重啟App(AndroidUtilCode工具類(lèi)里面的方法)
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)
}
總結(jié)
一般會(huì)有4套環(huán)境: 開(kāi)發(fā)環(huán)境,測(cè)試環(huán)境,預(yù)發(fā)布環(huán)境,正式環(huán)境。如果再區(qū)分國(guó)內(nèi)外則乘以2。除了base的主機(jī)一般還會(huì)引入其他主機(jī),比如h5的主機(jī),這樣會(huì)導(dǎo)致整個(gè)環(huán)境復(fù)雜多變。
剛開(kāi)始是給測(cè)試打多渠道包,測(cè)試抱怨切環(huán)境,頻繁卸載安裝App很麻煩,于是做了這個(gè)優(yōu)化。上線(xiàn)時(shí)記得把Properties文件isRelease設(shè)置為true,則發(fā)布的包就不會(huì)有問(wèn)題,這個(gè)一般都不會(huì)忘記,風(fēng)險(xiǎn)很小。相比存文件或者其他形式安全很多。
寫(xiě)的比較匆忙,代碼略粗糙,主要體現(xiàn)思路。
以上就是Android多套環(huán)境的維護(hù)思路詳解的詳細(xì)內(nèi)容,更多關(guān)于Android多套環(huán)境維護(hù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android從xml加載到View對(duì)象過(guò)程解析
這篇文章主要介紹了 Android從xml加載到View對(duì)象過(guò)程解析的相關(guān)資料,需要的朋友可以參考下2016-03-03
解決EditText編輯時(shí)hint 在6.0 手機(jī)上顯示不出來(lái)的問(wèn)題
下面小編就為大家?guī)?lái)一篇解決EditText編輯時(shí)hint 在6.0 手機(jī)上顯示不出來(lái)的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05
快速調(diào)試Android應(yīng)用系統(tǒng)修改ro.debuggable屬性的兩種方式
這篇文章主要為大家介紹了快速調(diào)試Android應(yīng)用系統(tǒng)修改ro.debuggable屬性的兩種方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
Android入門(mén)之IntentService的使用教程詳解
IntentService的生命周期中有一個(gè)非常好的方法-onHandleIntent方法,它是一個(gè)abstract方法,開(kāi)發(fā)者在實(shí)現(xiàn)IntentService時(shí)可以覆蓋它來(lái)處理“長(zhǎng)事務(wù)”。本文就來(lái)聊聊IntentService的使用,需要的可以參考一下2022-12-12
Android布局控件View?ViewRootImpl?WindowManagerService關(guān)系
這篇文章主要為大家介紹了Android布局控件View?ViewRootImpl?WindowManagerService關(guān)系示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
Android編程實(shí)現(xiàn)google消息通知功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)google消息通知功能,結(jié)合具體實(shí)例形式分析了Android消息處理及C#服務(wù)器端與google交互的相關(guān)操作技巧,需要的朋友可以參考下2017-06-06
Android運(yùn)動(dòng)健康睡眠自定義控件的實(shí)現(xiàn)
這篇文章主要介紹了Android實(shí)現(xiàn)運(yùn)動(dòng)健康睡眠自定義控件的方法,幫助大家更好的理解和學(xué)習(xí)使用Android開(kāi)發(fā),感興趣的朋友可以了解下2021-03-03

