转载

Weex进阶

从年前的几天开始负责了公司的Weex项目,前2个月一直在写服务端和前端后台的代码,这个月把这两块内容梳理的七七八八,开始优化客户端的逻辑,发现有几点可优化的地方,记录下来与大家分享。

性能监控

用Weex代替原有的hybrid开发,最根本的原因就是“性能”,在不丢失h5的灵活性的前提下,将性能不断的提升到native的水准,这是大部分应用选择接入Weex的理由。而性能这个东西,绝不是肉眼看看就可以了的,没有详细的数据统计与分析做支撑,一切都是空中楼阁。

在翻看Weex的源码之后,发现Weex对性能方面的监控和统计做的是比较到位的,首先我们看看如何获取Weex的一切性能数据。

/**
 * Interface for commit log info. This interface works as an adapter for various log library.
 */
public interface IWXUserTrackAdapter {

  String MODULE_NAME = "weex";

  //Performance
  String LOAD = "load";

  //Alarm
  String JS_FRAMEWORK = "jsFramework";
  String JS_DOWNLOAD = "jsDownload";
  String DOM_MODULE = "domModule";
  String JS_BRIDGE = "jsBridge";
  String STREAM_MODULE = "streamModule";
  String INVOKE_MODULE = "invokeModule";

  void commit(Context context, String eventId, String type, WXPerformance perf, Map<String, Serializable> params);
}

Weex中有一个adapter叫IWXUserTrackAdapter,看注释就明白是提供了数据输出的能力,方法的参数中有一个入参叫WXPerformance,下面我们来看看这个类里有什么。

public class WXPerformance {

  public static final String DEFAULT = "default";

  /**
   * Business unit, mandatory. If no business unit can be provided, set the field as default
   */
  public String bizType = "weex";

  /**
   * URL used for rendering view, optional
   */
  public String templateUrl;

  /**
   * Time spent for reading, time unit is ms.
   */
  public double localReadTime;

  /**
   * Name of the page
   */
  public String pageName = DEFAULT;

  /**
   * Size of JavaScript framework, the unit is KB
   */
  public double JSLibSize;

  /**
   * Time of initial JavaScript library
   */
  public long JSLibInitTime;

  /**
   * Size of JavaScript template
   */
  public double JSTemplateSize;

  public long templateLoadTime;

  /**
   * Time used for
   * {@link com.taobao.weex.bridge.WXBridgeManager#createInstance(String, String, Map, String)}
   */
  public long communicateTime;

  /**
   * Time spent when rendering first screen
   */
  public long screenRenderTime;

  /**
   * Call native Time spent when rendering first screen
   */
  public long callNativeTime;

  /**
   * Create Instance Time spent when rendering first screen
   */
  public long firstScreenJSFExecuteTime;

  /**
   * Call native Time spent when rendering first screen
   */
  public long batchTime;

  /**
   * Call native Time spent when rendering first screen
   */
  public long parseJsonTime;

  /**
   *  UpdateDomObj Time spent when rendering first screen
   */
  public long updateDomObjTime;

  /**
   *  ApplyUpdate Time spent when rendering first screen
   */
  public long applyUpdateTime;


  /**
   *  CssLayout Time spent when rendering first screen
   */
  public long cssLayoutTime;

  /**
   * Time spent, the unit is micro second
   */
  public double totalTime;
  
  .........
}

可以看到,这个类中有能满足我们性能数据统计的大部分数据了,下面我取几个最重要的:

/**
 * load bundle js time, unite ms
*/
public long networkTime;

/**
  * Time used for
  * {@link com.taobao.weex.bridge.WXBridgeManager#createInstance(String, String, Map, String)}
*/
public long communicateTime;
  
/**
 * Time spent, the unit is micro second
*/
public double totalTime;

networkTime表示下载bundle的时间,communicateTime表示创建instance的时间,而totalTime则表示从渲染开始到渲染完成的时间。所以如果我们要统计用户从打开一个Weex页面到Weex渲染完成的时间,我们就可以用这里的networkTime+totalTime。

然后就像我前面说的,这些数据只能满足“大部分”的要求。举个例子,比如一个app的购物车页面用了Weex,用networkTime+totalTime就真的能够表示用户“从打开页面到页面首屏展示在用户面前”的时间了吗?显然是不对的。networkTime+totalTime只能表示“Weex渲染完成的时间”,而从渲染完成到首屏的view展示在用户面前,还有一个时间是需要统计的:就是业务接口的rt时间。拿购物车为例,在Weex渲染完成后,还是会存在一定时间的白屏,因为这个时候和购物车相关的业务接口并没有请求完成,还没有数据,而vue是基于data-binding的,所以这个时候的view是无法用数据进行填充的。

