Poplayer 雲音樂優化實踐

本文做者: Codey前端

背景介紹

你是否還在爲各類特殊場景特殊邏輯而煩惱,是否還在爲各類一次性業務而添加一堆代碼,是否還在爲各類奇奇怪怪的彩蛋而滿心疲憊? 在雲音樂不斷迭代的過程當中,咱們不止一次的遇到產品說要在某一個地方加個彩蛋,有的是在觸及特殊操做時,有的是在播放特定歌曲時,甚至有的是在特定時間點播放特定歌曲到特定播放進度時。java

每次聽到這些需求,頭都大了,又得在老代碼上面加一堆特殊邏輯,又得寫那麼多代碼,重點寫那麼多代碼還不能複用,同時也增長了穩定業務的複雜度,也沒有什麼實時性、動態性可言。android

在經歷了幾回這種需求以後,咱們就在想如何去避免這類臨時業務和穩定業務融合到一塊兒,如何去把這類臨時業務統一成一套通用可行方案?git

在這以前,咱們先了解下什麼是臨時業務,什麼是穩定業務。github

**臨時業務:**特定時間,特定場景,特定配置下須要上線的業務;不可複用,可能只一次使用。對雲音樂來講就是彩蛋系列,這裏舉一個特定的場景,國慶節國歌升旗彩蛋,須要在國慶節期間的天安門升旗時間播放國歌,喚起升旗的視頻。對於這類型的場景,基本知足了咱們對於臨時業務的定義,能夠將其認爲是臨時業務web

**穩定業務:**核心功能,基礎功能,長期存在,非必要狀況下不隨意修改。對雲音樂來講就是最重要的就是播放業務,包含播放列表,播放頁面等播放核心功能,這塊邏輯咱們實際上是不但願去隨意改動的,要是隨意增長個彩蛋就瘋狂改動這塊的邏輯,瘋狂添加些臨時且不可複用的代碼,那將會增長這塊的邏輯複雜度,維護複雜度,還未引發一些意外的問題。播放業務也是基本知足了咱們對穩定業務的定義,能夠將其認爲是穩定業務。canvas

瞭解了臨時業務和穩定業務以後,就能夠想辦法去作區分解決了。對於臨時業務,須要的通用方案得知足可配置,可複用,實時性,動態性等要求,在你們的討論下,便想到了 Poplayer 這個大殺器。緩存

什麼是 Poplayer

簡介

Poplayer ,顧名思義,就是Pop + Layer的組合,結合 Android 場景來看,其實就是頁面上層再加一層,稱做 Poplayer。經過 Poplayer 層咱們能夠將一些臨時業務交由這一層去處理維護,而這一層又交由 WebView 去承載,在增長動態性的同時又不影響既有穩定業務。性能優化

Poplayer 的設計

設計概要

設計概要

從上面的圖能夠很是清晰的看出來,所謂的 Poplayer 就是在客戶端頁面上增長一層,將這一層做爲展現臨時業務的容器,兩者經過 JSBridge 通訊,再結合一些客戶端頁面配置以及容器配置,達到臨時業務熱插拔,可複用的要求。markdown

總體流程與設計

總體流程

配置中心:雲音樂基礎能力之一,以 key / value 的形式存儲業務及功能的特殊配置,支持配置秒級下發及分端下發等功能

從上圖能夠看出,咱們經過配置能力將客戶端頁面和容器結合到一塊兒,總體流程結構都是很是清晰的。依賴於雲音樂完善的基礎設施,在完成 Poplayer 組件的時候減小了不少工做。

Poplayer雲音樂優化實踐

內存優化

在雲音樂實際應用過程當中,遇到一個問題,當使用 Poplayer 去播放視頻時,快速點擊 WebView 會致使視頻出現卡頓,也就是由於這個問題,咱們開始了 Poplayer 的內存優化

使用 Poplayer 的時候,其中一個技術點就是: 根據觸摸座標獲取該處彈框的 ARGB 值, 判斷 A 份量的值是否超過閾值,超過則交給 HTML5 處理

那麼咱們如何去獲取點擊位置的 alpha 值呢,通常咱們想到的是使用相似截圖的方式去獲取 View 的整個視圖。

以下:

// view 是 webView
private fun captureView(view: View): Bitmap {
        val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        view.draw(canvas)
        return bitmap
}

fun getViewTouchAlpha(ev: MotionEvent, view: View): Float {
        if (view.alpha <= 0f) {
            return 0f
        }
        val drawingCache = captureView(view)
        return drawingCache.getPixel(ev.x.toInt(), ev.y.toInt()).alpha.toFloat()
    }
複製代碼

如此去獲取 bitmap,寬高是 Webview 的寬高,在這裏至關於屏幕高度。首先,bitmap 佔用內存會很大,4 * 1080 * 2248byte ,同時去繪製整個 Webview,也會很是耗時,平均時間 90ms 左右。

事件衝突

某些 Activity 在 dispatchTouchEvent 的時候會攔截事件,進行一些操做,舉個雲音樂播放頁面的例子:

@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        // ...
        // Poplayer處理
        if (Poplayer.isContainerWebViewInterceptTouch(event, this)) {
            DebugLogUtils.log(TAG, "isContainerWebViewInterceptTouch");
            return super.dispatchTouchEvent(event);
        }
        return ... || commentGestureHelper.handleDispatchTouchEvent(event)
                || super.dispatchTouchEvent(event);
    }
複製代碼

