LiteApp源码分析

LiteApp 是爱奇艺开源的一个高性能跨平台的小程序框架,下面我从源码的角度分析下LiteApp的架构和原理

框架描述

下面是官方的描述的翻译~

LiteApp致力于使开发人员能够使用现代Web开发技术,通过单个代码库在Android和iOS上构建应用程序。 更具体地说,您可以使用javascript和现代前端框架Vue.js通过使用LiteApp开发移动应用程序,生产力和性能可以共存,您构建的应用程序将在网络上运行,性能接近本机。 我们通过将渲染引擎与语法层分离来实现这一点,请参阅下面的更多细节。

这是官方的架构图。可能看的并不是很明白。我简单的介绍下这个框架的工作流程吧。

  • 基于VUE和liteapp-cli开发功能代码,并打包成bundle文件
  • WebView提前初始化,并加载qy.webview.js框架和打包后的bundle里css文件
  • bundle.js和qy.thread.js文件运行在独立的JS引擎(jsc), 生成virtual-dom并注入到WebView中,到此界面就显示出来了
  • qy.webview.js 和 qy.thread.js 是基于vue.js 框架修改而来,去掉了dom和bom相关操作
  • 这种小程序框架的优势是核心的js代码和webView渲染可以异步执行,方便native和web通信

启动过程

我们从官方demo LiteAppListActivity类开始介绍,这个界面显示了本地已经存在的小程序列表,点击列表就打开了小程序。打开过程传递了什么参数呢?

// onItemClick
Bundle initData = new Bundle();  
initData.putInt("tvid",841976700);  
LiteAppHelper.startLiteAppPackage(LiteAppListActivity.this, item.getId(),true, initData,false);

