鸿蒙ArkUI开发进阶 - 实现简易的方法hook框架

46 篇文章 3 订阅
12 篇文章 0 订阅

当程序开发到一定程度的时候,我们不可避免的遇到针对方法进行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的方法查找过程:

  1. 如果类本身能够找到方法,比如静态方法,那么就直接执行,否则进行2
  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,希望能够对读者有所帮助!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值