Webview优化
webview是太强大了,微信简直一个webview的app。在游戏中集成webview,已经很多厂商都已经做了,大多集中在活动系统,网络直播推流等在游戏中有了广泛的应用。在腾讯的游戏中,一般都会集成MSDK,里面自带了webview, 算是一个典型的应用吧。
此文主要针对webview一些特性,提出一些优化建议。目前一般游戏引擎不会自带Webview, 需要开发者独自以Native的方式接入Webview。
Webview与游戏通信
一般web都是使用js来编写动态逻辑,当我们向webview通过url是哟get或者post的方式来请求的时候,native底层能截获发送的信息,native根据定义的协议来判断是派发给游戏逻辑,还是给webview来加载页面。
游戏收到通知之后,可以做出相应的响应,比如关闭webview。 又时候webview是放在子进程里了(Android平台),游戏进程就在后台如果长时间不发送心跳,游戏服务器会自动断开连接,这时候用处就显而易见了。
Android中截获url事件, 并重载调默认逻辑, 如下代码:
WebView webView = new WebView(gameActivity);
webView.setWebViewClient(new WebViewClient()
{
public boolean shouldOverrideUrlLoading(WebView view, String url)
{
gameActivity.Call(_gameObjectName, "OnLoadingUrl", url);
if (url.startsWith("litewebview://")) {
//返回true表示不需要的再做处理了
onJsCall(url);
return true;
}
else if (url.startsWith("file://") || url.startsWith("http://") || url.startsWith("https://")) {
//加载网页
return super.shouldOverrideUrlLoading(view, url);
}
else {
try {
// 以下固定写法
final Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
gameActivity.startActivity(intent);
return true;
}
catch (Exception e) {
MLog.e(TAG, e.getMessage());
}
}
return super.shouldOverrideUrlLoading(view, url);
}
});
上述代码中, Webview是android.webkit包体里的一个类, 这里通过shouldOverrideUrlLoading来截获url, 如果url以litewebview开头, 那就直接把消息返回给Unity, c#代码区中了。 webview就不需要在渲染网页内容了, Unity收到消息,可以在后台处理,比如发同步消息给Server, 来避免断开连接。
这个url的链接往往是以渲染web页面上的按钮, 对应的html代码可能类似于:
<a href="litewebview://test?arg1=1&arg2=hello">按钮</a>
或者是以js的方式存在:
<a id="c_btn">按钮</a>
<script type="text/javascript">
$("#c_btn").click(function(){
$("#c_btn").location.href="litewebview://test?arg1=123";
}
</script>
想要解析正确的url或者post过来的表单, 必须使用明文网络流量,因此AndroidManifest.xml必须做如下声明:
<application android:usesCleartextTraffic="true">
...
</application>
ios处理url获取完毕的回调可以实现NSObject
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *url = [[request URL] absoluteString];
NSRange range = [url rangeOfString:@"LiteWebView://"];
if(range.location != NSNotFound){
NSString *msg = [url substringFromIndex:range.length];
// 同步消息给 JS
UnitySendMessage([_gameObjectName UTF8String], "OnJsCall", [msg UTF8String]);
return YES;
}
return YES;
}
如果项目中使用的是WKWebView, 可以实现接口WKNavigationDelegate里的方法就好。
最后需要在设置Link Binary With Libraries中加入:
IOS WKWebView
UIWebview会在iOS12之后弃用,全面普及WKWebview。所以还在使用UIWebview的话需要考虑一下迁移到WKWebview了。下面是把项目WKWebview脱敏之后的一些基础功能的封装。
UIwebview特点:
- 加载速度慢;
- 内存占用多,内存优化困难;
- 内存泄漏;
- 第一次加载要卡一段时间白屏
- wkwebview特点:
wkwebview特点:
- 支持更多的HTML5的特性
- 高达60fps的滚动刷新率以及内置手势
- Safari相同的JavaScript引擎
- 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议[2]
- 增加加载进度属性:estimatedProgress
- 自身不附带cookie
建议在ios8以上都使用wkwebview,并通过宏的方式兼容ios7之下的webview.
#define GREATER_IOS8 (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_8_0)
@interface LiteWebView :
#if GREATER_IOS8
NSObject<WKNavigationDelegate>
#else
NSObject<UIWebViewDelegate>
#endif
{
#if GREATER_IOS8
WKWebView* _webView;
#else
UIWebView* _webView;
#endif
NSString* _gameObjectName;
}
@end
Android 独立进程
大量Web页面的使用容易导致App内存占用巨大,存在内存泄露,崩溃率高等问题, WebView独立进程的使用是解决Android WebView相关问题的一个合理的方案。
WebView独立进程的实现比较简单,只需要在AndroidManifest中找到对应的WebViewActivity,对其配置”android: process”属性即可
<activity
android:name=".remote.RemoteCommonWebActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:process=":remoteWeb"/>
Android多进程的通讯方式有很多种,主要的方式有以下几种:
- AIDL
- Messenger
- ContentProvider
- 共享文件
- 组件间Bundle传递
- Socket传输
考虑到WebView主要的通讯方式就是方法调用,所以采用AIDL方式。AIDL本质采用的是Binder机制,这里贴一张网上的Binder机制原理图,具体的AIDL的使用方式这里不赘述, 以下是几个核心AIDL文件
IBinderPool: Webview进程和主进程的通讯可能涉及到多个AIDL Binder,从功能上来讲,我们也会把不同功能的接口写成不同的AIDL Binder,所以IBinderPool用于满足调用方根据不同类型获取不同的Binder。
interface IBinderPool {
IBinder queryBinder(int binderCode); //查找特定Binder的方法
}
IWebAidlInterface: 最核心的AIDL Binder,这里把WebView进程对主进程的每一个调用看做一次action, 每个action都会有唯一的actionName, 主进程会提前注册好这些action,action 也有级别level,每次调用结束通过IWebAidlCallback返回结果
interface IWebAidlInterface {
/**
* actionName: 不同的action, jsonParams: 需要根据不同的action从map中读取并依次转成其他
*/
void handleWebAction(int level, String actionName, String jsonParams, in IWebAidlCallback callback);
}
IWebAidlCallback: 结果回调
interface IWebAidlCallback {
void onResult(int responseCode, String actionName, String response);
}
为了维护独立进程和主进程之间的连接,避免每次aidl调用时都去重新进行binder连接和获取,需要专门提供一个类去维护连接,并根据条件返回Binder. 这个类就叫做 RemoteWebBinderPool
public class RemoteWebBinderPool {
public static final int BINDER_WEB_AIDL = 1;
private Context mContext;
private IBinderPool mBinderPool;
private static volatile RemoteWebBinderPool sInstance;
private CountDownLatch mConnectBinderPoolCountDownLatch;
private RemoteWebBinderPool(Context context) {
mContext = context.getApplicationContext();
connectBinderPoolService();
}
public static RemoteWebBinderPool getInstance(Context context) {
if (sInstance == null) {
synchronized (RemoteWebBinderPool.class) {
if (sInstance == null) {
sInstance = new RemoteWebBinderPool(context);
}
}
}
return sInstance;
}
private synchronized void connectBinderPoolService() {
mConnectBinderPoolCountDownLatch = new CountDownLatch(1);
Intent service = new Intent(mContext, MainProHandleRemoteService.class);
mContext.bindService(service, mBinderPoolConnection, Context.BIND_AUTO_CREATE);
try {
mConnectBinderPoolCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public IBinder queryBinder(int binderCode) {
IBinder binder = null;
try {
if (mBinderPool != null) {
binder = mBinderPool.queryBinder(binderCode);
}
} catch (RemoteException e) {
e.printStackTrace();
}
return binder;
}
private ServiceConnection mBinderPoolConnection = new ServiceConnection() { // 5
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinderPool = IBinderPool.Stub.asInterface(service);
try {
mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
mConnectBinderPoolCountDownLatch.countDown();
}
};
private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() { // 6
@Override
public void binderDied() {
mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0);
mBinderPool = null;
connectBinderPoolService();
}
};
public static class BinderPoolImpl extends IBinderPool.Stub {
private Context context;
public BinderPoolImpl(Context context) {
this.context = context;
}
@Override
public IBinder queryBinder(int binderCode) throws RemoteException {
IBinder binder = null;
switch (binderCode) {
case BINDER_WEB_AIDL: {
binder = new MainProAidlInterface(context);
break;
}
default:
break;
}
return binder;
}
}
}
从代码中可以看到这个连接池连接的是主进程 MainProHandleRemoteService.
public class MainProHandleRemoteService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
Binder mBinderPool = new RemoteWebBinderPool.BinderPoolImpl(context);
return mBinderPool;
}
}
Native-Web交互和接口管理
一次完整的Web页面和Native交互过程是这样的:
- Native打开页面时注册接口:“webView.addJavascriptInterface(jsInterface, “webview”);” 其中jsInterface是JsRemoteInterface类的实例:
public final class JsRemoteInterface {
@JavascriptInterface
public void post(String cmd, String param) {
...
}
- Web页面通过“window.webview.post(cmd,JSON.stringify(para))”调用native;
- Native(即Webview进程)收到调用之后,通过IWebAidlInterface实例传递给主进程执行;
- 主进程收到action请求之后,根据actionname分发处理,执行结束之后通过IWebAidlCallback完成进程间回调。
总结
我们实现了一套口型动画合成系统,该系统利用深度学习完成从语音到口型动画的映射,可以有效解决语音动画同步的难题,增强动画的真实感和逼真性。同时,该系统对于说话人和语言不敏感,对于中英文的支持普遍好于市面上的同类产品。此外,该系统由于只需要音频文件,所以极大的简化了口型动画的制作流程,减少了相关的时间成本和人员开销。
参考文献: