半年小记(二) : 终端H5优化

转载请注明出处:https://blog.csdn.net/anyfive/article/details/87868347

在架构调整结束后,我们开始在终端设备上进行原生+WebView的技术方案尝试。我们希望将业务层尽可能地交由H5来实现,以便带来快速灵活的迭代,同时可以更好地满足不同客户之间的定制化需求,这对我们这种to B的项目来说,尤其重要。

众所周知的是,WebView的坑不少,性能比起原生也要差一点。但我们的项目只需要运行于搭载RK3399的设备上,而其上运行的rom也是开源的,这意味着我们拥有一定的掌控力,这给了我们信心。

在方案的落地过程中,我们也碰到了不少的问题。对一些印象比较深刻的点,也一并在此记录分享。

动画卡顿

在开发初期,就接到前端同学的反馈,说有一个动画播放过程中会卡住数秒。

在对这部分js代码进行review时,发现前端是使用动画框架生成序列帧动画,而该框架生成的代码中,有部分冗余的操作,因此进行recode。

但重写了动画后,卡顿并未消失,甚至连细微的改善也不太明显。使用Chrome的远程调试工具调试发现,卡顿的大头在于decode image的操作。因为该动画播放结束后,需显示一个布局,该布局中含有大量的图片,这些图片的解码直接导致了页面的卡顿。这个解码操作在电脑上并不会导致卡顿,但我们设备性能有限,就出现了卡顿。

在找到问题后,尝试了初始化时先将布局移至屏幕外、调用Image.decode()进行预解码等方法,均无法达到效果。后来想到,html的img便签,可以直接使用Base64字符串作为src显示图片,而解码Base64,显然会比解码图片文件快太多;而且,图片转Base64完全可以在运行期外进行,比如在服务端进行,这样一来,虽然数据的大小有增加,但卡顿问题就可以解决了。

于是,我们将所有图片在服务端转为Base64后,再下发给客户端,js获得Base64字符串后,直接使用img进行显示。事实证明,该方案是可行的,卡顿问题基本解决,优化完成。

视频卡顿,6路硬解码问题

另外一次让我印象深刻的优化经历,就是视频卡顿的优化。据产品同学的反馈,app运行一段时间后,会出现视频播放卡顿的情况。刚听到这个反馈的时候,第一反应就是是不是哪里内存泄露了,但review代码后发现并没有找到内存泄露的地方,且查看内存,发现内存的占用一直趋于稳定,视频卡顿时,内存也没有明显变化。

排除内存的原因后,依次对cpu占用和温度、gpu频率和温度等进行查看,都未发现异常。但在反复的测试过程中,发现一个规律:页面A中有6个视频轮播,当6个视频全部播放过之后,页面B的视频就会发生卡顿;而页面A中播放过的视频数不足6个时,跳转到页面B,视频不会卡顿,返回页面A将视频播放完成后,再跳转到页面B,页面B的视频又开始卡顿。于此同时,发现当视频卡顿时,使用adb shell top -m 10命令查看进程信息时,解码进程media.codec的cpu占用就会开始升高。

于是猜测代码中播放视频后未释放,导致硬解码占用数达到上限,下一个视频播放时触发软解,导致视频卡顿;之后开始review代码,发现页面A的js部分播放视频后,使用video.currentTime=0来重置视频,以便提高下次播放的加载速度;去掉该代码后,视频卡顿问题解决,优化完成。

之后与主板厂商沟通时,对方说RK3399最多支持视频的6路硬解,这也解答了为什么页面A播放完6个视频后,页面B才开始卡顿的疑问。

更换内核

由于客户端的开发与前端的开发是同时进行的,因此前端的开发和调试,是在设备的chrome上进行的。当客户端开发完成后,前端的同学开始在我们app的WebView上运行页面,结果发现比起Chrome上运行,运行于WebView上非常慢且卡顿。

通过Chrome的远程调试工具,发现设备的Chrome的内核是68版本,而app使用的系统自带的内核是53版本的,猜测是版本过低导致性能问题。于是打算更换系统内核,从网上下载了一个70版本的Android System WebView安装上去,结果系统还是使用旧的内核,查看包名发现新版的内核的包名是:com.google.android.webview,而系统自带内核的包名为com.android.webview,因此无法生效。

此时打算直接替换rom中的内核,再修改索引文件中的包名,以替换内核。但这样一来,所有设备都需要重新刷系统。于是开始在网上寻找其他方案,最后感谢这个帖子:https://bbs.360.cn/thread-15298418-1-1.html,按该帖子的方法使用MT文件管理器,找到framework-res.apk,反编译后修改包名再编译,重启设备就生效了。(由于设备不同,操作方式与原贴略有不同,但方法是类似的。)

替换内核成功后,再将修改后的framework-res.apk文件拖出来,写一个脚本,让同事挨个设备运行一遍脚本,便完成了这次替换内核的工作。事实证明,替换内核后,运行效果与设备上的Chrome一般无二。

jssdk封装

经过一系列的开发、调试与优化,我们得出结论:对于业务简单的项目,完全可以使用这套方案;而稍微复杂一些的项目,该方案还是有性能问题,不够流畅与稳定。

于是,我便开始统一客户端与前端的通信规范。收到前端同学的建议,简单封装了一个jssdk,由于涉及公司业务暂时不便公开,在这里便另写一个Demo与大家分享。

我们知道,android可以使用WebView的evaluateJavascript方法调用js的代码;而js要调用android的代码,在android端使用addJavascriptInterface方法注册Bridge-Object后,可以使用类似window.Name.funName()的形式调用Bridge-Object中有JavascriptInterface注解的方法。这部分内容,在此就不再累赘了,直接看Demo代码。

