Weex日记——2

经过一个星期的weex学习,踩到了一些坑,也更加了解了weex,继续weex系列的文章,这篇文章会有一些干货,希望大家看完会有收获~

前言

在看这篇文章之前,请确保你已经有了一定的Weex基础,至少要了解它是如何运作的。

Weex中的坑

首先,这里会以Android端为例,iOS和h5端我也没有深入理解过,所以就不讲了。

首先第一个坑,Weex在Android端不支持emoji。如果我们通过网络请求去拉去了一些数据,其中包含emoji图片的话,Weex在处理上会有一些问题,在特定情况下会导致页面渲染不出来。

下面我们一步一步来分析这个坑的原因。首先我们知道,Weex在Android端会有一个类叫做WXStreamModule,它是Weex的一个组件,用来执行网络请求,我们看它的fetch方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@WXModuleAnno
public void fetch(String optionsStr, final JSCallback callback, JSCallback progressCallback){

JSONObject optionsObj = null;
try {
optionsObj = JSON.parseObject(optionsStr);
}catch (JSONException e){
WXLogUtils.e("", e);
}

boolean invaildOption = optionsObj==null || optionsObj.getString("url")==null;
if(invaildOption){
if(callback != null) {
Map<String, Object> resp = new HashMap<>();
resp.put("ok", false);
resp.put(STATUS_TEXT, Status.ERR_INVALID_REQUEST);
callback.invoke(resp);
}
return;
}
String method = optionsObj.getString("method");
String url = optionsObj.getString("url");
JSONObject headers = optionsObj.getJSONObject("headers");
String body = optionsObj.getString("body");
String type = optionsObj.getString("type");
int timeout = optionsObj.getIntValue("timeout");

Options.Builder builder = new Options.Builder()
.setMethod(!"GET".equals(method)
&&!"POST".equals(method)
&&!"PUT".equals(method)
&&!"DELETE".equals(method)
&&!"HEAD".equals(method)
&&!"PATCH".equals(method)?"GET":method)
.setUrl(url)
.setBody(body)
.setType(type)
.setTimeout(timeout);

extractHeaders(headers,builder);
final Options options = builder.createOptions();
sendRequest(options, new ResponseCallback() {
@Override
public void onResponse(WXResponse response, Map<String, String> headers) {
if(callback != null) {
Map<String, Object> resp = new HashMap<>();
if(response == null|| "-1".equals(response.statusCode)){
resp.put(STATUS,"-1");
resp.put(STATUS_TEXT,Status.ERR_CONNECT_FAILED);
}else {
resp.put(STATUS, response.statusCode);
int code = Integer.parseInt(response.statusCode);
resp.put("ok", (code >= 200 && code <= 299));
if (response.originalData == null) {
resp.put("data", null);
} else {
String respData = readAsString(response.originalData,
headers!=null?getHeader(headers,"Content-Type"):""
);
resp.put("data",parseJson(respData,options.getType()));
}
resp.put(STATUS_TEXT, Status.getStatusText(response.statusCode));
}
resp.put("headers", headers);
callback.invoke(resp);
}
}
}, progressCallback);
}

当我把断点打到对应的onResponse回调中,会发现对应的emoji是这样的:

weex_emoji_1

可以看到emoji被转义了,而这样转义的emoji是可以在Android中正常显示的。在onResponse的最后,会通过callback传递到js层,经过JSFramework渲染之后重新下发到Native层,也就是WXBridgeManager的callNative方法,而当我把断点打到callNative方法里,却发现emoji变成了这样:

weex_emoji_2

不再是转义的了,而是一堆乱码。如果这个emoji是对应字符串的第一个或者最后一个字符,那么这个字符串的引号将丢失,导致json解析失败,页面也就渲染不出来了。

这就很头痛了,明明在WXStreamModule中还是好的,说明数据是没有问题的,但是经过JSFramework渲染之后就失败了,在询问了对应的Weex开发同学之后得到的答案是Android NDK的bug,对应的issue大家可以看这里这里

第二个坑,如果你在你的App的第一个页面就使用了Weex的模块,也就是在代码中使用了类似

1
2
var stream = require('@weex-module/stream');
stream.fetch({.....});

这样的代码,可能会报出这个类的方法找不到。

这个原因又是什么呢,我在看Weex源码的时候,发现它整个初始化过程是在子线程中完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static void doInitInternal(final Application application,final InitConfig config){
WXEnvironment.sApplication = application;
WXEnvironment.JsFrameworkInit = false;

WXBridgeManager.getInstance().getJSHandler().post(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
WXSDKManager sm = WXSDKManager.getInstance();
if(config != null ) {
sm.setIWXHttpAdapter(config.getHttpAdapter());
sm.setIWXImgLoaderAdapter(config.getImgAdapter());
sm.setIWXUserTrackAdapter(config.getUtAdapter());
sm.setIWXDebugAdapter(config.getDebugAdapter());
sm.setIWXStorageAdapter(config.getStorageAdapter());
if(config.getDebugAdapter()!=null){
config.getDebugAdapter().initDebug(application);
}
}
WXSoInstallMgrSdk.init(application);
boolean isSoInitSuccess = WXSoInstallMgrSdk.initSo(V8_SO_NAME, 1, config!=null?config.getUtAdapter():null);
if (!isSoInitSuccess) {
return;
}
sm.initScriptsFramework(null);

WXEnvironment.sSDKInitExecuteTime = System.currentTimeMillis() - start;
WXLogUtils.renderPerformanceLog("SDKInitExecuteTime", WXEnvironment.sSDKInitExecuteTime);
}
});
register();
}

可以看到,initScriptsFramework加载JSFramework和加载v8的so都是通过JSHandler去post完成的,而在register方法中调用的各种registerModule和registerComponent也是一样,而这个JSHandler是WXThread的handler,WXThread是一个HandlerThread,所以我们可以肯定整个Weex的初始化是在子线程完成的。当时看到这里我深吸一口凉气,我可以理解这么做的原因,毕竟初始化是非常耗时的,放在子线程中有一定的性能考虑,但是这样就导致[初始化]和[调用]是异步的,所以我虽然在Application中初始化了Weex但是在第一个页面使用模块的时候可能模块还没有加载完成,导致方法找不到。为此我特意给Weex的开发同学写了邮件,得到的回答是[Weex的js执行逻辑也都是放在同一个子线程中完成的,并且由于是HandlerThread存在一个队列,所以是同步的]。所以我又去仔细的看了看源码,发现确实是这样。

在WXBridgeManager中类似createInstance这样的方法中,也都是通过post的形式去完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
post(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
invokeCreateInstance(instanceId, template, options, data);
final long totalTime = System.currentTimeMillis() - start;
WXSDKManager.getInstance().postOnUiThread(new Runnable() {

@Override
public void run() {
WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(instanceId);
if (instance != null) {
instance.createInstanceFinished(totalTime);
}
}
}, 0);
}
}, instanceId);

这里确实是我看代码不仔细了。至于为什么还会出现模块的方法找不到。。。不清楚,还在调研中。

除了上面说的两个坑,Weex还存在大大小小不少的问题,不过也都是可以接受的,毕竟开源没多久,肯定会有很多问题,对症下药就好了~

Weex的最佳实践

关于自定义Component和自定义Module这里我就不说了,很简单的,可以参考这里

下面我说我的两个最佳实践吧。

第一个是关于自定义HttpAdapter的。看过源码的同学都知道,Weex原生的DefaultHttpAdapter使用的是HttpUrlConnection,这个对于我们来说显然是不可以接受的,所以我这里封装了一个OkHttpAdapter。具体的地址在这里

具体的做法就是继承IWXHttpAdapter接口,重写它的sendRequest方法。

1
2
3
4
5
6
public class OkHttpAdapter implements IWXHttpAdapter{
@Override
public void sendRequest(final WXRequest request, final IWXHttpAdapter.OnHttpListener listener) {

}
}

而上面的WXStreamModule的源码也有展示出,它最终就是通过获取注册在Weex中的IWXHttpAdapter的sendRequest去完成网络请求的。其中的WXRequest就是Weex帮我们组装的http request,listener是对应的回调,其中有很多方法,会回调到js层对应的两个方法(一个进度监听,一个完成监听)。我们需要做的就是把WXRequest重新组装成OkHttp的Request,再发送就可以了。具体的代码大家可以去看上面提到的Weex-OkHttp-Adapter这个库。其中进度监听这一部分参考了区长的CorePreogress

第二个最佳实践是关于加载页面的。我们知道,可以通过WXSDKInstance的render方法去加载一个对应的js文件,就像这样:

1
2
3
4
5
6
7
8
mInstance.render(
packageName,
template,
options,
jsonInitData,
WXViewUtils.getScreenWidth(this),
WXViewUtils.getScreenHeight(this),
WXRenderStrategy.APPEND_ASYNC);

但是呢这个加载是很耗时的,通过需要1-2s的时间,期间会出现白屏,这是有点无法接受的,特别是在将Weex嵌入到App中的情况下,用户在使用过程中点开一个页面居然有1-2s的白屏,什么鬼。这里我做的事情是将其render的时机提前,比如我们的A页面是一个纯Native的页面,B页面是一个Weex页面,我们可以在A页面的时候就去渲染B对应的js bundle,然后做一个缓存,就像这样:

1
public class AActivity extends AppCompatActivity implements IWXRenderListener

我们将AActivity也实现IWXRenderListener接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);

.........
preLoadWeex();
}

private void preLoadWeex() {
WXSDKInstance mInstance = new WXSDKInstance(this);
mInstance.registerRenderListener(this);
renderPage(mInstance,getPackageName(), WXFileUtils.loadAsset("BPage.js",this),WEEX_INDEX_URL,null);
}

在AActivity中就去渲染B页面的js bundle。

1
2
3
4
@Override
public void onViewCreated(WXSDKInstance instance, View view) {
PreLoader.put("BPage.js",new Weexer(instance,"BPage.js",view));
}

并且在对应onViewCreated回调中缓存对应的view和instance实例。

然后在BActivity中就可以直接使用了~

1
2
3
4
5
6
7
8
9
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if(PreLoader.get("BPage.js") != null && PreLoader.get("BPage.js").weexView != null){
((ViewGroup)findViewById(R.id.container)).addView(PreLoader.get("BPage.js").weexView);

}

通过这样的预加载,就能做到秒开一个Weex页面,不会影响用户体验~

后记

周末有事,所以今天就把周末的文章写完了,也算是一个小节,Weex之路还要继续走下去啊~

另外昨天我一个关系最好的初中同学去英国读书了,加上之前发生的一些事,感觉十多年的感情转瞬即逝。

这是一个流行离开的世界,但是我们都不擅长告别。

england