这里有一点需要注意的是,很多开发者会选择在OnRenderSuccess中调用dialog的dismiss方法去将加载框隐藏,这对用户来说是不友好的,因为有接口rt的存在,所以在dismiss只有还会有一定时间的白屏。正确的做法应该是自定义一个Loading的module,让前端的开发在接口成功/失败的回调中手动的调用module的dismiss方法。

回到前面的性能统计问题上,这个rt的时间不属于Weex的性能,所以Weex没有统计也是正常的,而对于业务方来说,想要还原用户真实的数据,这个时间还是有必要记录的,我们有2种方法进行数据的计算:1.纯粹计算从“Weex渲染完成”到“接口回调”的时间,也就是在onRenderSuccess的时候记录startTime,在LoadingModule的dismiss中计算时间差;2.计算整体的时间,也就是在对应页面的create生命周期中记录startTime,在LoadingModule的dismiss中计算时间差。

说了这么多,这些性能数据如果获取呢?

@Override
public void onActivityPause() {
  onViewDisappear();
  if(!isCommit){
    Set<String> componentTypes= WXComponentFactory.getComponentTypesByInstanceId(getInstanceId());
    if(componentTypes!=null && componentTypes.contains(WXBasicComponentType.SCROLLER)){
      mWXPerformance.useScroller=1;
    }
    mWXPerformance.maxDeepViewLayer=getMaxDeepLayer();
 mWXPerformance.wxDims = mwxDims;
 mWXPerformance.measureTimes = measureTimes;
    if (mUserTrackAdapter != null) {
      mUserTrackAdapter.commit(mContext, null, IWXUserTrackAdapter.LOAD, mWXPerformance, getUserTrackParams());
    }
    isCommit=true;
  }
  // module listen Activity onActivityPause
  WXModuleManager.onActivityPause(getInstanceId());
  if(mRootComp != null) {
    mRootComp.onActivityPause();
  }else{
    WXLogUtils.w("Warning :Component tree has not build completely,onActivityPause can not be call!");
  }

  WXLogUtils.i("Application onActivityPause()");
  if (!mCurrentGround) {
    WXLogUtils.i("Application to be in the backround");
    Intent intent = new Intent(WXGlobalEventReceiver.EVENT_ACTION);
    intent.putExtra(WXGlobalEventReceiver.EVENT_NAME, Constants.Event.PAUSE_EVENT);
    intent.putExtra(WXGlobalEventReceiver.EVENT_WX_INSTANCEID, getInstanceId());
    mContext.sendBroadcast(intent);
    this.mCurrentGround = true;
  }
}

我们来看WXSDKInstance的onActivityPause方法,其中有一段:

if(!isCommit){
   Set<String> componentTypes= WXComponentFactory.getComponentTypesByInstanceId(getInstanceId());
   if(componentTypes!=null && componentTypes.contains(WXBasicComponentType.SCROLLER)){
     mWXPerformance.useScroller=1;
   }
   mWXPerformance.maxDeepViewLayer=getMaxDeepLayer();
mWXPerformance.wxDims = mwxDims;
mWXPerformance.measureTimes = measureTimes;
   if (mUserTrackAdapter != null) {
     mUserTrackAdapter.commit(mContext, null, IWXUserTrackAdapter.LOAD, mWXPerformance, getUserTrackParams());
   }
   isCommit=true;
 }

在Activity不在前台的时候,会调用对应adapter的commit方法上报,我们所需要做的就是实现这个adapter并通过对应的方法注入到instance中。

WXSDKEngine.initialize(app, new InitConfig.Builder()
        .setImgAdapter(....)
        .setUtAdapter(....)
        .setJSExceptionAdapter(....)
        .build());

在初始化Weex的时候调用setUtAdapter就可以了。

除了上面的数据之外,Weex还提供了一个更加直观的GUI查看的方法。

首先我们需要依赖inspect:

debugCompile ('com.taobao.android:weex_inspector:0.13.4')
debugCompile ('com.taobao.android.weex_inspection:protocol:1.1.4.1')

在devTools包下有一个PerformanceActivity,可以很直观的查看一些性能,而这个Activity是如何打开的呢?

