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!

相关文章
相关标签/搜索