剑客
关注科技互联网

Weex日记——3

事隔近一个月之后,终于回归Weex系列了,这篇文章会带给大家一些Weex Android端源码中值得我们学习的片段,另外还有一个Native端主动调用JS方法的workaround。

我个人觉得看完这个系列的三篇文章,你应该对Weex Android端会有一个非常全面的了解了。至于具体的语法规则这类的嘛,去看wiki吧,这不是我写文章的目的~

前言

上一篇文章说了关于Android组件化的一些的东西,文章中提到可以用Android Studio模板去完成一些自动化的事情,比如build.gradle的修改,文件结构的修改等等,这里我写了一个传到了 我的GitHub 上,有需要的同学可以自取,使用方式已经写的很清楚了。

好了下面还是言归正传,开始Weex的学习吧。

Weex中值得学习的代码

首先,第一个值得学习的地方就是Weex对于so加载的方法。我们知道,对于so文件的加载,普通的方式就是直接使用如下代码:

System.loadLibrary(libName);

但是这样的方式会存在一定的问题,有时候会报出UnsatisfiedLinkError错误,发生这个错误的原因基本就是找不到对应的so文件,至于为什么会找不到,情况有很多种,比如拷贝文件的时候发生错误,又或者是没有考虑对应的cpu架构(例如mips)等等。而我们知道Weex在Android端是基于v8引擎的,如果这个so文件加载失败,那么整个Weex体系就崩溃了,所以确保so加载的成功在Weex上尤为重要,下面先看看Weex源码中加载so的逻辑:

public static boolean initSo(String libName, int version, IWXUserTrackAdapter utAdapter) {
String cpuType = _cpuType();
if (cpuType.equalsIgnoreCase(MIPS) ) {
return false;
}

boolean InitSuc = false;

if (checkSoIsInValid(libName, ARMEABI_Size) ||checkSoIsInValid(libName, X86_Size)) {

/**
* Load library with {@link System#loadLibrary(String)}
*/

try {
System.loadLibrary(libName);
commit(utAdapter, null, null);

InitSuc = true;
} catch (Exception | Error e2) {
if (cpuType.contains(ARMEABI) || cpuType.contains(X86)) {
commit(utAdapter, WXErrorCode.WX_ERR_LOAD_SO.getErrorCode(), WXErrorCode.WX_ERR_LOAD_SO.getErrorMsg() + ":" + e2.getMessage());
}
InitSuc = false;
}

try {

if (!InitSuc) {

//File extracted from apk already exists.
if (isExist(libName, version)) {
boolean res = _loadUnzipSo(libName, version, utAdapter);
if (res) {
return res;
} else {
//Delete the corrupt so library, and extract it again.
removeSoIfExit(libName, version);
}
}

//Fail for loading file from libs, extract so library from so and load it.
if (cpuType.equalsIgnoreCase(MIPS)) {
return false;
} else {
try {
InitSuc = unZipSelectedFiles(libName, version, utAdapter);
} catch (IOException e2) {
e2.printStackTrace();
}
}

}
} catch (Exception | Error e) {
InitSuc = false;
e.printStackTrace();
}
}
return InitSuc;
}

这个方法在WXSoInstallMgrSdk类中,调用的时机是Weex初始化的时候。

首先,该方法和普通的加载so逻辑是一样的,调用System.loadLibrary方法,但是它不仅仅是这样调用以后就简单的返回了,而是去进行一个校验,如果这样的方式加载失败了,那么会调用isExist方法,下面让我们看看这个方法:

static boolean isExist(String libName, int version) {

String file = _targetSoFile(libName, version);
File a = new File(file);
return a.exists();

}

static String _targetSoFile(String libName, int version) {
Context context = mContext;
if (null == context) {
return "";
}

String path = "/data/data/" + context.getPackageName() + "/files";

File f = context.getFilesDir();
if (f != null) {
path = f.getPath();
}
return path + "/lib" + libName + "bk" + version + ".so";

}

其实这个方法就是去判断在data/data/your_package目录下是否存在对应的so文件。

如果存在,则调用_loadUnzipSo方法去加载:

static boolean _loadUnzipSo(String libName, int version, IWXUserTrackAdapter utAdapter) {
boolean initSuc = false;
try {
if (isExist(libName, version)) {
System.load(_targetSoFile(libName, version));
commit(utAdapter, "2000", "Load file extract from apk successfully.");
}
initSuc = true;
} catch (Throwable e) {
commit(utAdapter, WXErrorCode.WX_ERR_COPY_FROM_APK.getErrorCode(), WXErrorCode.WX_ERR_COPY_FROM_APK.getErrorMsg() + ":" + e.getMessage());
initSuc = false;
WXLogUtils.e("", e);
}
return initSuc;
}

可以看到这里使用了System.load方法而不是System.loadLibrary方法,主要的区别就是System.load方法可以自定义文件路径。