private void enableMonitor(final String instanceId) {
  final WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(instanceId);
  if (instance != null) {
    RenderContainer container = (RenderContainer) instance.getContainerView();
    TextView textView = new TextView(instance.getUIContext());
    textView.setText("Weex MNT:" + instanceId);
    textView.setBackgroundColor(Color.parseColor("#AA1E90FF"));
    textView.setTextColor(Color.WHITE);
    textView.setPadding(10, 10, 10, 10);
    FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    lp.gravity = Gravity.RIGHT | Gravity.CENTER;
    textView.setLayoutParams(lp);

    container.addView(textView);
    textView.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        PerformanceActivity.start(instance.getUIContext(), Integer.parseInt(instanceId));
      }
    });
  }
}

在WXTracingAdapter中有一个enableMonitor方法,具体就是通过传入instanceId去获取该instance下面的数据并传入到Activity中,不过在最新的0.18.0版本的sdk中这个方法没有地方调用,不过没关系,我们已经知道如何展示了,可以在自己对应的页面里照葫芦画瓢,通过instanceId去打开Activity就好。(记得在正式上面的时候屏蔽这个入口,所以我上面是debugCompile)

还有一个问题我们需要考虑,就是数据筛选的问题,有时候我们可能只需要对某一个特定的页面或者是url进行数据的筛选,而上面的数据统计里,显然没有和页面还有url相关的内容,我们如何在上报的时候加上这两条数据呢?还是看WXSDKInstance的源码:

public void addUserTrackParameter(String key,Serializable value){
  if(this.mUserTrackParams == null){
    this.mUserTrackParams = new ConcurrentHashMap<>();
  }
  mUserTrackParams.put(key,value);
}

可以看到,我们可以往一个叫mUserTrackParams的map里添加自定义的数据,而这个map有什么用呢?

mUserTrackAdapter.commit(mContext, null, IWXUserTrackAdapter.LOAD, mWXPerformance, getUserTrackParams());

在之前的数据上报中,会将这个map作为参数放到commit方法中。所以我们可以调用WXSDKInstance的addUserTrackParameter去添加自定义的数据,包括上面说的那个接口的rt时长。

最后说一句,大家有兴趣可以看看WXTracingAdapter这个类,你就会知道PerformanceActivity里是如何统计数据的了~

异常监控

说完了性能监控,我们再来说说Weex的异常监控。我们知道,Weex中是可能会发生异常的,有可能是Weex sdk本身的异常,也有可能是使用不当造成的异常,还有可能是网络的抖动造成的请求失败,超时等等。对于这些异常我们都需要有一个监控的方法,并在合适的时候进行降级。这里我们主要讨论Weex本身的异常,毕竟一些业务上的异常如请求超时不是Weex该处理的。

Weex的异常分为2种,渲染异常和js执行异常。有过Weex开发经验的同学都知道,Weex中提供了一个onException的回调,但是如果仅仅使用这一个回调是远远不够的!

下面我们先看onException的触发时机:

public void onRenderError(final String errCode, final String msg) {
  if (mRenderListener != null && mContext != null) {
    runOnUiThread(new Runnable() {

      @Override
      public void run() {
        if (mRenderListener != null && mContext != null) {
          mRenderListener.onException(WXSDKInstance.this, errCode, msg);
        }
      }
    });
  }
}

public void onJSException(final String errCode, final String function, final String exception) {
  if (mRenderListener != null && mContext != null) {
    runOnUiThread(new Runnable() {

      @Override
      public void run() {
        if (mRenderListener != null && mContext != null) {
          StringBuilder builder = new StringBuilder();
          builder.append(function);
          builder.append(exception);
          mRenderListener.onException(WXSDKInstance.this, errCode, builder.toString());
        }
      }
    });
  }
}

在WXSDKInstance中有onRenderError和onJSException,最终都会调用onException方法,乍一看很完美,既有渲染异常,也有js异常,其实不然,在阅读Weex源码的时候我发现了一些猫腻:

有一个类叫WXExceptionUtil,其中有一个方法叫commitCriticalExceptionRT:

public static void commitCriticalExceptionRT(@Nullable final String instanceId,
                                 @Nullable final String errCode,
                                 @Nullable final String function,
                                 @Nullable final String exception,
                                 @Nullable final Map<String,String> extParams ) {
	IWXJSExceptionAdapter adapter = WXSDKManager.getInstance().getIWXJSExceptionAdapter();
    ....
    if (adapter != null) {
	  exceptionCommit = new WXJSExceptionInfo(instanceIdCommit, bundleUrlCommit, errCode, function, exceptionMsgCommit, commitMap);
	  adapter.onJSException(exceptionCommit);
	  WXLogUtils.e(exceptionCommit.toString());
	}
}

在这个方法中会获取IWXJSExceptionAdapter并通过这个adapter上报异常,并没有走我们熟悉的onException。

那么这个方法在什么时候调用呢?不看不知道一看吓一跳,很多地方都用到了这个方法,我们来看其中的一处:

public Object callNativeModule(String instanceId, String module, String method, JSONArray arguments, Object options) {

  if (WXEnvironment.isOpenDebugLog()) {
    mLodBuilder.append("[WXBridgeManager] callNativeModule >>>> instanceId:").append(instanceId)
        .append(", module:").append(module).append(", method:").append(method).append(", arguments:").append(arguments);
    WXLogUtils.d(mLodBuilder.substring(0));
    mLodBuilder.setLength(0);
  }

  try {
    if (WXDomModule.WXDOM.equals(module)) {
      WXDomModule dom = getDomModule(instanceId);
      return dom.callDomMethod(method, arguments);
    } else {
      return callModuleMethod(instanceId, module,
          method, arguments);
    }
  } catch (Exception e) {
 String err = "[WXBridgeManager] callNative exception: " + WXLogUtils.getStackTrace(e);
    WXLogUtils.e(err);
 WXExceptionUtils.commitCriticalExceptionRT(instanceId,
     WXErrorCode.WX_KEY_EXCEPTION_INVOKE.getErrorCode(), "callNativeModule",
     err, null);
  }

  return null;
}

WXBridgeManager的callNativeModule方法,js调用native的module会走到这里,在catch中就调用了commitCriticalExceptionRT,可以看到的是,这种异常上报在Weex中处处可见,如果我们遗漏了这一处,很多异常我们就没办法捕获了。

所以我们要做的就是在Weex初始化的时候初始化这个adapter:

WXSDKEngine.initialize(app, new InitConfig.Builder()
        .setImgAdapter(...)
        .setUtAdapter(...)
        .setJSExceptionAdapter(...)
        .build());

调用setJSExceptionAdapter就可以,自己去实现IWXJSExceptionAdapter。

最后,我们在onException和IWXJSExceptionAdapter的onJSException里去统计我们的异常并处理降级逻辑。

长列表性能优化

我们知道,在Weex中长列表的显示我们一般会采用list标签配合cell标签使用,但是不得不说一句,list标签在性能上实在是太差了,官方提供的文档说可以在cell中增加一个scope属性用以cell的复用,也就是说相同scope的cell会复用,这确实不假,但是复用的调用时“cell必须要任何时刻都保证相同”,也就是说如果你在cell中使用了v-if这样的条件判断,复用就会让整个列表错乱。我们从源码角度看看这是为什么:

