装饰器背后的秘密
学习完鸿蒙开发第一课,我兴高采烈的把一个个例子写出来后,一个个绚丽多彩的ui展示在屏幕上。依靠着ArkTS 提供的装饰器,比如Component等,我们可以方便定义好各种声明式的写法。装饰器!这么一个神奇的东西,背后肯定是离不开编译器的"加工",把TS本身的写法进行填充。比如Compsoe中的Composable ,其实就是离不开kotlin 编译器的背后"加工",才得以让我们如此方便使用。当第一次接触ArkTS的装饰器概念的时候,我就猜到,这肯定也是编译器的"魔法"。
下面让我们通过反编译,破解这一层面纱。
探索出发
我们以一个例子出发,从官方的demo开始,我们添加一个自定义的试图
typescript
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
import errorManager from '@ohos.app.ability.errorManager';
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
this.registerErrorObserver1();
this.registerErrorObserver2();
throw URIError
}
onDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
onWindowStageDestroy() {
// Main window is destroyed, release UI related resources
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
private registerErrorObserver1() {
errorManager.on("error", {
onUnhandledException(error: string) {
hilog.error(0x0000, 'hello',"registerErrorObserver1:"+error)
}
})
}
private registerErrorObserver2() {
errorManager.on("error", {
onUnhandledException(error: string) {
hilog.error(0x0000, 'hello',"registerErrorObserver2: "+error)
}
})
}
onForeground() {
// Ability has brought to foreground
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground() {
// Ability has back to background
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
}
};
less
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
RelativeContainer() {
Text("text1")
.fontSize(25)
.id("text1")
.textAlign(TextAlign.Center)
.backgroundColor("#ccaabb")
.alignRules({
top:{
anchor:"text2",
align:VerticalAlign.Top
}
})
Text("text2")
.fontSize(25)
.textAlign(TextAlign.Center)
.backgroundColor("#bbccaa")
.alignRules({
// 以text1 为参考系,顶部对齐
top:{
anchor:"text1",
align:VerticalAlign.Top
}
})
}
.width('100%')
.height(200)
.backgroundColor("#111111")
}
.width('100%')
}
.height('100%')
}
}
当我们进行编译的时候,可以在output路径下得到一个.hap后缀安装包
对hap进行解包
很多小伙伴对hap包非常陌生,其实跟android apk原理是相似的,都是从zip文件的变种而来,此时我们只需要把后缀改为zip,然后进行解压缩,我们就能看到里面的东西。
此时,我们得到了关键的产物,就是modules.abc
方舟字节码abc
方舟字节码(ArkCompiler Bytecode)文件,是ArkCompiler的编译工具链以源代码作为输入编译生成的产物,其文件后缀名为.abc。也就是说,我们得到的modules.abc,其实是运行在鸿蒙中的字节码(初始形态)。
也就是说,如果我们能够对abc文件进行解析,比如像dex文件一样进行格式解析,那么我们是能够得到所有的运行时信息的。遗憾的是,abc字节码目前资料比较少,关键的段定义还在变化,但是熟悉编译的我们都知道。代码的text段,无论是dex或者是abc,都是应该也原型记录的。如果我们能用一种工具直接解析abc文件的string字段,那么我们就能够看到真实的源码。
这里只需要对16进行进行字符解析的任何工具,都是可以的,我这里推荐010Editor,通过解析modules.abc,同时采取16进行解析后,我们可以得到以下代码
没错!这就是代码的"真面目"! 因为abc 通过ArkCompiler 解析后,其实就是TS的真实代码
理解Component
当我们用@Component修饰一个struct的时候,通过ArkCompiler编译后,其实会生成一个类,这个继承于ViewPU。这就是所有鸿蒙组件的基类,它承担着ui刷新,localstore存储更新等关键逻辑。
ViewPU定义在ArkTS framework arkui_ace_engine当中,是Openharmony中UI承载的关键类,其关键渲染逻辑在C++中(以后会讲到)。ViewPU 定义在pu_view.ts文件中,我们来看一下
我们本章不介绍ViewPU的具体内容,留到下一章节中,先看一下构造函数
typescript
constructor(parent: ViewPU, localStorage: LocalStorage, elmtId : number = -1) {
super();
构造特有id
this.id_= elmtId == -1 ? SubscriberManager.MakeId() : elmtId;
this.providedVars_ = parent ? new Map(parent.providedVars_)
: new Map<string, ObservedPropertyAbstractPU<any>>();
this.localStoragebackStore_ = undefined;
// 设置parent的关系
stateMgmtConsole.log(`ViewPU constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}}'`);
if (parent) {
// this View is not a top-level View
this.setCardId(parent.getCardId());
// Call below will set this.parent_ to parent as well
parent.addChild(this);
} else if (localStorage) {
this.localStorage_ = localStorage;
stateMgmtConsole.debug(`${this.debugInfo()}: constructor: Using LocalStorage instance provided via @Entry.`);
}
添加订阅
SubscriberManager.Add(this);
stateMgmtConsole.debug(`${this.debugInfo()}: constructor: done`);
}
ViewPU的概念其实很简单,它负责调用SubscriberManager添加自身,后面state的回调会进行callback,同时它也有父子组件的概念,ViewPU有一个parent属性,代表当前的父组件,父子双方共用同一个localStorage,其实就是通过构造函数保证的。
同时构造过程,每一个ViewPU会构造一个特有的id,用作组件的区分,后续刷新逻辑会用到。我们开胃菜就先到这里结束。
结束
到这里,本章先简单介绍,如何通过反编译查看abc字节码的内容,通过反编译后的类,我们能够窥探鸿蒙运行过程的真相。学习源码能够帮开发者快速理解系统背后的运行逻辑,也方便我们后续定制修改。当然,在不影响Android内容输出的同时,我后续将会发布更多ArkTS的内容,让我们了解鸿蒙运行的机制,bye~