切面编程(AOP)通过预编译和运行期间的动态代理来实现程序功能的统一维护。AOP将程序的关注点分离,通过插入代码实现横切关注点,从而隔离业务逻辑的各个部分,降低耦合度,提高可维护性和可重用性,进而提升开发效率。
在AOP中,定义切面(aspect)封装横切关注点,无需直接修改业务逻辑代码。这种方式在不修改源代码的前提下添加功能,常用于剥离业务代码和非业务代码,如参数校验、日志记录、性能统计等,以实现更好的代码解耦。
HarmonyOS主要通过插桩机制来实现切面编程,并提供了Aspect,该类包含addBefore()、addAfter()和replace()接口。这些接口允许在运行时对类方法进行前置插桩、后置插桩和替换实现,为开发者提供了更加灵活的操作方式。在具体业务场景中,不同的需求可能需要不同的埋点功能和日志记录。通过调用addBefore()、addAfter()和replace()接口,可以实现对类方法的各种功能增强和定制化需求:
本文将介绍对应接口的基本原理,并具体说明如何在上述业务场景中使用运行时插桩接口对类方法进行埋点和添加日志。
addBefore()、addAfter()、replace()接口的原理基于class的ECMAScript语义,即类的静态方法是类的属性,类的实例方法是类的原型对象(prototype)的属性。

类的实例有一个属性__proto__(称为原型),它是指向类的prototype的引用,如图2所示。实例调用方法时,会通过__proto__找到类的prototype,再在prototype中找到方法并执行。类的原型对象prototype被所有实例共享,因此修改原型对象中的方法会影响所有实例。

原型对象也有原型_proto_。类的继承通过原型实现。实例方法调用时,会在原型链上查找方法,找到后执行调用。具体如图3所示。

插桩和替换的操作是将回调参数与原方法组合成新函数,再用新函数替换原方法。具体如图4所示。

