本文做者: Codey前端
你是否还在为各类特殊场景特殊逻辑而烦恼,是否还在为各类一次性业务而添加一堆代码,是否还在为各类奇奇怪怪的彩蛋而满心疲惫? 在云音乐不断迭代的过程当中,咱们不止一次的遇到产品说要在某一个地方加个彩蛋,有的是在触及特殊操做时,有的是在播放特定歌曲时,甚至有的是在特定时间点播放特定歌曲到特定播放进度时。java
每次听到这些需求,头都大了,又得在老代码上面加一堆特殊逻辑,又得写那么多代码,重点写那么多代码还不能复用,同时也增长了稳定业务的复杂度,也没有什么实时性、动态性可言。android
在经历了几回这种需求以后,咱们就在想如何去避免这类临时业务和稳定业务融合到一块儿,如何去把这类临时业务统一成一套通用可行方案?git
在这以前,咱们先了解下什么是临时业务,什么是稳定业务。github
**临时业务:**特定时间,特定场景,特定配置下须要上线的业务;不可复用,可能只一次使用。对云音乐来讲就是彩蛋系列,这里举一个特定的场景,国庆节国歌升旗彩蛋,须要在国庆节期间的天安门升旗时间播放国歌,唤起升旗的视频。对于这类型的场景,基本知足了咱们对于临时业务的定义,能够将其认为是临时业务web
**稳定业务:**核心功能,基础功能,长期存在,非必要状况下不随意修改。对云音乐来讲就是最重要的就是播放业务,包含播放列表,播放页面等播放核心功能,这块逻辑咱们实际上是不但愿去随意改动的,要是随意增长个彩蛋就疯狂改动这块的逻辑,疯狂添加些临时且不可复用的代码,那将会增长这块的逻辑复杂度,维护复杂度,还未引发一些意外的问题。播放业务也是基本知足了咱们对稳定业务的定义,能够将其认为是稳定业务。canvas
了解了临时业务和稳定业务以后,就能够想办法去作区分解决了。对于临时业务,须要的通用方案得知足可配置,可复用,实时性,动态性等要求,在你们的讨论下,便想到了 Poplayer 这个大杀器。缓存
Poplayer ,顾名思义,就是Pop + Layer的组合,结合 Android 场景来看,其实就是页面上层再加一层,称做 Poplayer。经过 Poplayer 层咱们能够将一些临时业务交由这一层去处理维护,而这一层又交由 WebView 去承载,在增长动态性的同时又不影响既有稳定业务。性能优化
从上面的图能够很是清晰的看出来,所谓的 Poplayer 就是在客户端页面上增长一层,将这一层做为展现临时业务的容器,两者经过 JSBridge 通讯,再结合一些客户端页面配置以及容器配置,达到临时业务热插拔,可复用的要求。markdown
配置中心:云音乐基础能力之一,以 key / value 的形式存储业务及功能的特殊配置,支持配置秒级下发及分端下发等功能
从上图能够看出,咱们经过配置能力将客户端页面和容器结合到一块儿,总体流程结构都是很是清晰的。依赖于云音乐完善的基础设施,在完成 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_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出现 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 容器来作,只须要容器层去接入,同时传递点击事件便可。
本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!