首先整體介紹一下Hummer框架,官網地址 https://hummer.didi.cn/home#/ ;
Hummer 是一套高性能高可用的跨端開發框架,一套代碼可以同時支持開發 Android 和 iOS 應用。現已經支持 Vue/TypeScript/JavaScript 三種語法,面向大前端開發人員,總有一款適合你。
總之一句話,Hummer框架就是爲了提高開發效率。
系列文章
《碼一個簡易的跨端框架》
這篇文章介紹如何將Quickjs在androidstudio中編譯,以及實現一個簡易的js->js引擎->c->java的回調過程,適用於想要了解跨端框架的人羣。
本篇文章要達到的目的
- Hummer框架的項目結構簡介
- Hummer框架是如何將js代碼通過js引擎轉換成原生代碼的
Hummer項目結構以及依賴關係
+--- project :hummer
| +--- project :hummer-component
| | \--- project :hummer-sdk
| | +--- com.didi.hummer:hummer-annotation:0.2.2
| | +--- com.didi.hummer:hummer-plugin-interface:0.0.1
| | +--- project :hummer-core
| \--- project :hummer-dev-tools
project:hummer
主要功能:
- 初始化SDK:
public static void init(Context context, HummerConfig config)
- 提供頁面:
public class HummerActivity
public class HummerFragment
project :hummer-component
主要功能:
- 定義Hummer組件;例如在js代碼中寫
var button=new Button();
對應到原生組件就是
@Component("Button")
public class Button extends HMBase<android.widget.Button>
@Component("Image")
public class Image extends HMBase<RoundedImageView>
@Component("View")
public class View extends HummerLayoutExtendView
具體這個組件是如何定義的,以及爲什麼要繼承HMBase
等我們以後再做分析,這裏主要是知道js中寫的組件對應的都是原生組件在這個工程下就可以了。
project :hummer-sdk
主要功能:
- 適配器(Adapter):將網絡請求
okhttp
、圖片加載glide
、本地存貯SharedPreferences
等一些功能進行隔離,對上層框架提供接口封裝底層實現方便以後替換實現。 - 上下文(Context):爲Hummer的上下文環境,返回全局的屬性
HummerLayout、JSContext
等;處理生命週期onStart()、onResume、onPause、onStop、onDestory
等;給JS引擎注入JS模板mJsContext.evaluateJavaScript(AssetsUtil.readFile(HUMMER_DEFINITION_FILE), "HummerDefinition.js");
;設置JS引擎回調到Java層的invoke類,進行具體的操作registerInvoker(new HummerInvoker()); registerInvoker(new NotifyCenterInvoker());
- 模塊層:將適配器(Adapter)的功能轉換成JS代碼可以用的Module
@Component("Memory")
public class Memory //存儲
@Component("Request")
public class Request implements ILifeCycle //網絡請求
@Component("Location")
public class HMLocation implements ILifeCycle //定位
- 頁面渲染(render):
- HummerLayout是對YogaLayout的封裝,修改了一些bug,以及支持對View的添加和刪除等Hummer中自定義的功能;
- BackgroundDrawable這個主要是處理View背景的渲染,設置邊框的Hummer中的特殊操作;
- HummerStyleUtils主要是用來解析js代碼中的Style屬性,把他對應到yogalayout或者Hummer自定義的原生屬性上
project :hummer-core
主要功能:
這層主要是和JS引擎交互層,
- 調用native本地方法註冊JS引擎的Bridge
HummerBridge.initHummerBridge(long jsContext)
- JS引擎解析JS代碼後通過JNI接口回調Java層的方法
HummerBridge.invoke(String className, long objectID, String methodName, long... params)
- 通過native本地方法執行、注入JS代碼的方法
JavaScriptRuntime.evaluateJavaScriptNative(long jsContext, String script, String scriptId)
- JS引擎代碼層,我們看jni目錄下,有hermes、jsc(JavaScriptCore)、qjs(Quickjs),還有編譯他們的CMakeList.txt文件,我們目前只看qjs目錄;./quickjs目錄爲github上下載的源碼;./hummer目錄爲對quickjs代碼封裝的JNI接口,提供給java層使用;
HummerBridge.cpp、HummerRecycler.cpp
等目前不做過多代碼解析
Hummer頁面渲染(一)
- Hummer是如何註冊quickjs_bridge:
時序圖步驟4、5、6、7、8、9、10、11、12、13代碼非常簡單都是順序調用;
重點介紹14、15、16、17
HummerBridge.cpp
/**
* 14步
* 對QuickJs引擎設置屬性,也就是設置回調方法,將invoke這個C方法通過JS_SetPropertyStr設置到Quickjs引擎中
* 這樣在js代碼中就可以調用invoke這個全局方法了
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_didi_hummer_core_engine_jsc_jni_HummerBridge_initHummerBridge(JNIEnv *env, jobject thiz, jlong js_context) {
auto jsContext = QJS_CONTEXT(js_context);
//創建HummerBridge類的jni對象
jobject bridge = env->NewGlobalRef(thiz);
HUMMER_BRIDGE_MAP[js_context] = bridge;
//找到HummerBridge.java
//private long invoke(String className, long objectID, String methodName, long... params)
HUMMER_BRIDGE_INVOKE_ID = env->GetMethodID(
env->GetObjectClass(thiz),
"invoke",
"(Ljava/lang/String;JLjava/lang/String;[J)J");
//將C方法invoke創建成JS引擎中的方法
auto funcName = "invoke";
auto invokeFunc = JS_NewCFunction(jsContext, invoke, funcName, strlen(funcName));
//將這個創建之後的js引擎中的invokeFunc方法注入到js引擎中,成爲全局方法,這樣在寫js代碼時就可以直接使用invoke這個方法
JS_SetPropertyStr(jsContext,
JS_GetGlobalObject(jsContext),
"invoke",
invokeFunc);
}
/**
*15步
* js引擎通過JS_SetPropertyStr將這個方法注入到js引擎中,寫js代碼時可以直接調用這個方法,
* 這個方法的聲明是參照JSCFunction方法聲明的可以查看,在這個方法中通過jni調用java中的代碼,
* 實現js引擎和java代碼的通信
*/
static JSValue invoke(JSContext* ctx, JSValueConst thisObject, int argumentCount, JSValueConst* arguments) {
long ctxId = QJS_CONTEXT_ID(ctx);
jobject bridge = HUMMER_BRIDGE_MAP[ctxId];
if (bridge == nullptr) return JS_NULL;
JNIEnv* env = JNI_GetEnv();
//取出 long invoke(String className,long objectID,String methodName,long ...param);的最後一個參數long數組
jlongArray params = nullptr;
if (argumentCount > 3) {
int methodParamsCount = argumentCount - 3;
params = env->NewLongArray(methodParamsCount);
jlong paramsC[methodParamsCount];
for (int i = 3; i < argumentCount; i++) {
paramsC[i - 3] = QJS_VALUE_PTR(arguments[i]);
}
env->SetLongArrayRegion(params, 0, methodParamsCount, paramsC);
}
//取出objectID
int64_t objId;
JS_ToInt64(ctx, &objId, arguments[INDEX_OBJECT_ID]);
//取出className
jstring className = TypeConvertor::JSString2JavaString(ctx, arguments[INDEX_CLASS_NAME]);
//取出methodName
jstring methodName = TypeConvertor::JSString2JavaString(ctx, arguments[INDEX_METHOD_NAME]);
//調用HummerBridge.java
// private long invoke(String className, long objectID, String methodName, long... params)
jlong ret = env->CallLongMethod(
bridge, HUMMER_BRIDGE_INVOKE_ID,
className, objId, methodName,
params);
env->DeleteLocalRef(className);
env->DeleteLocalRef(methodName);
env->DeleteLocalRef(params);
JNI_DetachEnv();
return QJS_VALUE(ret);
}
HummerBridge.java
/**
* 16步
* JNI回調的java代碼
*/
private long invoke(String className, long objectID, String methodName, long... params) {
if (mCallback == null) {
return JSCUtils.POINTER_NULL;
}
long result = JSCUtils.POINTER_NULL;
Object[] parameters = null;
try {
// 把long型的JS對象指針參數轉換爲Object或JSValue對象參數
parameters = JSCUtils.jsValuesToObjects(jsContext, params);
// 執行具體的invoke方法,並得到Object類型的返回值
Object ret = mCallback.onInvoke(className, objectID, methodName, parameters);
// 把Object類型的返回值轉換成long型的JS對象指針
result = JSCUtils.objectToJsValue(jsContext, ret);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
JSCHummerContext.java
/**
* 17步
* 從HummerBridge.java通過callback回調到這個方法中
* 在mRegistry.get(className)中找到註冊過的Invoker組件
* 具體註冊:例如
* registerInvoker(new HummerInvoker());
* registerInvoker(new NotifyCenterInvoker());
* registerInvoker(new Button$$Invoker());
* registerInvoker(new View$$Invoker());
*/
@Override
public Object onInvoke(String className, long objectID, String methodName, Object... params) {
Invoker invoker = mRegistry.get(className);
if (invoker == null) {
HMLog.w("HummerNative", String.format("Invoker error: can't find this class [%s]", className));
return null;
}
return invoker.onInvoke(this, objectID, methodName, params);
}
- Hummer是如何註冊js_base模板代碼、js_component組件模板代碼
18步;註冊js_base模板代碼
HummerContext.java
18步
protected void onCreate() {
......
//註冊組件
registerInvoker(new HummerInvoker());
registerInvoker(new NotifyCenterInvoker());
//註冊js_base模板代碼
mJsContext.evaluateJavaScript(AssetsUtil.readFile(HUMMER_DEFINITION_FILE), "HummerDefinition.js");
......
}
/**
*HummerDefinition.js文件截選
*預先將這段js代碼注入引擎,js組件都繼承Base類
*構造方法,以及每個定義的方法都調用了之前實現在js引擎中
*創建好的invoke方法,通過這個方法回調到java側執行相關操作
*/
class Base {
constructor(className, ...args) {
this.className = className;
this.objID = idGenerator();
this.recycler = new Recycler(this.objID);
let params = transArgs(...args);
invoke(this.className, this.objID, "constructor",
this, ...params);
this.initialize(...args);
}
initialize(...args) {}
set style(arg) {
this._style = arg;
arg = transSingleArg(arg);
invoke(this.className, this.objID, "setStyle", arg);
}
get style() {
return this._style;
}
}
19步註冊js_component組件模板代碼
HummerRegister$$hummer_component.java
/**
* 註冊java層組件,注入js_component模板代碼
* js_component繼承js_base的Base類,以及通過
* 預先註冊好的invoke方法和java側建立聯繫,
* 回調到registerInvoker註冊的Java側組件進行相關操作
*/
public static void init(HummerContext hummerContext) {
hummerContext.registerInvoker(new Image$$Invoker());
hummerContext.registerInvoker(new Loading$$Invoker());
hummerContext.registerInvoker(new TextArea$$Invoker());
hummerContext.registerInvoker(new Input$$Invoker());
hummerContext.registerInvoker(new Switch$$Invoker());
hummerContext.registerInvoker(new ViewPager$$Invoker());
hummerContext.registerInvoker(new Toast$$Invoker());
hummerContext.registerInvoker(new Dialog$$Invoker());
hummerContext.registerInvoker(new Button$$Invoker());
hummerContext.registerInvoker(new List$$Invoker());
hummerContext.registerInvoker(new View$$Invoker());
hummerContext.registerInvoker(new Anchor$$Invoker());
hummerContext.registerInvoker(new Text$$Invoker());
hummerContext.registerInvoker(new Scroller$$Invoker());
hummerContext.registerInvoker(new HorizontalScroller$$Invoker());
hummerContext.evaluateJavaScript(JS_CODE, "hummer_component.js");
}
hummer_component.js代碼片段
/**
*繼承js_base的Base類,擁有了Base類的基礎能力
*定義自己的特有能力appendChild方法內調用
*js注入的invoke方法,通知到java側View組件執行相關操作
*/
class View extends Base {
constructor(...args) {
super('View', ...args);
}
appendChild(...args) {
let stash = args;
args = transArgs(...args);
invoke('View', this.objID, 'appendChild', ...args);
}
removeChild(...args) {
let stash = args;
args = transArgs(...args);
invoke('View', this.objID, 'removeChild', ...args);
}
}
class Button extends Base {
constructor(...args) {
super('Button', ...args);
}
set text(arg) {
this._text = arg;
arg = transSingleArg(arg);
invoke('Button', this.objID, 'setText', arg);
}
get text() {
return this._text;
}
}
- Hummer是如何註冊原生組件
20步註冊原生組件
HummerRegister$$hummer_component.java
//在這個aop生成的java文件中註冊原生組件,在這裏註冊之後
// 17步的Invoker invoker = mRegistry.get(className);才能獲取到,
//才能執行到java組件中的invoke方法執行相關的操作
hummerContext.registerInvoker(new View$$Invoker());
/**
* 17步根據className獲取到對應的組件,執行android組件的invoke方法
* aop根據@Component("View")生成的代碼
* 具體執行回調到invoke方法通過methodName進行判斷
*/
public class View$$Invoker extends BaseInvoker<View> {
@Override
public String getName() {
return "View";
}
@Override
protected View createInstance(JSValue jsValue, Object[] params) {
String param0 = params.length > 0 && params[0] != null ? String.valueOf(params[0]) : null;
return new View(mHummerContext, jsValue, param0);
}
@Override
protected Object invoke(View instance, String methodName, Object[] params) {
Object jsRet = null;
switch (methodName) {
case "appendChild": {
long childId0 = params.length > 0 && params[0] != null ? ((Number) params[0]).longValue() : 0;
HMBase param0 = mInstanceManager.get(childId0);
instance.appendChild(param0);
} break;
case "removeChild": {
long childId0 = params.length > 0 && params[0] != null ? ((Number) params[0]).longValue() : 0;
HMBase param0 = mInstanceManager.get(childId0);
instance.removeChild(param0);
} break;
}
return jsRet;
}
}
Hummer頁面渲染(二)
通過上面介紹我們明白了
- js引擎和java側的交互通道
- js代碼中的組件是通過預先注入js_base、js_component代碼實現的
- java側組件的注入,實現js側和java側一對一的組件
- 我們寫的js組件通過1、2應對到3中的java側組件執行相關操作,
下面介紹回調到java側組件之後我們 如何操作
第一步創建對象
//js側創建一個對象回調到BaseInvoker.java文件中,調用如下方法創建一個對象
//根據objectID保存到mInstanceManager中
private T getInstance(long objectID, String methodName, Object... params) {
// objectID <= 0說明是static方法
if (objectID <= 0) {
return null;
}
T instance = mInstanceManager.get(objectID);
if (instance == null && "constructor".equals(methodName)) {
JSValue jsValue = params.length > 0 ? ((JSValue) params[0]) : null;
if (jsValue != null) {
if (params.length > 1) {
Object[] parameters = Arrays.copyOfRange(params, 1, params.length);
instance = createInstance(jsValue, parameters);
} else {
instance = createInstance(jsValue);
}
if (instance instanceof ILifeCycle) {
((ILifeCycle) instance).onCreate();
}
mInstanceManager.put(objectID, instance);
}
}
return instance;
}
第二步設置屬性
//創建完成對象我們js側調用 this.style = {}設置樣式
//回調到BaseInvoker.java文件中
//invokeHMBase這個方法根據methodName來匹配js側調用了什麼方法,去執行相應的操作
private Object invokeHMBase(T instance, String methodName, Object... params) {
HMBase base = (HMBase) instance;
Object ret = null;
switch (methodName) {
case "setStyle":
//解析js側傳過來的style樣式
Map<String, Object> styleMap = HMJsonUtil.toMap(String.valueOf(params[0])); // json耗時1ms
base.setStyle(getSortedMap(styleMap)); // sort耗時1ms
break;
......
}
return ret;
}
//因爲每個對象的基類都是HMBase.java,所以調用這個類的
//setStyle方法,這個方法會調用HummerNode.setStyle方法
//在這個方法中調用HummerStyleUtils.applyStyle(linkView, style);
//來具體解析Style屬性,YogaNode支持的屬性直接設置,有很多不支持的屬性需要
//Hummer自己進行處理,自己處理部分查看HMBase.setHummerStyle方法,都是背景的處理
// 設置控件樣式
static void applyStyle(boolean needIntercept, HMBase view, Map style) {
if (view == null || style == null) {
return;
}
Map<String, Object> transitionStyle = new HashMap<>();
for (Object k : style.keySet()) {
String key = String.valueOf(k);
Object value = style.get(k);
if (needIntercept && HummerLayoutExtendUtils.interceptDisplayStyle(view, key, value)) {
// 如果需要攔截,並且是Display相關屬性,需要先被攔截
continue;
} else if (Hummer.POSITION.equals(key) || Hummer.POSITION_TYPE.equals(key) || Hummer.DISPLAY.equals(key)) {
// 如果是Position或者Display相關屬性,優先處理
if (view.setHummerStyle(key, value)) {
continue;
}
} else if (key.startsWith(Hummer.TRANSITION)) {
// transition 參數跳過 在最後處理
transitionStyle.put(key, value);
continue;
}
try {
// transform 必須走 animator 進行變化
if (Hummer.TRANSFORM.equals(key) || (isTransitionStyle(key) && (view.supportTransitionStyle("all") || view.supportTransitionStyle(key)))) {
view.handleTransitionStyle(key, value);
} else if (isYogaStyle(key)) {
// 處理Yoga支持樣式
applyYogaStyle(view.getYogaNode(), key, value); // 耗時1ms
} else {
// 處理Hummer自定義樣式
applyHummerStyle(view, key, value); // 耗時1ms
}
} catch (Exception e) {
// 處理異常信息顯示不全的問題,補充異常是在哪個key和value下產生
ExceptionUtil.addStackTrace(e, new StackTraceElement("<<Style>>", "", String.format("%s: %s", key, value), -1));
HummerException.nativeException(view.getJSValue().getJSContext(), e);
}
}
view.getView().requestLayout();
// 設置transition樣式
for (Object k : transitionStyle.keySet()) {
String key = String.valueOf(k);
Object value = style.get(k);
view.setTransitionStyle(key, value);
}
view.runAnimator();
}
第三步渲染
//js側調用view.appendCild()會回調到invoke方法,通過methodName判斷
@Override
protected Object invoke(View instance, String methodName, Object[] params) {
Object jsRet = null;
switch (methodName) {
case "appendChild": {
long childId0 = params.length > 0 && params[0] != null ? ((Number) params[0]).longValue() : 0;
HMBase param0 = mInstanceManager.get(childId0);
instance.appendChild(param0);
} break;
}
return jsRet;
}
//調用到java側View組件
@Component("View")
public class View extends HummerLayoutExtendView {
......
@Override
@JsMethod("appendChild")
public void appendChild(HMBase child) {
super.appendChild(child);
if (child == null) {
return;
}
hummerNode.appendChild(child.getNode());
......
}
......
}
/**
* 從如下代碼看如果YogaLayout支持的屬性直接調用addView添加
* 如果不支持的屬性需要單獨進行處理
*/
public void appendChild(HMBase child) {
.....
// 支持 position:fixed 佈局樣式,添加至窗口視圖
if (child.getPosition() == HummerLayoutExtendUtils.Position.FIXED) {
hummerContext.getContainer().addView(child);
FixedNoneBox fixedNoneBox = new FixedNoneBox(hummerContext);
fixedNoneBoxMap.put(child, fixedNoneBox);
finalChild = fixedNoneBox;
}
// 支持 display:inline 佈局樣式,外部擴展橫向flex佈局
if (getDisplay() == HummerLayoutExtendUtils.Display.BLOCK) {
HummerLayoutExtendUtils.markExtendCssView(child);
if ((child.getDisplay() == HummerLayoutExtendUtils.Display.INLINE
|| child.getDisplay() == HummerLayoutExtendUtils.Display.INLINE_BLOCK)) {
HMBase lastChild = getView().getLastChild();
if (lastChild instanceof InlineBox) {
child.setInlineBox((InlineBox) lastChild);
((InlineBox) lastChild).add(child);
finalChild = null;
} else {
InlineBox box = new InlineBox(hummerContext);
child.setInlineBox(box);
box.add(child);
inlineBoxes.add(box);
finalChild = box;
}
}
}
// 擴展樣式處理
if (HummerLayoutExtendUtils.isExtendCssView(child)) {
HummerLayoutExtendUtils.applyChildDisplayStyle(getDisplay().value(), child);
}
// 默認處理
if (finalChild != null) {
getView().addView(finalChild);
}
}
/**
*HummerLayout中調用YogaLayout.addView添加View,此時subview.getYogaNode()已經設置好Yoga支持的屬性
*添加子視圖
* @param subview 子視圖
* @param index 位置
*/
public void addView(@NonNull HMBase subview, int index) {
if (subview == null) {
return;
}
addView(subview.getView(), subview.getYogaNode());
// 當removeChild再addChild的時候,data會被置爲null,需要重新setData
if (subview.getYogaNode().getData() == null) {
subview.getYogaNode().setData(subview.getView());
}
int childCount = getYogaNode().getChildCount();
index = index < 0 ? childCount : index;
getYogaNode().addChildAt(subview.getYogaNode(), index);
children.put(subview.getViewID(), subview);
if (index == childCount) {
lastChild = subview;
}
}
總結:
- Hummer框架通過quickjs和jni實現對js側和java側的通信橋樑;
- 通過提前注入js組件使得開發者可以使用預先設置的組件和功能;
- java側註冊和js一對一的組件,通過橋樑的方式回調到java側的組件實現相應的操作