体验了一下weex,发现weex语法还挺简单,上手容易,发现自己没什么前端知识,也能极易上手,出于强烈好奇和业务预研的需要,分析了其Android端的Weex Sdk一些源码.
先从WXSDKManager入手后,画出其结构图如图:
IWXUserTrackAdapter:用来处理日志信息接口,常常拿来做一些用户埋点统计.
IWXImgLoaderAdapter:用来处理View加载图片接口,可以实现其控制如何加载远程和本地图片.
IWXHttpAdapter:用来处理网络请求的接口,常常处理请求一系列过程,默认实现DefaultWXHttpAdapter.
IActivityNavBarSetter:用来处理页面跳转接口,可以实现其接口来控制页面的跳转.
IWXStorageAdapter:用来处理存储接口,例如SQLite存储,默认实现DefaultWXStorage.
IWXDebugAdapter:用来处理调试接口,通常实现其接口来在Chrom上做一些页面的调试.
WXDomManager:专门用来管理Dom节点一些操作,如创建节点对应对象,但真正操作是委托给其他的对象,其关联如图:
WXBridgeManager:用来处理Js和Android端的通信,例如Js端调用Android端Native层的方法.其关联如图:
WXRenderManager:用来处理一些渲染操作,例如通过WXRenderStatement将Js层标签转到native层的View组件,其关联如图:
从上面看知道,一个weex页面在Android端渲染,分了三大模块,Dom节点操作管理模块,跨端通信模块,渲染模块,其三个端具体关联分别如下.
节点操作模块:
跨端通信模块:
渲染模块:
在分析weex如何在android端绘制流程之前,首先先弄清楚一个weex页面在native层的生命周期是如何?
那么在没有WebView的情况下,Native层又如何去解析Js代码呢?梳理了一下其源码,发现weex主要通过下图方式,建起js和java之间的通信桥梁:
从图可知,Js如果要与java通信,那么可以通过google v8引擎先与c++通信,然后在通过jni机制来实现与java的通信,从解决了Js页面与Native的通信了.同理,java与Js通信也一样.
接下来就分析了其weex之android端的绘制流程了,但限于前端和v8引擎知识有限,所以还不能很好的深入到里面,只能肤浅概况其绘制流程:
weex能很灵活的支持组件扩展,在weex android sdk里,定义一系列weex组件,并且映射到native对应View组件.这里大概概况一下组件注册流程:
那weex组件设置属性又如何映射到native层,weex组件转换native组件步骤如图(非根节点,js调用过程类似上面时序图):
在这里weex的module自扩展注册和weex的组件注册流程差不多,也是通过@WXModuleAnno注解标记native层方法供js调用,其调用流程如下:
说了那么多,还是来实践一个浮窗的weex控件吧,(浮窗控件还有很多没完成,完成会尝试同步到weex开源项目上)这里我直接贴核心代码了.
原生部分代码:
//浮窗接口 public interface FloatWindowInterface { void init(WindowManager windowManager,View windowView); void show(); void hide(); } //浮窗native的View容器 public class WXFloatFrameLayout extends WXFrameLayout { private FloatWindowInterface mFloatWindow; public WXFloatFrameLayout(Context context, FloatWindowInterface floatWindow) { super(context); this.mFloatWindow = floatWindow; } public FloatWindowInterface getFloatWindow() { return mFloatWindow; } public void setFloatWindow(FloatWindowInterface floatWindow) { mFloatWindow = floatWindow; } public boolean isIntercept() { return isIntercept; } public void setIntercept(boolean intercept) { isIntercept = intercept; } private boolean isIntercept=true; public WXFloatFrameLayout(Context context) { super(context); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(isIntercept){ return true; } return super.onInterceptTouchEvent(ev); } } //浮窗的weex组件 @Component(lazyload = false) public class WXWindowComponent extends WXDiv implements WXSDKInstance.OnInstanceVisibleListener,View.OnTouchListener,FloatWindowInterface { private WXSDKInstance mViewInstance; private String src; private boolean mIsVisible=true; private String originUrl; private FloatViewRenderListener mListener; private WindowManager mWm; private View mWindowView; private WindowManager.LayoutParams mLayoutParams; private int mGravity=Gravity.CENTER; private int mFlag=WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; private float mTouchX; private float mTouchY; private int mLeft=0; private int mTop=0; private int mDeviceWidth; private boolean mDisableFloat=false; public WXWindowComponent(WXSDKInstance instance, WXDomObject node, WXVContainer parent, boolean lazy) { super(instance, node, parent, lazy); mListener=new FloatViewRenderListener(this); } private void updateViewPosition(){ this.mLayoutParams.x=(int) (mTouchX-mLeft); this.mLayoutParams.y=(int) (mTouchY-mTop); mWm.updateViewLayout(mWindowView,this.mLayoutParams); } @Override public boolean onTouch(View v, MotionEvent event) { if(mDisableFloat){ return false; } mTouchX = event.getRawX(); mTouchY = event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: updateViewPosition(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if(mTouchX>=this.mDeviceWidth>>1){ mTouchX=this.mDeviceWidth; }else { mTouchX=0; } updateViewPosition(); break; } return true; } public void init(WindowManager windowManager,View windowView) { this.mDeviceWidth= WXViewUtils.getScreenWidth(getContext()); this.mLayoutParams=new WindowManager.LayoutParams(); this.mLayoutParams.height=WindowManager.LayoutParams.WRAP_CONTENT; this.mLayoutParams.width=WindowManager.LayoutParams.WRAP_CONTENT; this.mLayoutParams.format= PixelFormat.TRANSLUCENT; this.mLayoutParams.type=WindowManager.LayoutParams.TYPE_APPLICATION; this.mLayoutParams.gravity=this.mGravity; this.mLayoutParams.flags =this.mFlag; this.mWm= windowManager; this.mWindowView=windowView; ((WXFloatFrameLayout)getHostView()).setIntercept(true); this.mWindowView.setOnTouchListener(this); } void loadInstance(){ mViewInstance=createInstance(); } @Override public void onAppear() { if(mIsVisible&&mViewInstance!=null){ WXComponent comp=mViewInstance.getRootCom(); show(); if(comp!=null){ mViewInstance.fireEvent(comp.getRef(), Constants.Event.VIEWAPPEAR,null, null); } } } @Override public void onDisappear() { if(mIsVisible && mViewInstance != null){ WXComponent comp = mViewInstance.getRootCom(); hide(); if(comp != null) mViewInstance.fireEvent(comp.getRef(), Constants.Event.VIEWDISAPPEAR,null, null); } } public void renderNewURL(String url){ this.src=url; loadInstance(); } public ViewGroup getViewContainer(){ return getHostView(); } private WXSDKInstance createInstance() { WXSDKInstance sdkInstance =new WXSDKInstance(getContext()); getInstance().addOnInstanceVisibleListener(this); sdkInstance.registerRenderListener(mListener); final String url=src; if(TextUtils.isEmpty(url)){ return sdkInstance; } ViewGroup.LayoutParams layoutParams = getHostView().getLayoutParams(); sdkInstance.renderByUrl(WXPerformance.DEFAULT, url, null, null, layoutParams.width, layoutParams.height, WXRenderStrategy.APPEND_ASYNC); return sdkInstance; } static class FloatViewRenderListener implements IWXRenderListener{ WXWindowComponent mComponent; public FloatViewRenderListener(WXWindowComponent wxWindowComponent){ this.mComponent=wxWindowComponent; } @Override public void onViewCreated(WXSDKInstance instance, View view) { FrameLayout hostView=this.mComponent.getHostView(); view.invalidate(); hostView.removeAllViews(); hostView.addView(view); } @Override public void onRenderSuccess(WXSDKInstance instance, int width, int height) { } @Override public void onRefreshSuccess(WXSDKInstance instance, int width, int height) { } @Override public void onException(WXSDKInstance instance, String errCode, String msg) { } } @Override protected boolean setProperty(String key, Object param) { switch (key) { case Constants.Name.SRC: String src = WXUtils.getString(param,null); if (src != null) setSrc(src); return true; } return super.setProperty(key, param); } @WXComponentProp(name = Constants.Name.SRC) public void setSrc(String src) { originUrl=this.src; this.src = src; if (mViewInstance != null) { mViewInstance.destroy(); mViewInstance = null; } loadInstance(); } @WXComponentProp(name = com.scau.beyondboy.weexdemo.weex.common.Constants.Name.GRAVITY) public void setGravity(int gravity){ this.mGravity=gravity; this.mLayoutParams.gravity=this.mGravity; show(); } @WXComponentProp(name = com.scau.beyondboy.weexdemo.weex.common.Constants.Name.DISPlAY_WINDOW) public void displayWindow(boolean displayWindow){ if(displayWindow){ show(); }else { hide(); } } @WXComponentProp(name = com.scau.beyondboy.weexdemo.weex.common.Constants.Name.DISABLE_FLOAT) public void disableFloat(boolean disableFloat){ this.mDisableFloat=disableFloat; if(this.mDisableFloat){ ((WXFloatFrameLayout)getHostView()).setIntercept(false); }else{ ((WXFloatFrameLayout)getHostView()).setIntercept(true); } } @WXComponentProp(name = com.scau.beyondboy.weexdemo.weex.common.Constants.Name.FLAG) public void setFlag(int flag){ this.mFlag=flag; } public void show(){ if(this.mWm==null){ return; } if(this.mWindowView.getParent()!=null){ if(this.mWindowView.getParent()!=null){ this.mWm.removeView(mWindowView); } } this.mWm.addView(this.mWindowView,this.mLayoutParams); this.mWindowView.post(new Runnable() { @Override public void run() { int[] location =new int[2]; mWindowView.getLocationOnScreen(location); mLeft=location[0]+(mWindowView.getWidth()>>1); mTop=location[1]+(mWindowView.getHeight()>>1); } }); } public void hide(){ if(this.mWm==null){ return; } if(this.mWindowView!=null&&this.mWindowView.getParent()!=null){ this.mWm.removeView(this.mWindowView); this.mLayoutParams.x=0; this.mLayoutParams.y=0; } } public String getSrc() { return src; } public String getOriginUrl() { return originUrl; } public void setOriginUrl(String originUrl) { this.originUrl = originUrl; } @Override public void destroy() { super.destroy(); hide(); if(mViewInstance!=null){ mViewInstance.destroy(); mViewInstance=null; } src=null; } @Override protected WXFloatFrameLayout initComponentHostView(@NonNull Context context) { return new WXFloatFrameLayout(context,this); } }
这里别忘了注册一下组件,这里我用这行代码注册WXSDKEngine.registerComponent("float", WXWindowComponent.class,true);.
we文件代码:
通过写bash脚本去编译一下,这些we文件会通过weex工具去转换js文件存到我的android项目assets目录下,运行的结果如图(红点是受录制影响):
weex渲染一个页面有几个性能指标要测试一下,这部分网上也有一些数据,我这里也将测试几个性能指标:内存消耗,时间消耗,GPU渲染性能测试,文件大小尺寸.
为了避免因GC带来影响,这里测试条件是为多次触发GC后,内存恢复没还加载we或原生布局页面时的水平,同时等稳定后时候再点击按钮重新打开页面,统计一次相关数据,测试是跟加载原生布局页面做对比,加载布局为hello.we页面,其代码:
Hello World.
为了尽量接近,View视图树也尽量一样,先看看hello.we界面视图树:
这里原生布局代码没有引用weex的组件,因此会形成一点不同,原生布局代码:
<framelayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android"> <framelayout android:layout_height="wrap_content" android:layout_width="wrap_content"></framelayout> </framelayout>
测试代码核心:
//AbstractWeexActvity onCreate createWeexInstance方法已注释掉 public class WXPageActivity extends SimpleWeexActivity { private static final String DEFAULT_IP = "localhost"; private static String CURRENT_IP= DEFAULT_IP; // your_current_IP private static final String WEEX_INDEX_URL = "http://"+CURRENT_IP+":12580/examples/build/index.js"; private boolean isloadJs=true; private Button mButton; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mButton=new Button(this); FrameLayout.LayoutParams layoutParams=new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,FrameLayout.LayoutParams.WRAP_CONTENT); layoutParams.gravity= Gravity.CENTER; mButton.setLayoutParams(layoutParams); mButton.setText("测试内存JS"); mButton.setTextColor(Color.BLACK); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getContainer().removeAllViews(); if(isloadJs){ //每次都会创建we instance实例 renderPage(WXFileUtils.loadAsset("hello.js",WXPageActivity.this),"file://assets/hello.js"); mButton.setText("测试内存xml"); }else{ View view= LayoutInflater.from(WXPageActivity.this).inflate(R.layout.weex_hello,null); getContainer().addView(view); mButton.setText("测试内存JS"); } isloadJs=!isloadJs; } }); ((FrameLayout)findViewById(android.R.id.content)).addView(mButton); } //恢复原来页面 @Override public void onBackPressed() { //如果是加载we页面会销毁we文件Instance实例,否则什么都不做 super.onBackPressed(); getContainer().removeAllViews(); getContainer().addView(mButton); } @Override public void onDestroy() { super.onDestroy(); finish(); } @Override public void onResume() { super.onResume(); } }
其测试结果如下:
虽然官方对we在native层渲染有时间统计,但为了一致,我是通过统计addOnGlobalLayoutListener()其结束时间(统计的结束时间会比onRenderSuccess时间长一一些),布局还是hello.we和weex_hellow.xml两个文件做对比.基于上面代码填加如下时间测试核心代码:
//来自AbstarctWeexActivity的方法,并在createInstance方法获取时间起始start @Override public void onViewCreated(WXSDKInstance wxsdkInstance, View view) { if(view==null&&!(view instanceof ViewGroup)){ return; } ViewGroup viewGroup=(ViewGroup)view; if(viewGroup.getChildCount()<=0){ return; } final View rootView=viewGroup.getChildAt(0); if(rootView instanceof WXFloatFrameLayout){ viewGroup.removeAllViews(); viewGroup.removeView(rootView); viewGroup=null; final FloatWindowInterface floatWindowInterface=((WXFloatFrameLayout)rootView).getFloatWindow(); floatWindowInterface.init(this.getWindowManager(),rootView); floatWindowInterface.show(); }else if(mContainer != null){ mContainer.removeAllViews(); mContainer.addView(view); } //添加时键测试代码 getContainer().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onGlobalLayout() { getContainer().getViewTreeObserver().removeOnGlobalLayoutListener(this); runOnUiThread(new Runnable() { @Override public void run() { if(isRender){ isRender=false; end=System.currentTimeMillis(); Log.i("Time","WEEXTime:"+(end-start)); } } }); } }); } //来自WXPageActivity的方法 @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mButton=new Button(this); FrameLayout.LayoutParams layoutParams=new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,FrameLayout.LayoutParams.WRAP_CONTENT); layoutParams.gravity= Gravity.CENTER; mButton.setLayoutParams(layoutParams); mButton.setText("测试内存JS"); mButton.setTextColor(Color.BLACK); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getContainer().removeAllViews(); isRender=true; if(isloadJs){ renderPage(WXFileUtils.loadAsset("hello.js",WXPageActivity.this),"file://assets/hello.js"); mButton.setText("测试内存xml"); }else{ start=System.currentTimeMillis(); View view= LayoutInflater.from(WXPageActivity.this).inflate(R.layout.weex_hello,null); getContainer().addView(view); getContainer().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onGlobalLayout() { getContainer().getViewTreeObserver().removeOnGlobalLayoutListener(this); runOnUiThread(new Runnable() { @Override public void run() { if(isRender){ isRender=false; end=System.currentTimeMillis(); Log.i("Time","XMLTime:"+(end-start)); } } }); } }); mButton.setText("测试内存JS"); } isloadJs=!isloadJs; } }); ((FrameLayout)findViewById(android.R.id.content)).addView(mButton); }
其时间对比如图所示:
GPU渲染性能测试主要是通过adb shell dumpsys gfxinfo命令获取数据,然后导入excel表格来生成图表,GPU渲染we文件如图(时间为ms):
第二次测试:
第三次测试:
GPU渲染原生布局文件如图:
第二次测试:
第三次测试:
最后一个指标就是文件大小,对比一下weex_hello.xml和hello.js文件尺寸:
文件 | weex_hello.xml | hello.js |
---|---|---|
大小(字节) | 600 | 2020 |
以上测试还存在一些局限,如布局文件单一,机型单一等情况,但从上面测试情况来看,weex相对于native的原生加载页面还是存一些性能瓶颈,如内存消耗,时间消耗,通常内存消耗和时间消耗是互相关联,同时也关联了CPU的性能.文件尺寸.对于优化内存消耗部分,可以采用一些复用对象方式或对象池方式等手段来减少内存开销,如触屏事件的Target.对于时间消耗,可以采用缓存策略来去管理一些weex实例,如缓存常用对象等手段,对于文件尺寸来说,可以采用js代码压缩,甚至通过技巧去共享依赖模块,而不是每次转换js文件,就要导入依赖模块等方式来减少文件尺寸.
由于对于前端知识缺少了解,有不足之处望多多指正.不过后面时间还是会继续写一些Weex之Android端的细节地方.