@Override
public void onBindViewHolder(final ListBaseViewHolder holder, int position) {
  if (holder == null) return;
  holder.setComponentUsing(true);
  WXComponent component = getChild(position);
  if (component == null
      || (component instanceof WXRefresh)
      || (component instanceof WXLoading)
      || (component.getDomObject() != null && component.getDomObject().isFixed())
      ) {
    if (WXEnvironment.isApkDebugable()) {
      WXLogUtils.d(TAG, "Bind WXRefresh & WXLoading " + holder);
    }
    if(component instanceof  WXBaseRefresh
            && holder.getView() != null
            && component.getDomObject() != null
            && (component.getDomObject().getAttrs().get("holderBackground") != null)){
       Object holderBackground = component.getDomObject().getAttrs().get("holderBackground");
      int color = WXResourceUtils.getColor(holderBackground.toString(), Color.WHITE);
       holder.getView().setBackgroundColor(color);
       holder.getView().setVisibility(View.VISIBLE);
       holder.getView().postInvalidate();
    }
    return;
}

我们之前看onBindViewHolder方法,如果你的holder是复用的,并且其中使用了if的条件判断,那就出大事了,我们可以看到onBindViewHolder没有任何数据的判断,回想我们自己使用RecyclerView的时候,在adapter是不是会写很多if-else去做数据的判断以保证复用不会错乱呢?

针对这个问题,官方在最新的0.18.0版本的sdk中进行了优化。

registerComponent(WXRecyclerTemplateList.class, false,WXBasicComponentType.RECYCLE_LIST);

我们可以看到多了一个WXRecyclerTemplateList,对于的标签是recycle-list。

@Override
public void onBindViewHolder(final TemplateViewHolder templateViewHolder, int position) {
    if(templateViewHolder == null){
        return;
    }
    WXCell component = templateViewHolder.getTemplate();
    if(component == null){
        return;
    }
    long start = System.currentTimeMillis();
    templateViewHolder.setHolderPosition(position);
    Object data = cellDataManager.listData.get(position);
    CellRenderState cellRenderState = cellDataManager.getRenderState(position);
    if(component.getRenderData() == data && (cellRenderState == null || !cellRenderState.isDirty())){
        if(WXEnvironment.isOpenDebugLog() && ENABLE_TRACE_LOG){
            WXLogUtils.d(TAG,  position + " position "+ getTemplateKey(position) + " onBindViewHolder none data update ");
        }
        return;  //none update just return
    }else{
        List<WXComponent> updates = doRenderTemplate(component, position);
        Statements.doInitCompontent(updates);
        component.setRenderData(data);
        Layouts.doLayoutAsync(templateViewHolder, true);
        if(WXEnvironment.isOpenDebugLog() && ENABLE_TRACE_LOG){
            WXLogUtils.d(TAG,  position + " position "+ getTemplateKey(position) + " onBindViewHolder used " + (System.currentTimeMillis() - start));
        }
    }
}

在这个组件的onBindViewHolder中就厉害了,判断了当前position的data和holder的data,如果不一致就会重新绑定一遍数据和dom。

recycle-list的具体使用方法大家可以去看 官网的介绍 。

bundle缓存

为了节省用户流程并提升Weex的秒开率,bundle的缓存是势在必行的,而实现一个有效的缓存逻辑在Weex中也很简单。

首先我们可以继承WXSDKInstance并重写renderByUrl方法,在该方法中获取到bundle的url,这个时候我们就可以根据这个url去获取缓存了,如果有缓存就加载缓存,如果没有缓存则调用WXSDKInstance本身的renderByUrl。而renderByUrl最终会去下载该bundle文件,并通过render方法渲染,我们要做的就是重写render方法,其中会有一个template入参,就是bundle文件,将其进行保存就ok。

这里我们需要注意的是:1.保存的文件要和url进行关联,可以是md5或其他方法,保证缓存和url的可关联性;2.在加载本地缓存的时候要做md5校验,防止恶意篡改;3.在发生异常的时候,可对具体的场景进行缓存的删除;4.由于缓存和url是关联的,所以该url必须具体独立性,并且在每一次的线上发布之后需要带有版本号或者时间戳以保证缓存不会误用。

断点调试

这次所讨论的断点是针对于前端代码的断点,我们可以使用Weex官方的dev-tool去进行debug, 使用方法的传送门在这 。继承之后,我们可以在chrome里查看对应bundle的源码并进行断点调试,以后Weex页面出现问题再也不用求前端的开发一起联调排查了,自己对前端代码和客户端代码进行断点,so easy~

不过如果你的项目中okhttp的版本比较高,需要自定义一个WebSocketClient才行,具体的实现我已经写好并放在 我的Github上了 。

写在最后

最后我想说的是,Weex现在越来越多的被运用到了各大项目中,很多同学都在摩拳擦掌的准备使用它,但是在使用的过程中,有以下几点必须要注意:

  1. 一切用数据说话。Weex带来的改变绝不是用肉眼看看就行了的,一切都要用数据做支撑,改造之前和改造之后的数据对比才是使用Weex的原因。
  2. 准备接受挑战。Weex和RN这种动态化项目的出现是非常符合“大前端”这样的概念的,也许你是客户端开发,但是如果你使用了Weex,请让自己具备一定的前端开发能力,就像我在最后一个单元“断点调试”里说的,遇到问题可以自己debug,甚至在日常开发中可以自己开发一些前端组件,模块都是必须的。
  3. 全局考虑问题。Weex不仅仅是一个客户端sdk这么简单,背后的构建,发布,降级等逻辑都是需要考虑的,如何做到构建友好,一键发布,快速灰度,安全降级等等,这些都是需要仔细斟酌的,一个完整的Weex产品绝对不是做好一个sdk这么简单。
原文  http://zjutkz.net/2018/04/17/Weex进阶/
正文到此结束
Loading...