var DemoSDK = (function () {
    var that = this;
    var _export = {
        Events: {
            EVENT_TEST_1: "event_test_1", // 测试事件1
            EVENT_TEST_2: "event_test_2", // 测试事件2
        }
    };
    // 所有事件, 用于检验Event
    that._All_Events = [];
    for (var key in _export.Events) {
        that._All_Events.push(_export.Events[key]);
    }
    // 初始化, js通过`DemoSDK.init()`设置config
    _export.init = function (config) {
        if (!config) {
            config = {};
        }
        that.config = config;
    };
    // 客户端通过`DemoSDK._getConfig()`获得js注册的config
    _export._getConfig = function () {
        return that.config || {};
    };
    // 添加事件监听
    // js通过`DemoSDK.addEventListener(DemoSDK.Events.EVENT_TEST_1, function(arg1, arg2...){})`注册事件监听
    _export.addEventListener = function (event, cb) {
        if (!event || !(typeof event === 'string')) {
            throw "event is null or not String";
        }
        if (that._All_Events.indexOf(event) < 0) {
            throw "sdk doesn't have this event";
        }
        if (!cb || !(cb instanceof Function)) {
            throw "event-listener is null or not a Function";
        }
        if (!that[event] || !(that[event] instanceof Array)) {
            that[event] = [];
        }
        that[event].push(cb);
    };
    // 移除事件监听
    // js通过`DemoSDK.removeEventListener(Demo.Events.EVENT_TEST_1, cb)`移除事件监听器
    _export.removeEventListener = function (event, cb) {
        if (!event || !(typeof event === 'string')) {
            throw "event is null or not String";
        }
        if (that._All_Events.indexOf(event) < 0) {
            throw "sdk doesn't have this event";
        }
        if (!that[event] || !(that[event] instanceof Array)) {
            return;
        }
        if (!cb) {
            that[event] = [];
            return;
        }
        if (that[event].indexOf(cb) >= 0) {
            that[event].splice(that[event].indexOf(cb), 1);
        }
    };
    that.applyCallback = function () {
        if (arguments.length < 1) {
            return;
        }
        var event = arguments[0];
        var args = (arguments.length === 1 ? undefined : Array.apply(null, arguments).slice(1));
        if (that[event] && that[event] instanceof Array) {
            that[event].forEach(function (cb) {
                if (cb instanceof Function) {
                    cb.apply(this, args);
                }
            });
        }
    };
    // 是否 ready
    that.hasInited = false;
    // 在 ready 状态前,各需要延迟的函数
    that.beforeReadyFuns = [];
    // 通过该函数调用的方法, 若此时已经ready, 方法将直接调用
    // 若未ready, 方法和参数将保存到beforeReadyFuns数组中,待ready时依次调用
    that.applyFunNeedInited = function (fun, args) {
        if (that.hasInited) {
            return fun.apply(window.Demo, args);
        }
        that.beforeReadyFuns.push([fun, args]);
    };
    // 客户端调用`DemoSDK._ready()`方法回调js的ready方法
    // 一些需要在ready之后才能执行的方法, 可通过applyFunNeedInited函数调用
    _export._ready = function () {
        that.hasInited = true;
        that.beforeReadyFuns.forEach(function (value) {
            value[0].apply(window.Demo, value[1]);
        });
        that.beforeReadyFuns = [];
    };
    // js通过`DemoSDK.ready(function(){})`方法注册ready监听
    _export.ready = function(cb) {
        that.applyFunNeedInited(cb);
    };
    // 客户端调用js示例函数1
    // 客户端调用`DemoSDK._funEvent1()`时, js会收到EVENT_TEST_1事件
    _export._funEvent1 = function () {
        that.applyCallback(this.Events.EVENT_TEST_1);
    };
    // 客户端调用js示例函数2, 带参数
    _export._funEvent2 = function (args) {
        that.applyCallback(this.Events.EVENT_TEST_2, args);
    };
    // js调用客户端示例1
    _export.callAndroidTestFun1 = function () {
        that.applyFunNeedInited(window.Demo.callAndroidTestFun1);
    };
    // js调用客户端示例2, 带参数
    _export.callAndroidTestFun2 = function (args) {
        that.applyFunNeedInited(window.Demo.callAndroidTestFun2, args);
    };
    // js调用客户端示例3, 不需等ready回调, 即立刻执行
    _export.callAndroidTestFun3 = function (args) {
        window.Demo.callAndroidTestFun3(args);
    };
    return _export;
})();

注释已经非常详细,在此就不再说明以上代码了。调用方式也非常简单:

// 初始化
DemoSDK.init({});
// 事件监听器
var Event2CB = function(args) {};
// 注册事件监听
DemoSDK.addEventListener(DemoSDK.Events.EVENT_TEST_2, Event2CB);
// 取消事件监听
DemoSDK.removeEventListener(DemoSDK.Events.EVENT_TEST_2, Event2CB);
// 调用客户端方法
DemoSDK.callAndroidTestFun2(args);

jssdk使得客户端与前端的通信变得更为规范,配合文档,极大程度减少了两边的沟通成本;甚至一些简单的业务,产品可以直接通过前端同学完成,而无需再经过客户端。

后记

在整个方案实施过程中,还有许多坑,我们也做了很多的工作来保证开发进度;甚至做好了随时切换到客户端来开发业务的准备。虽然这套方案最后没有达到完全替代原生的程度,但对我们来说,这是一次有意义有价值的尝试;该方案可以满足大多数简单的定制化业务需求,对我们这种to B的项目来说,这一点非常重要。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值