如果这样还是加载so失败,则进行第三步,首先判断对应cpu架构是否是mips,如果是的话直接返回false,表示加载失败,否则调用unZipSelectedFiles方法,先去解压程序的apk文件,查看是否有对应的so文件,如果有则进行加载,下面是具体的逻辑:

static boolean unZipSelectedFiles(String libName, int version, IWXUserTrackAdapter utAdapter) throws ZipException, IOException {

String sourcePath = "lib/armeabi/lib" + libName + ".so";

String zipPath = "";
Context context = mContext;
if (context == null) {
return false;
}

ApplicationInfo aInfo = context.getApplicationInfo();
if (null != aInfo) {
zipPath = aInfo.sourceDir;
}

ZipFile zf;
zf = new ZipFile(zipPath);
try {

for (Enumeration<?> entries = zf.entries(); entries.hasMoreElements(); ) {
ZipEntry entry = ((ZipEntry) entries.nextElement());
if (entry.getName().startsWith(sourcePath)) {

InputStream in = null;
FileOutputStream os = null;
FileChannel channel = null;
int total = 0;
try {

//Make sure the old library is deleted.
removeSoIfExit(libName, version);

//Copy file
in = zf.getInputStream(entry);
os = context.openFileOutput("lib" + libName + "bk" + version + ".so",
Context.MODE_PRIVATE);
channel = os.getChannel();

byte[] buffers = new byte[1024];
int realLength;

while ((realLength = in.read(buffers)) > 0) {
//os.write(buffers);
channel.write(ByteBuffer.wrap(buffers, 0, realLength));
total += realLength;

}
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}

if (channel != null) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}

if (os != null) {
try {
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}

if (zf != null) {
zf.close();
zf = null;
}
}

if (total > 0) {
return _loadUnzipSo(libName, version, utAdapter);
} else {
return false;
}
}
}
} catch (java.io.IOException e) {
e.printStackTrace();

} finally {

if (zf != null) {
zf.close();
zf = null;
}
}
return false;
}

通过这种方式,Weex增强了加载so文件的成功率,确保整个框架的顺利运行。

Weex日记——3

说完了so加载,让我们再来看看Weex在Android端内部是如何进行消息通信的。

我们知道,在使用Weex的时候,会在Application里调用对应的初始化逻辑,就是WXSDKEngine的initialize方法。在这个方法中,Weex会去执行一些具体的任务,比如前面提到的so文件加载,初始化JSFramework,注册module模块等等。而这样的任务其实是比较耗时的,所以不可能将他们放在主线程中进行。但是放在子线程中就会出现一个问题,怎么保证这些任务的顺序呢?我们不可能在没有加载so文件或者没有初始化JSFramework的情况下就去注册module吧?所以这里Weex采用了一个比较巧妙的做法,那就是利用了Android的HandlerThread机制。

在WXBridgeManager中存在一个JSThread和JSHandler:

private WXThread mJSThread;
private Handler mJSHandler;

其中WXThread是一个HandlerThread:

public class WXThread extends HandlerThread

关于HandlerThread这里我不展开了,不了解的同学自行谷歌吧。而我们前面提到的加载so文件,初始化JSFramework和注册模块这些任务,都是通过这个JSHandler去post执行的:

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);
}
});

public static boolean registerModule(final String moduleName, final ModuleFactory factory, final boolean global) throws WXException {
if (moduleName == null || factory == null) {
return false;
}

if (TextUtils.equals(moduleName,WXDomModule.WXDOM)) {
WXLogUtils.e("Connot registered module name is dom.");
return false;
}

WXBridgeManager.getInstance().getJSHandler().post(new Runnable() {
@Override
public void run() {
if (sModuleFactoryMap.containsKey(moduleName)) {
WXLogUtils.w("WXComponentRegistry Duplicate the Module name: " + moduleName);
}

if (global) {
try {
WXModule wxModule = factory.buildInstance();
sGlobalModuleMap.put(moduleName, wxModule);
} catch (Exception e) {
WXLogUtils.e(moduleName + " class must have a default constructor without params. ", e);
}
}

try {
registerNativeModule(moduleName, factory);
} catch (WXException e) {
WXLogUtils.e("", e);
}
registerJSModule(moduleName, factory);
}
});
return true;
}

对于Handler机制,大家不熟悉的可以先去看我的这篇文章。其实就是内部存在一个消息队列,每一个消息都遵循先进先出的方式执行。而我们前面提到的这些任务,Weex也规定好了它们的顺序:先加载so文件,再初始化JSFramework,最后注册模块。这样就保证了所有任务在子线程中也可以按顺序执行。这样的方式是值得我们学习的,而不是去写一堆的锁,又低效又容易出错。

但是Weex团队在使用在使用这种方式进行子线程任务分发的时候犯了一个致命的错误,导致Weex在使用上会出现问题。