addBefore: 类方法前插桩
// Instrument before class method execution
static addBefore(targetClass, methodName, isStatic, before): void {
let target = isStatic ? targetClass : targetClass.prototype;
let origin = target[methodName];
// Define a new function that first executes "before" and then executes the old method.
let newFunc = function (...args) {
before(this, ...args);
return origin.bind(this)(...args);
}
// Replace the method with a new function
target[methodName] = newFunc;
} addAfter: 类方法后插桩
// Instrument after class method execution
static addAfter(targetClass, methodName, isStatic, after) : void {
let target = isStatic ? targetClass : targetClass.prototype;
let origin = target[methodName];
// Define a new function that first executes the old method, then executes after.
let newFunc = function (...args) {
let ret = origin.bind(this)(...args);
return after(this, ret, ...args);
}
// Replace the method with a new function
target[methodName] = newFunc;
} replace: 替换类方法
static replace(targetClass, methodName, isStatic, instead) : void {
let target = isStatic ? targetClass : targetClass.prototype;
// Define a new function that only executes "instead" inside.
let newFunc = function (...args) {
return instead(this, ...args);
}
// Replace the method with a new function
target[methodName] = newFunc;
} 业务开发团队忽略了参数的合法性,运维团队发现了这一问题,需要紧急修复。由于让业务开发团队修改的流程较为复杂,运维团队决定临时通过插桩的方式为方法添加参数合法性校验的逻辑。
业务开发团队开发了基础能力模块,并将能力封装在类A中。在应用集成基础能力模块时,发现需要对类A的方法加入参数校验逻辑,以应对非法输入。因此,运维团队决定在临时修复过程中,通过插桩的方式临时添加参数合法性校验逻辑,以确保系统的稳定性和安全性。
在addBefore()接口的回调参数中,可以访问原方法的参数,因此可以利用addBefore()在方法前插入参数校验逻辑。这是运行时行为,需要在addBefore()执行后生效,通常在应用入口调用该接口进行插桩。
// baseAbility.ts
export class A {
getElementByIndex<T>(arr: Array<T>, idx: number): T {
return arr[idx];
}
} // index.ets
import { util } from '@kit.ArkTS';
import { A } from '../components/baseAbility';
@Entry
@Component
struct Index {
build() {
// UI code
}
}
util.Aspect.addBefore(A, 'getElementByIndex', false,
// Check the parameters
(instance: A, arr: Object, idx: number) => {
if (!(arr instanceof Array)) {
throw Error('arg arr is expected to be an array');
}
if (!(Number.isInteger(idx) && idx >= 0)) {
throw Error('arg idx is expected to be a non-negative integer');
}
if (idx >= arr.length) {
throw Error('arg idx is expected to be smaller than arr.length');
}
});
// The original method is executed
let buffer: Array<number> = [1, 2, 3, 5];
let that = new A();
that.getElementByIndex(buffer, -1);
that.getElementByIndex(buffer, 5);
that.getElementByIndex(123 as Object as Array<number>, 5) 在性能分析或调试场景中,性能管控团队需统计应用运行过程中调用某个方法的次数或执行时间。如果要求业务开发团队临时修改源代码并重新打包,效率低下且业务团队可能无法提供足够的人力支持。因此,性能管控团队需临时插入一个插桩,以获取所需信息。
在方法前插入调用次数自增的逻辑,addBefore()可用于统计调用次数。执行时间统计时,使用addBefore()记录开始时间,addAfter()记录结束时间。
利用闭包变量或覆盖每次执行的变量存储执行次数和时间。
// somePackage.ets
export class Test {
foo(){}
} // index.ets
import { util } from '@kit.ArkTS';
import { Test } from '../components/somePackage';
@Entry
@Component
struct Index {
build() {
// UI code
}
}
util.TextDecoder.toString();
// increment call count
let countFoo = 0;
util.Aspect.addBefore(Test, 'foo', false, () => {
countFoo++;
});
// Invoke and print logs
new Test().foo();
console.log('countFoo = ', countFoo);
// [LOG]: "countFoo = ", 1
let a = new Test();
a.foo()
console.log('countFoo = ', countFoo);
// [LOG]: "countFoo = ", 2
function bar(a: Test) {
a.foo();
console.log('countFoo = ', countFoo);
new Test().foo();
console.log('countFoo = ', countFoo);
}
bar(a);
// [LOG]: "countFoo = ", 3
// [LOG]: "countFoo = ", 4
console.log('countFoo = ', countFoo);
// [LOG]: "countFoo = ", 4 // somePackage1.ets
export class Test1 {
doSomething() { // instance method
// ...
}
static test() { // static method
// ...
}
} // index2.ets
import {util} from '@kit.ArkTS';
import { Test1 } from '../components/somePackage1';
@Entry
@Component
struct Index {
build() {
// UI code
}
}
// Print the time before and after the insertion, and encapsulate the insertion action into an interface
function addTimePrinter(targetClass: Object, methodName: string, isStatic: boolean) {
let t1 = 0;
let t2 = 0;
util.Aspect.addBefore(targetClass, methodName, isStatic, () => {
t1 = new Date().getTime();
});
util.Aspect.addAfter(targetClass, methodName, isStatic, () => {
t2 = new Date().getTime();
console.log("t2---t1 = " + (t2 - t1).toString());
});
}
// Add the logic for printing the execution time to the doSomething instance method of Test
addTimePrinter(Test1, 'doSomething', false);
new Test1().doSomething()
// Add the logic for printing the execution time to the test static method of the test
addTimePrinter(Test1, 'test', true);
Test1.test()
不推荐使用该方式统计多个线程执行的函数,以免造成方法次数变量或执行时间变量的写冲突。
// someThirdParty.ets
export class WebHandler {
getWebAddrHttps(): string {
let ret = 'http';
// ...
return ret;
}
} // index.ets
import {util} from '@kit.ArkTS';
import { WebHandler } from '../components/someThirdParty';
@Entry
@Component
struct Index {
build() {
// UI code
}
}
util.Aspect.addAfter(WebHandler, 'getWebAddrHttps', false, (instance: WebHandler, ret: string) => {
if (!ret.startsWith('https')) {
throw Error('Handler\'s method \'getWebAddrHttps\': return value does not start with \'https\'');
}
// Verification is correct, remember to return the original method's return value.
return ret;
});
new WebHandler().getWebAddrHttps(); // somePackage.ets
export class Person {
name: string;
age: number;
constructor(n: string, a: number) {
this.name = n;
this.age = a;
}
getInfo(): string {
return 'name: ' + this.name + ', ' + 'age: ' + this.age.toString();
}
} // index.ets
import {util} from '@kit.ArkTS';
import { Person } from '../components/somePackage';
@Entry
@Component
struct Index {
build() {
// UI code
}
}
// Verify the name and age members
util.Aspect.addBefore(Person, 'getInfo', false, (instance: Person) => {
if (instance.name.length == 0) {
throw Error('empty name');
}
if (instance.age < 0) {
throw Error('invalid age');
}
});
new Person('c', -1).getInfo(); replace()的第四个参数是回调函数,该回调函数会代替原方法执行。回调函数的第一个参数是this对象,从第二个参数开始依次是原方法的参数。通过replace()的回调参数,可以获取原方法的所有执行上下文,从而实现对原方法执行过程的全面控制和定制。
export class Test2 {
foo(arg: string) {
console.log(arg);
}
} // index.ets
import {util} from '@kit.ArkTS';
import { Test2 } from '../components/somePackage';
@Entry
@Component
struct Index {
build() {
// UI code
}
}
new Test2().foo('123');
// [LOG]: "123"
// replace the original method
util.Aspect.replace(Test2, 'foo', false, (instance: Test2, arg: string) => {
console.log(arg + ' __replaced implementation');
});
new Test2().foo('123');
// [LOG]: "123 __replaced implementation" 利用replace()接口以子类为targetClass参数,替换子类方法的实现。
这一操作的底层原理是基于JavaScript的原型链机制。通过replace()接口,新函数会被放置到子类的原型上。这样当执行子类的方法时,原型链机制会首先在子类原型上查找新函数来执行,而不会执行父类的方法,也不会影响到父类的其他子类。
// base.ets
export class Base {
foo() {
console.log('hello');
}
} // child1
import {Base} from './base';
export class Child1 extends Base {} // child.ets
import {Base} from './base';
export class Child extends Base {
// Inherit the getCurrentLocation method of the parent class
} import { geoLocationManager } from "@kit.LocationKit";
export class Base1 {
getCurrentLocation() {
return geoLocationManager.getCurrentLocation();
}
} // child.ets
import {Base1} from "./base";
export class Child extends Base1 {
// inherit the getCurrentLocation method from the parent class
} // index.ets
import { util } from '@kit.ArkTS';
import { geoLocationManager } from "@kit.LocationKit";
import { Child } from '../components/child';
@Entry
@Component
struct Index {
build() {
// UIcode
}
}
let cached_location: Object | undefined;
let time: number | undefined;
util.Aspect.replace(Child, 'getCurrentLocation', false, () => {
let newTime = new Date().getTime();
// Real-time location can be called at most once per minute.
if (!cached_location || !time || newTime - time > 60000) {
time = newTime;
cached_location = geoLocationManager.getCurrentLocation();
}
// Return cached location information
return cached_location;
});
new Child().getCurrentLocation()
访问设备的位置信息,必须申请以下权限,并且获得用户授权:
具体方法可参考向用户申请授权。
在EntryAbility的onCreate()方法中,对UIAbilityContext类的startAbility()方法进行插桩,以获取Want参数的bundleName属性。由于UIAbilityContext是系统提供的类且没有导出,无法直接导入,因此可以通过EntryAbility的context成员(该成员从UIAbility继承而来)获取UIAbilityContext类对象,然后在onCreate()方法中完成插桩操作。这样可以实现对目标方法的监控和定制,以满足特定需求。
// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { util } from '@kit.ArkTS';
// Obtain the target package name
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'testTag', '%{public}s', ' onCreate');
util.Aspect.addBefore(this.context.constructor, 'startAbility', false,
(instance: Object, wantParam: Want) => {
console.info('UIAbilityContext startAbility: want.bundleName is ' + wantParam.bundleName);
});
this.context.startAbility(want, () => {})
}
// Other related configurations
// ...
} 系统SDK的接口暴露有以下两种方式:
对于第一种场景,可以接将类名、类方法名传给util.Aspect。如果类未导出,仅有类的示例,可参考场景7,通过实例的构造方法返回传参。
对于第二种场景,应用可以将该接口自行包装成类,再使用util.Aspect进行插桩。也可以自行封装入参为namespace的Aspect接口供插桩使用。
// WindowWrap.ets
import { window } from '@kit.ArkUI';
import { util } from '@kit.ArkTS';
export class WindowWrap {
private static instance: WindowWrap = new WindowWrap();
private constructor() {
}
public static getInstance() {
return WindowWrap.instance;
}
public createWindow() {
let config: window.Configuration = {
name: 'test',
windowType: window.WindowType.TYPE_DIALOG
}
window.createWindow(config, () => {
// do something
})
}
}
util.Aspect.addBefore(WindowWrap, 'createWindow', false, (instance: WindowWrap): void => {
console.info('addBefore createWindow');
}) // NameSpaceAspect.ts
type AnyType = Object | null | undefined;
export class NameSpaceAspect {
static addBefore(namespace: ESObject, methodName: string, before: Function): void {
let oldFunc = (namespace as Record<string, Function>)[methodName];
let isAsync = oldFunc.constructor.name === 'AsyncFunction';
if (!isAsync) {
let newFunc = function (...args: AnyType[]): AnyType {
before(...args);
return oldFunc.bind(this)(...args);
};
(namespace as Record<string, Function>)[methodName] = newFunc;
} else {
let newFunc = async function (...args: AnyType[]): Promise<AnyType> {
before(...args);
return await oldFunc.bind(this)(...args);
};
(namespace as Record<string, Function>)[methodName] = newFunc;
}
}
static addAfter(namespace: ESObject, methodName: string, after: Function): void {
let oldFunc = (namespace as Record<string, Function>)[methodName];
let isAsync = oldFunc.constructor.name === 'AsyncFunction';
if (!isAsync) {
let newFunc = function (...args: AnyType[]): AnyType {
let ret1 = oldFunc.bind(this)(...args);
let ret2 = after(this, ret1, ...args);
return ret2;
};
(namespace as Record<string, Function>)[methodName] = newFunc;
} else {
let newFunc = async function (...args: AnyType[]): Promise<AnyType> {
let ret1 = oldFunc.bind(this)(...args);
let ret2 = after(this, ret1, ...args);
return ret2;
};
(namespace as Record<string, Function>)[methodName] = newFunc;
}
}
static replace(namespace: ESObject, methodName: string, instead: Function): void {
let oldFunc = (namespace as Record<string, Function>)[methodName];
let isAsync = oldFunc.constructor.name === 'AsyncFunction';
if (!isAsync) {
let newFunc = function (...args: AnyType[]): AnyType {
return instead(...args);
};
(namespace as Record<string, Function>)[methodName] = newFunc;
} else {
let newFunc = async function (...args: AnyType[]): Promise<AnyType> {
return await instead(...args);
};
(namespace as Record<string, Function>)[methodName] = newFunc;
}
}
} // WindowAop.ts
import { window } from '@kit.ArkUI';
import { NameSpaceAspect } from './NameSpaceAspect';
NameSpaceAspect.addBefore(window, 'createWindow', () => {
console.info('addBefore createWindow');
});
如使用Aspect接口报以下错误信息:
Error name:TypeError
Error message:Cannot assign to read only property
原因为被插桩方法的属性描述符的writable字段设置为false,该方法无法进行插桩。
HarmonyOS 7.0版本开始,系统SDK接口的属性描述符的writable字段默认设置为true。
class Test {
foo() {}
}
util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => {
instance.foo();
});
// Infinite recursion
new Test().foo(); 如果有需要调用原方法的场景,实现方法参考如下示例。
class Test {
foo() {}
}
// Save the original method implementation first
let originalFoo = new Test().foo;
util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => {
// If the original method does not use this, you can directly call the originalFoo () method.
// If this is used in the original method, bind should be used to bind the instance, but there will be a compilation warning originalFoo.bind (instance);
}); // Counterexample
@Component
struct Index {
foo() {}
build() {};
}
util.Aspect.replace(Index, 'foo', false, () => {
// ...
});
util.Aspect.replace(Index, 'build', false, () => {
// ...
}); // Example of unrecommended usage:
class Test {
foo(): string {
return 'hello';
}
}
util.Aspect.addAfter(Test, 'foo', false, () => {
console.log('execute foo');
});
// The correct usage example is as follows:
class Test1 {
foo(): string {
return 'hello';
}
}
util.Aspect.addAfter(Test1, 'foo', false, (instance: Test, ret: string) => {
console.log('execute foo');
return ret; // Return the return value of the original method
})
如果类方法的属性描述符的writable字段为false,比如冻结(freeze) 的场景, 则不能调用接口操作这个类方法。
方法的属性描述符的writable字段默认为true。