// startLiteAppPackage
public static void startLiteAppPackage(Activity activity, String liteAppID, boolean needUpdate, Bundle pageDataMap, boolean newTask) {  
        //check lite app package exist before enter
        Intent intent = new Intent(activity, LiteAppFragmentActivity.class);
        intent.putExtra(MINI_PROGRAM_ID, liteAppID );
        intent.putExtra(MINI_PROGRAM_NEED_UPDATE, needUpdate);
        intent.putExtra(MINI_PROGRAM_PAGE_DATA_MAP, pageDataMap);

        if(newTask) {
            intent.putExtra(MINI_PROGRAM_NEW_TASK, true);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        activity.startActivityForResult(intent,1);
    }

其实主要就是传递了liteAppID, 用来确认小程序路径和唯一ID。执行完成就跳到了LiteAppFragmentActivity页面,这是小程序主页面。我们先来看下这个页面的继承关系。

  • LiteAppFragmentActivity extends LiteAppBaseActivity
  • LiteAppBaseActivity extends Activity

LiteAppBaseActivity是一个抽象类,包含一个抽象方法routerGoPage, 主要是负责标题和菜单栏一些初始化,和小程序关系不大,主要我们还是来看LiteAppFragmentActivity实现。

//load
Intent intent = getIntent();  
liteAppID = intent.getStringExtra(MINI_PROGRAM_ID);  
needUpdate = intent.getBooleanExtra(  
                MINI_PROGRAM_NEED_UPDATE,true);
initPageData = intent.getBundleExtra(MINI_PROGRAM_PAGE_DATA_MAP);  
liteAppMainView = findViewById(R.id.lite_app_main_view);  
try {  
     validateAppIDWithCache();
} catch (Exception e){
     coldStart();
}

接收传递过来的liteAppID参数并执行validateAppIDWithCache方法,我们看下具体怎么实现的。(只贴出关键代码)

...
// 获取小程序信息,版本号、页面信息、Tab信息等
liteAppDetail = LiteAppPackageProvider.getClient().prepareLiteAppIfCache(liteAppID);  
// 获取当前页面对应的bundle.js内容
String pageString = LiteAppPackageProvider.getClient().getLiteAppPageBundle(  
                            liteAppID, liteAppDetail.getPageByName(indexPage).getPath());
...
// 获取当前页面对应css的本地路径
String pageCssPath = LiteAppPackageProvider.getClient()  
                            .getLiteAppPageBundleCssPath(liteAppID,  liteAppDetail.getPageByName(indexPage).getPath());
// 显示加载内容                            
try {  
      validateRootView(pageName, cachedString, cssPath);
} catch (LiteAppException e) {
      showRetry();
}                    

这个方法最主要的操作是根据liteAppId获取小程序配置信息,然后加载当前页面对应的bundle.js和css的内容,然后交给validateRootView方法执行,目前暂时没有界面上展示。我们继续看validateRootView方法实现q

protected void validateRootView(List<String> pageName, List<String> tabScripts, List<String> css) throws LiteAppException {  
...
// 创建 LiteAppFragment 加载单个页面
LiteAppFragment fragment = LiteAppFragment.  
                requireInstance(LiteAppFragmentActivity.this, liteAppID, pageName, tabScripts,css, bundle);
rootFragment = fragment;  
rootFragment.setOnPageChangeListener(new LiteAppFragment.OnPageChangeListener() {  
    @Override
    public void OnPageChanged(int index) {
         selectedTab.toggleSelect(false);
            tabBarItemViews.get(index).toggleSelect(true);
            selectedTab = tabBarItemViews.get(index);
         }
   });
rootFragment.setTabView(tabBarView);  
String tag = MINI_PROGRAM_ID + "/root/" + String.valueOf(fragmentCount);  
fragmentCount = fragmentCount +1;  
//pop up back stacks if activity is restored
FragmentTransaction transaction = fragmentManager.beginTransaction();  
if (fragmentManager.getBackStackEntryCount() > 0){  
     transaction.replace(R.id.lite_app_container_fragment, fragment, tag);
} else {
     transaction.add(R.id.lite_app_container_fragment, fragment, tag);
}       LogUtils.log(LogUtils.LOG_MINI_PROGRAM_FRAGMENT_LIFE_CYCLE,LogUtils.LIFE_FRAGMENT+LogUtils.FRAGMENT_PUSH);
        transaction.setTransition(FragmentTransaction.TRANSIT_NONE);
transaction.addToBackStack(tag);  
transaction.commit();  
stopLoading();  
liteAppMainView.setVisibility(View.VISIBLE);  
}

这个方法主要是创建了LiteAppFragment, 参数是liteAppID和加载的bundle、css。把LiteAppFragment添加到页面中并进行Fragment栈的管理,接着看LiteAppFragment的实现。在创建LiteAppFragment过程中先执行了prepareView方法,然后再看onCreateView

private void prepareView(Context context, List<String> pageName, final List<String> scripts,final List<String> cssPath, JSONObject bundle) throws LiteAppException {  
    if(scripts.size() == 1) {
        // 创建 LiteAppPage
        LiteAppPage page = LiteAppFactory.getWebViewLiteAppPageCache
                            (context, LiteAppPackageProvider.getClient().prepareLiteAppIfCache(liteAppID),bundle);
        // page对象和liteAppID绑定                    
        page.setAppID(liteAppID);
        // 注入css
        if(cssPath!=null){
             if(cssPath.size()>0){
                  page.injectPageCss(cssPath.get(0));
             }
        }
        liteAppPages.put(0,page);
        // 注入bundle.js
        if(!TextUtils.isEmpty(scripts.get(0))) {
            ExecutorManager.executeScript(page, scripts.get(0));
        }
    }
}

这个方法有太多的谜题待解决,LiteAppPage类的作用,page.injectPageCss的实现,ExecutorManager.executeScript(page, scripts.get(0))的实现,我们一个个来。先看LiteAppPage类吧

LiteAppPage继承自LiteAppContext, 主要对外暴露了下面几个方法:

  • ILiteAppContainer getContainer() // 获取 ILiteAppContainer
  • void injectPageCss(String cssPath) // 注入css
  • LiteAppPage setContainer(ILiteAppContainer container) // 设置ILiteAppContainer
  • public static LiteAppPage createPageInstance(Context androidContext, LiteAppDetail liteAppDetail) // 创建LiteAppPage对象

ILiteAppContainer是一个接口,实现类是WebViewLiteAppContainer, 看下这个类有哪些属性:

public class WebViewLiteAppContainer implements ILiteAppContainer {  
    private FrameLayout mContainerView;
    private WebView mWebView;
    private Context mContext;
    private FrameLayout mNativeLayerView;
    private FrameLayout mNativeHoverLayer;
    private ConcurrentLinkedQueue<String> patchBuffer = new ConcurrentLinkedQueue<>();
    private boolean loadFinished;
    private SwipeRefreshLayout swipeRefreshLayout;
    private LiteAppContext bindContext;
    ...
}

初步分析这个类是UI相关的类,包含了小程序运行容器WebView以及相关的View。我们着重来看一下这个类的bindLiteAppContext方法。

public void bindLiteAppContext(final LiteAppPage context) {  
        bindContext = context;
        // 创建一个FrameLayout布局
        mContainerView = new FrameLayout(context.getAndroidContext());
        mContext = context.getAndroidContext();
        // 创建一个WebView控件
        mWebView = new HalWebView(context.getAndroidContext());
        // 此处移除部分webView参数配置!
        mWebView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
                   // 利用console来实现Native和Web通信
                String messageContent = consoleMessage.message();
                Log.v(TAG, messageContent);
                if(messageContent.startsWith("hal:")){
                    String eventString = messageContent.substring(4,messageContent.length());
                    new ConsoleEventTask(context, eventString).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                }
                if(messageContent.startsWith("execute:")){
                    String executeString = messageContent.substring(8,messageContent.length());
                    if(TextUtils.isEmpty(executeString)){
                        return true;
                    }
                    ExecutorManager.executeScript(context,executeString);
                }
                return true;
            }
        });

        //native view create
        mNativeLayerView = new FrameLayout(context.getAndroidContext()){
            @Override
            protected void dispatchDraw(Canvas canvas) {
                int top = ((FrameLayout.LayoutParams)getLayoutParams()).topMargin;
                //Log.v("RENDER" ,"top:" + top + "  web view:" + webScroll + "   real top:" + getTop() );
                //hacking on top position 来防止拖影
                setTop(top);
                super.dispatchDraw(canvas);
            }
        };
        // 此处移除部分View的创建和参数配置             
        // 加载默认的框架内容
        mWebView.loadDataWithBaseURL("file:///mp_local/package",       LiteAppPackageProvider.getClient().getLiteAppFrameworkWebViewPart(context.getBindAppID()),
                "text/html", "utf-8", "");
        // 将LiteAppPage和WebViewLiteAppContainer建立联系        
        context.setContainer(this);
    }