下面想象这样一个场景,我们在整个应用的第一个页面就需要使用Weex的某个模块,比如Stream,那我们在对应的.we文件中会去require它,这样问题就来了,很大概率会出现该模块找不到,换句话说就是我们在使用这个模块的时候,它还没有注册完。

首先我们知道,渲染一个页面调用的是Weex的render方法,在这个方法中,具体的createInstance其实也是通过JSHandler去post执行的。这下问题就变得不清晰了,明明我们在Application中去初始化了Weex,而在第一个Activity中调用render从而渲染页面,然后去require Stream模块,都是通过Handler的post去执行的,应该会严格按照顺序来啊,为什么会出现[调用模块在注册模块之前]这样的问题呢?

还是从代码中寻找原因吧。上面已经给出,注册一个模块,最终调用的是registerModule方法,在这个方法内会调用Handler的post方法。所以到这里,还是不会有问题的,问题就在它内部的registerJSModule方法里。这个方法最后调用的是WXBridgeManager的registerModules方法:

public void registerModules(final Map<String, Object> modules) {
if ( mJSHandler == null || modules == null
|| modules.size() == 0) {
return;
}
post(new Runnable() {
@Override
public void run() {
invokeRegisterModules(modules);
}
}, null);
}

可以看到,在这个方法里,又去执行了一个post,这样就会有问题了。

Weex日记——3

如图所示,真正执行注册模块的逻辑是那个repost中的代码,其实已经在调用模块的后面了。 Weex日记——3

就像上面这个demo一样,ccc这个task由于在内部进行了repost,实际执行会在ddd这个task之后。

关于这个问题,具体可见 这个issue 。在和对应的Weex开发进行邮件通过之后,他们已经修复了这个问题。

如何在native层主动调用js方法

在实际业务场景中可能存在这样一个需求,我们的native层需要主动去调用js层自定义的方法,这个不做任何处理是做不到的。对于这个问题,我也进行一个思考,目前发现了两个可行的方案。

方案一:修改源码做一个类似timer的机制。

我们知道在Weex中调用native向js层通信其实就是执行一段js代码,具体逻辑在WXBridgeManager中:

mWXBridge.execJS("", null, METHOD_REGISTER_COMPONENTS, args);

其中第三个参数表示是一个什么类型的逻辑,第四个参数就是具体要传到js层的参数。而对于第三个参数,Weex其实已经提供了类似[主动调用js层方法]的功能:

public static final String METHOD_CREATE_INSTANCE = "createInstance";
public static final String METHOD_DESTROY_INSTANCE = "destroyInstance";
public static final String METHOD_CALL_JS = "callJS";
public static final String METHOD_SET_TIMEOUT = "setTimeoutCallback";
public static final String METHOD_REGISTER_MODULES = "registerModules";
public static final String METHOD_REGISTER_COMPONENTS = "registerComponents";
public static final String METHOD_FIRE_EVENT = "fireEvent";
public static final String METHOD_CALLBACK = "callback";
public static final String METHOD_REFRESH_INSTANCE = "refreshInstance";
private static final String UNDEFINED = "-1";

可以看到存在一个METHOD_CALL_JS。

而在Weex的众多模块中,有一个timer模块,可以做到native层主动调用js方法,用的就是METHOD_CALL_JS这个类型。但是这个方法不管是handler的messageType还是内部逻辑都已经写死了,如果要做到自定义,我们可以通过类似timer的逻辑去自己写一套机制。这个方案我这里不具体展开了,有兴趣的同学可以自行实现。

方案二:通过callback实现。

既然Weex没有提供对应的方案,我们可以通过一些手段去变相的完成这样的需求。

首先,我们可以在.we文件里去主动调用一个native层的方法:

created: function(){
this.prepareForNative();
},
prepareForNative: function(){
var native = require('@weex-module/native');
var log = require('@weex-module/wxlog');
native.nativeCall(function(data){
log.log(data.param);
});
}

在created回调中调用这个方法,该方法中存在一个回调,我们把需要native层主动调用的js层方法放在这个回调中,这里我简单的放了一个log函数。

接着,在native层接收到这个方法的时候,保存这个callback:

public class NativeCall extends WXModule {

private static final String TAG = "NativeCall";

private JSCallback callback;
private Map<String,Object> data = new HashMap<>();

private static NativeCall instance;

@WXModuleAnno
public void nativeCall(JSCallback callback){
instance = this;
this.callback = callback;
}

public static NativeCall getInstance() {
if(instance == null){
synchronized (NativeCall.class){
if(instance == null){
instance = new NativeCall();
}
}
}
return instance;
}

public void callJS(){
data.put("param","This is a param");
if(callback != null){
callback.invokeAndKeepAlive(data);
}
}
}

然后,我们就可以在需要调用js层方法的时候,调用NativeCall类的callJS方法,通过callback的形式返回。

在实际使用过程中,这样的方案肯定是会存在问题的,需要大家自己斟酌一下,不过这样的思路是可行的就对了。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址