当步伐开发到一定水平的时候,我们不可制止的遇到针对方法进行hook的需求,好比针对方法调用的监听,好比修改方法的参数亦或者是返回值。同样在ArkUI开发中,我也同样遇到了相干的需求,好比我想监控某个控件的刷新(rerender),或者是修改一些控件的默认活动等等。
JavaScript的动态特性
下面是大概的方舟前端编译流程:
在JavaScript中,类中的方法并不像Java一样在编译期间就会固定下来,而是可以在运行时进行动态修改。好比我们随时可以在运行时针对某个类进行方法的增编削,好比下面一个例子针对Person类新加一个方法
- class Person {
- constructor(name) {
- this.name = name;
- }
- greet() {
- console.log(`Hello, my name is ${this.name}`);
- }
- }
- // 创建一个Person实例
- const person1 = new Person('Alice');
- person1.greet(); // 输出:Hello, my name is Alice
- // 动态添加一个新方法
- Person.prototype.sayGoodbye = function() {
- console.log('Goodbye!');
- };
- // 调用新添加的方法
- person1.sayGoodbye(); // 输出:Goodbye!
复制代码 之以是可以或许做到,是因为每个 JavaScript 对象都有一个 proto 属性,指向它的原型对象。当我们访问一个对象的属性或方法时,如果对象本身没有这个属性或方法,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(Object.prototype)。
JavaScript允许我们修改对象,因此实现方法hook可以说是天生就支持的特性,我们再来认识一下JavaScript的方法查找过程:
- 如果类本身可以或许找到方法,好比静态方法,那么就直接实行,否则进行2
- 查找prototype对象中,有没有对应的方法,有则实行,否则出现error
ArkTS中在编译期克制了针对方法的动态修改,但是不要紧,我们都知道实在前端编译产物是JS,之后是abc字节码,因此我们依旧可以在运行时进行方法的修改,方法的修改代码我们可以放在ts/js中,只必要外部传递一个对象进来即可。
实现方法hook
当然,并不是所有方法都可以或许被修改,好比在JavaScript中,通过getOwnPropertyDescriptor 方法返回属性形貌符对象,其中有以下几个属性:
value: 属性的值。
writable: 一个布尔值,表示该属性是否可写。如果为 false,则不能修改该属性的值。
get: 一个函数,当读取该属性时会被调用。
set: 一个函数,当给该属性赋值时会被调用。
enumerable: 一个布尔值,表示该属性是否可罗列。如果为 false,则在使用 for...in 循环或 Object.keys() 方法时不会罗列到该属性。
configurable: 一个布尔值,表示该属性是否可设置。如果为 false,则不能删除该属性,也不能修改该属性的形貌符。
因此如果我们遇到writable为false的方法,我们就无法进行方法的直接更换,同时ArkUI中,有很多方法被提前注册进了Native引擎,因此这些方法即使修改乐成了也无法达到我们运行时更换想要的效果。
鸿蒙官方的实现
从上面先容我们相识了JavaScript的方法特性,我们也相识到了一个类的方法要么在类对象本身,或者在其prototype中,因此我们只必要针对类对象的属性以及prototype本身进行查找就可以或许找到方法本身,然后再更换掉这个方法即可。
当然,使用上面的原理,鸿蒙官方HarmonyOS也并提供了Aspect类,包罗addBefore、addAfter和replace接口。这些接口可以在运行时对类方法进行前置插桩、后置插桩以及更换方法实现。
但是,官方工具有个非常大的缺陷是,我们无法通过这些方法只修改函数的入参以及返回值,我们只能更换方法的实现,究其原因是被插桩的原函数没有暴露给开发者,因此我们盼望有一个机制可以把原函数暴露给开发者。
实现更加机动的MiniHook
我们根据上面的原理,依旧可以实现一个自定义的hook方法框架,同时我们可以把原函数按照“尾参数”的情势,暴露给开发者,使用者编写hook类,方法参数列表如下【原参数1,原参数2,.... ,原方法】,如许使用者就无需关注调用addBefore、addAfter还是replace,只必要关注一个方法即可!
- export function hookFunc(target, action, temp) {
- let origin = target[action]
- let protoTypeOrigin = target.prototype[action]
- let destination = origin ?? protoTypeOrigin
- if (!destination) {
- throw new Error(`target not found `);
- }
- let isPrototype = protoTypeOrigin != null && protoTypeOrigin != undefined
- if (destination) {
- // 替换原有函数为插桩函数
- let copyOrigin = isPrototype ? target.prototype : target
- const descriptor = Object.getOwnPropertyDescriptor(copyOrigin, action);
- // writable 为false方法无法替换
- if (descriptor && !descriptor.writable) {
- throw new Error(`target is an unwritable obj `);
- }
- copyOrigin[action] = function (...args: any[]) {
- if (temp) {
- args.push(destination)
- return temp.apply(this, args);
- }
- }
- }
- }
复制代码 同时我们也提供装饰器的方式,提供一个函数装饰器,用于把被修饰的函数当作更换函数
- // hook 装饰器
- export function MiniHook(callTarget: any, action: any) {
- return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
- const tempMethod = descriptor.value;
- hookFunc(callTarget, action, tempMethod)
- };
- }
复制代码 通过上面两个关键实现,我们想要hook一个方法时,就变得非常简单,下面我们以Test类的几个方法举例子
- export class Test {
- myhook() {
- hilog.error(0, "hello", "myhook")
- }
- // 带参数的情况
- myhook2(param: number) {
- hilog.error(0, "hello", "myhook2 " + param)
- }
- // 带返回值的情况
- myhook3(param: number): number {
- hilog.error(0, "hello", "myhook3 " + param)
- return param + 1
- }
- }
复制代码 hook方法时,我们只必要注册一个函数,并且每个函数的的参数对应着原始参数,并在最后添加一个原函数对象,使用姿势如下:
- export class MyHook {
- // MiniHook 第一个参数为hook的类,第二参数为hook的函数, 定义一个需要替换的函数,其中必须要有一个参数,即origin 为你要hook的原函数对象
- @MiniHook(Test,"myhook")
- stubFunc1(origin: () => void) {
- hilog.error(0, "hello", "前面插入一条日志")
- origin()
- hilog.error(0, "hello", "后面插入一条日志")
- }
- @MiniHook(Test,"myhook2")
- stubFunc2(param: number, origin: (param: number) => void) {
- hilog.error(0, "hello", "修改param参数为2")
- origin(2)
- }
- @MiniHook(Test,"myhook3")
- stubFunc3(param: number, origin: (param: number) => number) {
- let result = origin(param)
- hilog.error(0, "hello", "stubFunc 原函数返回值" + result)
- return 3
- }
- }
复制代码 通过上面的例子我们可以看到,整个框架使用起来非常方便,作为使用者,我们只必要编写一个hook函数,这个函数可以是某个类函数,方便我们做层级分别或者功能分别。接着在hook函数上可以通过我们预先编写的装饰器@MiniHook,输入指定的参数,第一个为类对象(类构造函数),第二个参数为我们想要hook的方法名,接着我们编写hook参数列表时,按照被hook函数的参数列表依次填写即可,最后别忘了增补一个原函数对象,因为这个对象会在hook实行过程中被我们添加到最后。
- copyOrigin[action] = function (...args: any[]) {
- if (temp) {
- args.push(destination)
- return temp.apply(this, args);
- }
- }
复制代码 装饰器的进一步封装,低落了使用者的成本,同时我们使用者也无需关注调用哪个api,只必要定义好本身的hook函数就可以或许实行修改参数、修改返回值、前后或者更换插桩等操作,方便我们用于后续的活动监控或者其他目的。
总结
通过相识JavaScript的方法查找机制与鸿蒙官方提供的Aop机制,我们可以或许快速相识到实现一个方法hook所必要的步调,同时我们剖析了官方实现的缺陷,并通过针对原函数的改进以及装饰器的封装,实现了一个更加便捷功能更加全面的MiniHook,盼望可以或许对读者有所资助!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |