Android:雙11已經過了雙12都要到了,還不給你的APP加上自動換圖標的功能嗎?

原文:椎鋒陷陳

前言

也許你也注意到了,在臨近雙11之際,手機上電商類APP的應用圖標已經悄無聲息換成了雙11專屬圖標,比如某寶和某東:

可能你會說,這有什麼奇怪的,應用市場開啓自動更新不就可以了麼?

真的是這樣嗎?

爲此,我特意查看了我手機上的某寶APP的當前版本,並對比了歷史版本上的圖標,發現並不對應。

默認是88會員節專屬圖標,而現在顯示的是雙11圖標。

那麼,作爲開發者的嗅覺,讓你自然而然想要從技術角度揣測是怎麼實現的,而這便是這篇文章想要與你分享的。

知識儲備

<activity-alias>

某一個Activity 的別名,用於實例化該目標Activity。目標必須與別名在同一應用中,並且在清單中必須在別名之前進行聲明。
介紹下幾個重要的屬性:

android:enabled:必須設爲“true”,系統才能通過別名實例化目標 Activity
android:icon:通過別名呈現給用戶時目標 Activity 的圖標。
android:name:別名的唯一名稱。與目標 Activity 的名稱不同,別名名稱是任意的,它不引用實際類。
android:targetActivity:可通過別名激活的 Activity 的名稱。

PackageManager#setComponentEnabledSetting

可以利用 PackageManager 在清單文件中所定義的任何組件上切換啓用狀態,包括您想啓用或停用的任何一個Activity。

有了以上知識儲備後,下面就該剖析一下這個需求的具體場景了。

場景剖析

以電商類APP雙11活動爲例,在雙11活動開始前的某個時間點(比如10天前)就要開始對活動的預熱,此時就要實現圖標的自動更換,而在活動結束之後,也必須要能更換回正常圖標,並且要求過程儘量對用戶無感知,更不能影響用戶對APP的正常使用。

具體拆分成要實現的功能點便是:圖標更換、自動操作、用戶無感知。

方案實現

1.圖標更換:禁用Launcher組件,啓用Alias組件,並將targetActivity指向原先的Launcher組件。

2.自動操作:指定日期轉換爲時間戳,並與當前時間戳對比,超過預設時間則執行替換操作。

3.用戶無感知:儘量選擇APP不活躍的階段的,比如切換應用/回到桌面時。

代碼實踐

首先,我們需要在AndroidManifest清單文件中添加<activity-alias>元素,默認爲禁用狀態,name屬性作爲我們找到此組件的唯一標誌,而icon屬性即是我們要替換的圖標資源,並通過targetActivity屬性將作爲LANCHUER的SplashActivity作爲實例化的目標 Activity:

<activity android:name=".SplashActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<!--88會員節專屬Activity別名-->
<activity-alias
    android:name=".SplashAliasActivity"
    android:enabled="false"
    android:icon="@mipmap/ic_launcher_88"
    android:targetActivity=".SplashActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

<!--雙11專屬Activity別名-->
<activity-alias
    android:name=".SplashAlias2Activity"
    android:enabled="false"
    android:icon="@mipmap/ic_launcher_11_11"
    android:targetActivity=".SplashActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity-alias>

隨後,我們圖標替換的工作視作一項任務,定義一個數據類:

/**
 * 切換圖標任務
 */
data class SwitchIconTask (val launcherComponentClassName: String,  // 啓動器組件類名
                           val aliasComponentClassName: String,  // 別名組件類名
                           val presetTime: Long,            // 預設時間
                           val outDateTime: Long)           // 過期時間

定義一個LauncherIconManager單例,負責圖標更換相關的工作。開放添加圖標切換任務的接口,做好參數合法性的校驗:

/**
 * 啓動器圖標管理器
 */
object LauncherIconManager {

    /** 切換圖標任務Map */
    private val taskMap: LinkedHashMap<String, SwitchIconTask> = LinkedHashMap()

    /**
     * 添加圖標切換任務
     * @param newTasks 新任務,可以傳多個
     */
    fun addNewTask(vararg newTasks: SwitchIconTask) {
        for (newTask in newTasks) {
            // 防止重複添加任務
            if (taskMap.containsKey(newTask.aliasComponentClassName)) return

            // 校驗任務的預設時間和過期時間
            for (queuedTask in taskMap.values) {
                if (newTask.presetTime > newTask.outDateTime) throw IllegalArgumentException("非法的任務預設時間${newTask.presetTime}, 不能晚於過期時間")
                if (newTask.presetTime <= queuedTask.outDateTime) throw IllegalArgumentException("非法的任務預設時間${newTask.presetTime}, 不能早於已添加任務的過期時間")
            }

            taskMap[newTask.aliasComponentClassName] = newTask
        }
    }

