关于android:Poplayer-云音乐优化实践

57次阅读

共计 5160 个字符,预计需要花费 13 分钟才能阅读完成。

本文作者:Codey

背景介绍

你是否还在为各种非凡场景非凡逻辑而懊恼,是否还在为各种一次性业务而增加一堆代码,是否还在为各种奇奇怪怪的彩蛋而满心疲乏? 在云音乐一直迭代的过程中,咱们不止一次的遇到产品说要在某一个中央加个彩蛋,有的是在涉及非凡操作时,有的是在播放特定歌曲时,甚至有的是在特定工夫点播放特定歌曲到特定播放进度时。

每次听到这些需要,头都大了,又得在老代码下面加一堆非凡逻辑,又得写那么多代码,重点写那么多代码还不能复用,同时也减少了稳固业务的复杂度,也没有什么实时性、动态性可言。

在经验了几次这种需要之后,咱们就在想如何去防止这类长期业务和稳固业务交融到一起,如何去把这类长期业务对立成一套通用可行计划?

在这之前,咱们先理解下什么是长期业务,什么是稳固业务。

长期业务:特定工夫,特定场景,特定配置下须要上线的业务;不可复用,可能只一次应用。对云音乐来说就是彩蛋系列,这里举一个特定的场景,国庆节国歌升旗彩蛋,须要在国庆节期间的天安门升旗工夫播放国歌,唤起升旗的视频。对于这类型的场景,根本满足了咱们对于长期业务的定义,能够将其认为是长期业务

稳固业务:外围性能,根底性能,长期存在,非必要状况下不随便批改。对云音乐来说就是最重要的就是播放业务,蕴含播放列表,播放页面等播放外围性能,这块逻辑咱们其实是不心愿去随便改变的,要是随便减少个彩蛋就疯狂改变这块的逻辑,疯狂增加些长期且不可复用的代码,那将会减少这块的逻辑复杂度,保护复杂度,还未引起一些意外的问题。播放业务也是根本满足了咱们对稳固业务的定义,能够将其认为是稳固业务。

理解了长期业务和稳固业务之后,就能够想方法去做辨别解决了。对于长期业务,须要的通用计划得满足 可配置,可复用,实时性,动态性 等要求,在大家的探讨下,便想到了 Poplayer 这个大杀器。

什么是 Poplayer

简介

Poplayer,顾名思义,就是 Pop + Layer 的组合,联合 Android 场景来看,其实就是页面下层再加一层,称作 Poplayer。通过 Poplayer 层咱们能够将一些长期业务交由这一层去解决保护,而这一层又交由 WebView 去承载,在减少动态性的同时又不影响既有稳固业务。

Poplayer 的设计

设计概要

从下面的图能够十分清晰的看进去,所谓的 Poplayer 就是在客户端页面上减少一层,将这一层作为展现长期业务的容器,二者通过 JSBridge 通信,再联合一些客户端页面配置以及容器配置,达到长期业务热插拔,可复用的要求。

整体流程与设计

配置核心:云音乐根底能力之一,以 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!

正文完
 0