这个方法主要是负责View的初始化和LiteAppPage的绑定。 也就意味着LiteAppPage其实是一个View容器类,负责界面渲染。

回到前面的话题,我们已经了解了LiteAppPage作用,再去看下page.injectPageCss的实现。

public void injectPageCss(String cssPath){  
    if(!TextUtils.isEmpty(cssPath)){
        container.injectCss(cssPath);
    }
}

调用了WebViewLiteAppContainer.injectCss(), 继续往下追

public void injectCss(String cssPath){  
    cssPath = "file:///" + cssPath;
    postPatch("addCssNative('"+  cssPath + "')");
}

给css路径添加"file:///"前缀。然后执行postPatch方法,我们继续看postPatch方法。

patchBuffer.add(data);  
Handler mainHandler = new Handler(mContext.getMainLooper());  
Runnable myRunnable = new Runnable() {  
    @Override
    public void run() {
         invalidBuffer();
    }
};
mainHandler.post(myRunnable);  

主要调用了invalidBuffer方法,invalidBuffer直接调用了invalidPatch,

private void invalidPatch(){  
        if(loadFinished) {
            if(patchBuffer.size()>0) {
                final String data = patchBuffer.poll();
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    if(mWebView == null){
                        return;
                    }
                    mWebView.evaluateJavascript(data, new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String value) {
                            invalidPatch();
                            Log.v(TAG, "invalid buffer:" + data);
                        }
                    });
                } else {
                    mWebView.loadUrl("javascript:" + data);
                    invalidPatch();
                }
            }
        } else {
            Log.v(TAG,"bridge not yet,cache");
        }
    }

主要是调用mWebView.evaluateJavascript来执行JS代码。上面传过来的addCssNative($cssPath) 就是执行JS内容。addCssNative是什么呢?这就设计到前端框架了。我提前把框架内容贴出来。

<!DOCTYPE html>  
<html lang="zh-CN">  
    <head>
      <title>this is template</title>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
        <link rel="stylesheet" href="./static/reset.css">
        <link rel="stylesheet" href="./component/component.css">
        <script>
          function addCssNative( cssPath ){
            var link = document.createElement('link');
            link.setAttribute('rel','stylesheet');
            link.setAttribute('href',cssPath);
            document.getElementsByTagName('head')[0].appendChild(link);
          }
        </script>
    </head>
    <body>
        <div id="page" class="mp-page">
        </div>
        <script src="./core/qy.webview.js" ></script>
        <script src="./component/component.webview.js" ></script>
    </body>
</html>  

addCssNative是一个JS方法,动态添加css文件, 通过这种方法动态渲染小程序的css。

接下来就是ExecutorManager.executeScript(page, scripts.get(0))分析了,粗略看起来就是JS引擎渲染执行JS代码。上代码:

public static void executeScript(final LiteAppContext context, final String script){  
        if(context.isPurged()){
            return;
        }
        // 切换到主线程
        context.getThread().getHandler().post(new Runnable() {
            @Override
            public void run(){
                if(context.nativeHandle == 0 ){
                   LogUtils.logError(LogUtils.LOG_MINI_PROGRAM_ERROR,
                            "null reference for executing script " ,null);
                    return;
                }
                if(TextUtils.isEmpty(script)){
                    return;
                }
                // 执行JS
                long timeout = ExecutorManagerNative.executeScript(context.nativeHandle,script);
                tickTimerDelay(context,timeout,context.nativeHandle);
            }
        });
    }

context.nativeHandle 是什么呢? 看看nativeHandle的来源:

nativeHandle = ExecutorManagerNative.createAsyncExecutor();  

而 createAsyncExecutor 是一个Native方法

public class ExecutorManagerNative {  
    ...
    public native static long createAsyncExecutor();
    ...
}

我们来看看实现。实现代码在native-lib.cc

JNIEXPORT jlong JNICALL  
Java_com_iqiyi_halberd_liteapp_context_ExecutorManagerNative_createAsyncExecutor(  
        JNIEnv *env, jobject instance) {
    jsc_bridge_executor* context =  hal_native_task_executor::createNewExecutor();
    return reinterpret_cast<jlong>(context);
}

原来 nativeHandle 是一个jsc_bridge_executor对象的应用强制转化为long的结果,我们再来看看jsc_bridge_executor

class jsc_bridge_executor{  
   public:
       jsc_bridge_executor();
       ~jsc_bridge_executor();

        void gc();
        bool executor_script(const char* buffer, std::string& e);
        bool call_object_function(JSContextRef ctx,JSObjectRef ref,
                                  std::string argument);
        liteapp::bridge::timer timer_;
        JSGlobalContextRef executor_context_ = nullptr;
        JSObjectRef global_ = nullptr;
        hal_javascript_object_ref global_ref;
    private:
        std::string executor_last_error_ = "";
        std::string executor_last_error_info_ = "";
    };

executor_script方法顾名思义执行JS, call_object_function执行对象的方法,其他的方法后续用来再说。上面创建对象用的hal_native_task_executor::createNewExecutor(), 代码里面其实就是new了一个对象,就不展开说了,看代码一目了然

jsc_bridge_executor * hal_native_task_executor::createNewExecutor() {  
    if(current_thread_executor== nullptr){
        current_thread_executor = new jsc_bridge_executor;
    }
    return current_thread_executor;
}

到这里 nativeHandle 基本介绍清楚了,我们再来看刚才执行JS的这两句话:

long timeout = ExecutorManagerNative.executeScript(context.nativeHandle,script);  
tickTimerDelay(context,timeout,context.nativeHandle);  

ExecutorManagerNative.executeScript也是一个Native方法, 参数是上述的nativeHandle和执行的js脚本

JNIEXPORT jlong JNICALL  
Java_com_iqiyi_halberd_liteapp_context_ExecutorManagerNative_executeScript(  
        JNIEnv *env, jclass type, jlong nativeHandle, jstring script_) {
    jboolean copy = JNI_FALSE;
    const char *script = env->GetStringUTFChars(script_, &copy);
    hal_native_task_executor::postTaskToExecutor(
            reinterpret_cast<jsc_bridge_executor*>(nativeHandle), (char *) script);
    env->ReleaseStringUTFChars(script_, script);
    return 1;
}

将nativeHandle转化为jscbridgeexecutor对象,并执行halnativetask_executor::postTaskToExecutor方法。

long hal_native_task_executor::postTaskToExecutor(jsc_bridge_executor* executor, char* jsJobString){  
    if(executor== nullptr || jsJobString == nullptr){
        return false;
    }
    if(strlen(jsJobString) > 0) {
        std::string exception = "";
        executor->executor_script(jsJobString, exception);
    }
    return true;
}

最后调用的jscbridgeexecutor的 executor_script 方法。这个方法的实现就是调用JSC(JavaScriptCore简称,后续都称为JSC)来执行JS, 暂不进行详细介绍,只需要知道是执行JS就行了。

未完待续!!!