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 接口, 在shouldStartLoadWithRequest里去实现:

- (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交互过程是这样的:

  1. Native打开页面时注册接口:“webView.addJavascriptInterface(jsInterface, “webview”);” 其中jsInterface是JsRemoteInterface类的实例:
public final class JsRemoteInterface {
@JavascriptInterface
public void post(String cmd, String param) {
    ...
}
  1. Web页面通过“window.webview.post(cmd,JSON.stringify(para))”调用native;
  2. Native(即Webview进程)收到调用之后,通过IWebAidlInterface实例传递给主进程执行;
  3. 主进程收到action请求之后,根据actionname分发处理,执行结束之后通过IWebAidlCallback完成进程间回调。

总结

我们实现了一套口型动画合成系统,该系统利用深度学习完成从语音到口型动画的映射,可以有效解决语音动画同步的难题,增强动画的真实感和逼真性。同时,该系统对于说话人和语言不敏感,对于中英文的支持普遍好于市面上的同类产品。此外,该系统由于只需要音频文件,所以极大的简化了口型动画的制作流程,减少了相关的时间成本和人员开销。

参考文献: