转载请注明出处: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的项目来说,这一点非常重要。