其中一個就是攔截事件,上滑的時候進入評論頁。因此咱們在這裏首先要判斷 WebView 是否會攔截這個事件,那麼也就會調用 captureView 方法去繪製,那麼也就會多一次 captureView 的調用,若是 WebView 未攔截事件,最終事件回到 Activity 又會致使一次 captureView 調用。

總共就是三次調用,所耗費的時間是三倍的繪製時間 3 * 90ms ,申請的內存也是 3 倍。

優化措施

新增位置及 alpha 值的緩存:

data class AlphaCache(var eventX: Int = -1, var eventY: Int = -1, var alpha: Float = 0f)
複製代碼

記錄上次點擊的 X, Y 及 alpha 值,若是下次方法調用和以前的點擊點一致的話,就不從新計算

fun getViewTouchAlpha(ev: MotionEvent, view: View): Float {
        if (view.alpha <= 0f) {
            return 0f
        }
        if (alphaCache.eventX == ev.x.toInt() && alphaCache.eventY == ev.y.toInt()) {
            return alphaCache.alpha
        }
        val drawingCache = captureView(view)
        return drawingCache.getPixel(ev.x.toInt(), ev.y.toInt()).alpha.toFloat()
    }
複製代碼

這樣能夠減小 2 次內存申請和 2 次繪製,性能優化了 4 倍。

Bitmap 大小優化

bitmap 若是大小是屏幕寬高的話,申請的內存會很是大,那咱們是否是能夠縮小 bitmap 的大小,因而想到了一種方案

優化措施一:
// BITMAP_WIDTH = 10 
private fun captureView(evX: Int, evY: Int, view: View): Bitmap {
        val bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_WIDTH, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        canvas.translate(-evX + BITMAP_WIDTH / 2f, -evY + BITMAP_WIDTH / 2f)
        view.draw(canvas)
        return bitmap
}
複製代碼

咱們把 bitmap 的大小改成了 10 * 10 ,同時移動畫布,使繪製的位置恰好在這個 bitmap 內,經過傳入的點擊位置去肯定畫布的位置

此時內存變爲 4 * 10 * 10byte,內存減小了 20000 多倍。

優化措施二

在優化了 bitmap 內存以後,發現快速點擊視頻仍是會出現一點卡頓,因而測試了 view.draw(canvas) 方法,發現其在每一次觸發的時候須要消耗的時間平均在 90ms 左右,因此會致使卡頓出現

draw 繪製的時候是否能夠只去繪製一小部分:

private fun captureView(evX: Int, evY: Int, view: View): Bitmap {
        val bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_WIDTH, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        canvas.translate(-evX + BITMAP_WIDTH / 2f, -evY + BITMAP_WIDTH / 2f)
        canvas.clipRect(evX - BITMAP_WIDTH / 2f, evY - BITMAP_WIDTH / 2f ,evX + BITMAP_WIDTH / 2f, evY + BITMAP_WIDTH / 2f)
        view.draw(canvas)
        return bitmap
    }
複製代碼

使用 clipRect 的方式,讓其在繪製的時候只去繪製一小部分。

優化後實測在 100 * 100 的 Rect 中繪製只需 9ms ,而在10 * 10的 rect 中繪製平均只需 1ms ,這裏速度優化了 90 倍!

優化措施三

bitmap 的大小爲 4 * 10 * 10byte,這個 4 是 ARGB_8888 中來的,可是咱們這一次只是用到了其中的 alpha 值,那是否是不用這麼多?

private fun captureView(evX: Int, evY: Int, view: View): Bitmap {
        // 使用 ALPHA_8
        val bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_WIDTH, Bitmap.Config.ALPHA_8)
        val canvas = Canvas(bitmap)
        //...
        return bitmap
    }
複製代碼

使用這種方式,又把內存佔用優化到了本來的四分之一

優化總結

總體優化下來,內存佔用少了 80000 多倍,繪製耗時少了 90 倍,快速點擊 web 頁面在播放視頻時徹底感受不到卡頓,很是完美!

WebView 加強

在實際上線以後,發現有不少WebView出現 android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed 異常,這個異常在平時也能看到,但都沒有引發重視,因爲咱們 Poplayer 用在了流量很是大的一個頁面,因此該問題直接暴露。

解決方法其實很簡單:

// Poplayer容器由一個Fragment包裹
val rootView = runCatching {
            super.onCreateView(inflater, container, savedInstanceState)
        }.getOrElse {
            activity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss()
            return null
        }
複製代碼

固然須要注意的是 onCreateView 返回以後,一些 super 的邏輯執行不到,可能引起一些問題,須要在開發的時候規避。

總結

當你一直在作一些重複工做,感受到噁心時,就必須考慮,這些工做是否存在必定的共通性,是否有辦法能夠進行優化,是否能夠藉助一些工具來提高效率,而不是又雙叒叕的去重複着這些事情。

不少場景不僅是本身會遇到,也許業界早已經有了相對成熟的方案可使用,平時也能夠多關注業界的發展,拓寬本身的思路。固然在參考一些方案的時候也要去適配本身的一些特性,達到高可用狀態。

後續改進

目前 Poplayer 容器仍是 HTML5 來承載,HTML5 自己的性能以及不穩定性問題仍然存在,後續能夠考慮使用 ReactNative 容器,對於非動態化場景,也能夠考慮 Flutter 容器來作,只須要容器層去接入,同時傳遞點擊事件便可。

參考資料

利用 Poplayer 在手淘中實現穩定業務和臨時業務分離

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!

相關文章
相關標籤/搜索