    ...
}

LauncherIconManager.addNewTask(
    SwitchIconTask(
        SplashActivity::class.java.name,
        "$packageName.SplashAliasActivity",
        format.parse("2020-08-02").time,
        format.parse("2020-08-09").time
    ),
    SwitchIconTask(
        SplashActivity::class.java.name,
        "$packageName.SplashAlias2Activity",
        format.parse("2020-11-05").time,
        format.parse("2020-11-12").time
    )
)

通過Application#registerActivityLifecycleCallbacks方法註冊了對應用內Activity生命週期的監聽,通過是否有活躍狀態的Activity判斷應用是否進入了後臺:

/**
 * 應用運行狀態註冊器
 */
object RunningStateRegister {

    fun register(application: Application, callback: StateCallback) {
        application.registerActivityLifecycleCallbacks(object : SimpleActivityLifecycleCallbacks() {
            private var startedActivityCount = 0
            override fun onActivityStarted(activity: Activity) {
                if (startedActivityCount == 0) {
                    callback.onForeground()
                }
                startedActivityCount++
            }

            override fun onActivityStopped(activity: Activity) {
                startedActivityCount--
                if (startedActivityCount == 0) {
                    callback.onBackground()
                }
            }
        })
    }

}   

class BaseApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        LauncherIconManager.register(this)
    }
}

判斷應用進入後臺後,就可以開始對圖標的更換工作了:

/**
 * 啓動器圖標管理器
 */
object LauncherIconManager {
    ...

    /**
     * 註冊以監聽應用運行狀態
     */
    fun register(application: Application) {
        RunningStateRegister.register(application, object: RunningStateRegister.StateCallback{
            override fun onForeground() {
            }

            override fun onBackground() {
                proofreadingInOrder(application)
            }
        })
    }

    /**
     * 依次校對預設時間
     * @param context 上下文
     */
    fun proofreadingInOrder(context: Context) {
        for (task in taskMap.values) {
            if (proofreading(context, task)) break
        }
    }

    /**
     * 校對預設時間/過期時間
     * @param context 上下文
     * @return true 已過預設時間      false 未達預設時間或已過期
     */
    private fun proofreading(context: Context, task: SwitchIconTask) =
        when {
            isPassedOutDateTime(task) -> {
                disableComponent(context, ActivityUtil.getLauncherActivityName(context)!!)
                enableComponent(context, task.launcherComponentClassName)
                false
            }
            isPassedPresetTime(task) -> {
                disableComponent(context, ActivityUtil.getLauncherActivityName(context)!!)
                enableComponent(context, task.aliasComponentClassName)
                true
            }
            else -> false
        }

    /**
     * 是否已超過預設時間
     * @param task 任務
     */
    private fun isPassedPresetTime(task: SwitchIconTask) =
        System.currentTimeMillis() > task.presetTime

    /**
     * 是否已超過過期時間
     * @param task 任務
     *
     */
    private fun isPassedOutDateTime(task: SwitchIconTask) =
        System.currentTimeMillis() > task.outDateTime

    ...
}        

以上代碼均已上傳到GitHub。核心的類都封裝到Library模塊了,並提供Demo模塊演示如何使用。

如果覺得項目不錯的話點個Star吧~
https://github.com/madchan/LauncherIconLib

效果預覽

總結

通過以上構建的方案,便可讓我們的APP在預設的時間點實現對應用圖標的自動替換,缺點是隻能加載隨APK打包的圖片資源,適用於運營活動時間相對固定的的場景。

參考文章

https://developer.android.google.cn/guide/topics/manifest/activity-alias-element


我目前在深圳,13年java轉Android開發,在小廠待過,也去過華爲,OPPO等,去年四月份進了阿里一直到現在。等大廠待過也面試過很多人。深知大多數初中級Android工程師,想要提升技能,往往是自己摸索成長,不成體系的學習效果低效漫長且無助。

所以爲了幫助大家深刻理解Android相關知識點的原理以及面試相關知識,這裏放上相關的我搜集整理的24套騰訊、字節跳動、阿里、百度2019-2020BAT 面試真題解析,我把大廠面試中常被問到的技術點整理成了視頻和PDF(實際上比預期多花了不少精力),包知識脈絡 + 諸多細節。

還有 高級架構技術進階腦圖 幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習。

以上內容均放在了開源項目:【github】 中已收錄,裏面包含不同方向的自學Android路線、面試題集合/面經、及系列技術文章等,資源持續更新中...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章