纯血鸿蒙TS应用层NovaHook方案【附源码】
1.AOP概述1.1.AOP概念
AOP(Aspect-Oriented Programming)是面向切面编程的缩写,是一种通过预编译和运行时动态代理实现步伐功能增强的编程范式。它是对OOP(面向对象编程)的重要补充,专门用于解决横切关注点(Cross-Cutting Concerns)问题。
1.2.核心原理
关键概念阐明:
[*]切面(Aspect):切面界说了在何时、何地、以何种方式“切入”到业务代码中。每个切面都可以包含多个切点和通知,以决定切面在应用中的举动方式。
[*]切入点(Pointcut):通过某种方式界说需要拦截的毗连点
[*]通知(Advice):在毗连点执行的增强逻辑(前置/后置/返回/围绕/非常)
[*]毗连点(JoinPoint):步伐执行中的可拦截点(方法调用、非常处理等)
[*]织入(Weaving):将切面逻辑应用到目的对象生成代理对象的过程
[*]目的对象(TargetObject):被增强的原始业务对象
[*]代理对象(ProxyObject):增强后的生成对象
关系流程:
切面通过切入点匹配目的对象的毗连点,将通知逻辑织入到这些毗连点,最终生成代理对象实现功能增强。
代码执行流程:
注意:执行流程过程中抛非常会反馈给非常通知
2.官方Aspect方案
HarmonyOS主要通过插桩机制来实现切面编程,Aspect类用于封装提供切面能力(Aspect Oriented Programming,简写AOP)的接口,这些接口可以用来对类方法进行前后插桩或者替换实现,包括addBefore、addAfter和replace接口。
参数名范例必填阐明targetClassObject是指定的类对象或定名空间。methodNamestring是指定的方法名,不支持read-only方法。isStaticboolean是指定的原方法是否为静态方法,true表现静态方法,false表现实例方法。before/after/insteadFunction是要插入的函数对象。函数有参数,则第一个参数是this对象(若isStatic为true,则为类对象即targetClass;若isStatic为false,则为调用方法的实例对象),别的参数是原方法的参数。函数也可以无参数,无参时不做处理。 缺点:
1.不支持hook特定函数的执行
2.不支持hook多target场景
3.不支持hook read-only即writeable为false的target
依靠履历来判定,我们的hook需求绝大部分都不支持
3.NovaHook方案
NovaHook是Aspect的增强,真正意义上做到了为所欲为的hook!它支持Aspect的所有能力,同时也解决了Aspect的不支持read-only方法及多target场景等问题,对顶层只开放一个函数,做到了API统一,解决了调用复杂的问题。
3.1.专用名词阐明
名词表明普通hook即writable为true的api,可通过调用NovaHook.hook方法实现插件hook必须通过hvigor插件hook的api 3.2.核心技术
[*]类/对象的数据属性
[*]原型模式
[*]Hvigor插件开发
[*]HAP包编译构建流程
[*]正则表达式
[*]组件化
感兴趣的可自行阅读源码
注意,NovaHook与Java的动态代理差别,它是静态织入,在编译期间将切面逻辑织入到目的代码中。
3.3.怎样集成
直接clone项目即可,Demo有测试用例。
novahook里存放hook核心能力
proxy里存放代理类
entry里的hvigorfile.ts里存放自界说插件的源码
3.3.1.普通hook集成
通过ohpm长途集成:
所需要依赖的模块,比如entry的oh-package.json5
"dependencies": {
"novahook":"^1.0.0",
}
同步项目即可。
3.3.2.插件hook集成
通过ohpm长途集成:
[*]在项目根目次的hvigor的hvigor-config.json5
"dependencies": {
"com.github.novahook": "1.0.2"
}
[*]在所需依赖的模块,比如entry的hvigorfile.ts
import { novaHookPlugin} from 'com.github.novahook'
export default {
system: xxx,/* Built-in plugin of Hvigor. It cannot be modified. */
plugins: /* Custom plugin to extend the functionality of Hvigor. */
}
[*]创建HookPluginConfig.txt(必须如许定名)
编写hook规则,可参考demo的entry目次下的HookPluginConfig.txt
同步项目即可。
3.4.怎样使用
3.4.1.普通hook
novahook实现了普通hook的核心能力
为方便二次开发者开发,此模块对外仅开放一个api,实现hook能力
export class NovaHook {
static hookMethod(options: HookOptions): void
}
参数阐明
/**
* hook配置项
*/
export interface HookOptions {
/**
* 被hook的目标
*/
target: any
/**
* 被hook的成员:方法|属性名
*/
member: string
/**
* 被hook的子方法|子属性名
* 优先级第二
*/
childMember?: string
/**
* 是否hook高阶函数
* 优先级最高
*/
isHookHighFunction?: boolean
/**
* 异常通知
*/
exceptionAdvice?: (msg: string) => void
/**
* 前置通知
*/
beforeAdvice?: (context: any, ...args) => any | void
/**
* 环绕通知
*/
replaceAdvice?: (context: any, origin: Function, ...args) => any
/**
* 后置通知
*/
afterAdvice?: (context: any, ...args) => void
/**
* 返回通知
*/
returnAdvice?: (context: any, result: any, ...args) => void
}
参数阐明:
target、member、childMember、isHookHighFunction 构成切入点,共同确定了我们要代理哪些范例的函数
exceptionAdvice、beforeAdvice、replaceAdvice、afterAdvice、returnAdvice构成五种通知,共同确定了我们hook住源函数后要做什么增强
我们只需要这些就够了,至于织入被代理对象细节已被框架内部实现,框架使用者无需关心。
3.4.2.插件hook
[*]创建自界说代理类
发起将Proxy类独立成proxylib,proxylib是与hook业务相关的,代理类集中存放的地方,之以是单独抽离,一方面是为了解耦、另一方面被导包引用的地方可以做到包名一致,为代码插装提供了便利!
发起,源码级hook遵循源码名Proxy的定名规范,内部代码的成员变量及函数也与源码保持一致
[*]hookPluginConfig配置文件
hookPluginConfig.txt,它是插件hook前扫包的规则限定
-hook 扫包路径
-keep 不hook的路径
-replace 要被替换的具体api,格式如下:
^-replace\s+([^\s]+)\s+\[(import.+from.+)\]\s+([^\s]+)\s*(?:\[(.*)\])?
例子可参考源Demo
[*]执行编译打包assembleHap任务即可
下面结合具体场景来阐明使用步骤
4.NovaHook典范使用场景举例
4.1.hook通例api场景
[*]界说源Target
// NormalApiTest.ts
/**
* 普通api
*/
export class NormalApiTest {
/**
* 静态函数
*/
static staticMethod(name: string): string {
Logger.d(`execute static method ${name}`)
return name
}
/**
* 非静态函数
*/
public normalMethod(name: string, age: number): string {
Logger.d(`execute normal method name is ${name},age is ${age}`)
return `${name}-${age}`
}
}
[*]hook源Target并调用源Target函数
4.1.1.静态函数
// Hook.ts
export function hookStaticMethod() {
Logger.d('=======================hook静态方法=========================')
// 常规hook静态方法
NovaHook.hookMethod({
target: NormalApiTest,
member: 'staticMethod',
beforeAdvice: (context: any, args: any[]) => {
Logger.d(`NormalApiTest staticMethod before, context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
replaceAdvice: (context: any, origin: Function, args: any[]) => {
let result = origin(args)
Logger.d(`NormalApiTest staticMethod replace, context exist: ${context != null &&
context != undefined},args is ${args.toString()}`)
return result
},
afterAdvice: (context: any, args: any[]) => {
Logger.d(`NormalApiTest staticMethod after,context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
returnAdvice: (context: any, result: any, args: any[]) => {
Logger.d(`NormalApiTest staticMethod return,context exist: ${context != null && context != undefined},
args is ${args.toString()},result is ${result}`)
}
})
NormalApiTest.staticMethod('test')
}
4.1.2.非静态函数
// Hook.ts
export function hookCommonMethod() {
// 常规hook非静态方法
Logger.d('===================hook非静态方法=============================')
NovaHook.hookMethod({
target: NormalApiTest,
member: 'normalMethod',
beforeAdvice: (context: any, ...args) => {
Logger.d(`NormalApiTest normalMethod before, context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
replaceAdvice: (context: any, origin: Function, args: any[]) => {
let result = origin.apply(this, [...args])
Logger.d(`NormalApiTest normalMethod replace, context exits: ${context != null &&
context != undefined},args is ${args.toString()}`)
return result
},
afterAdvice: (context: any, args: any[]) => {
Logger.d(`NormalApiTest staticMethod after,context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
returnAdvice: (context: any, result: any, args: any[]) => {
Logger.d(`NormalApiTest staticMethod return,context exist: ${context != null && context != undefined},
args is ${args.toString()},result is ${result}`)
}
})
new NormalApiTest().normalMethod('test', 12)
}
4.2.hook高阶函数的执行场景
// FunctionApiTest.ts
// 定义模拟源Target FunctionApiTest.ts
export class FunctionApiTest {
static functionTest(func: (value1: number, value2: number) => number): number {
Logger.d(`func 函数执行前`)
let result = func(10, 20)
Logger.d(`func 函数执行后`)
return result * 10;
}
}
// Hook.ts
// hook高阶函数
export function hookHighFunction() {
// hook高阶函数的执行场景
Logger.d('===================hook高阶函数的执行场景=============================')
NovaHook.hookMethod({
target: FunctionApiTest,
member: 'functionTest',
isHookHighFunction: true,
beforeAdvice: (context: any, ...args) => {
Logger.d(`FunctionApiTest functionTest before, context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
replaceAdvice: (context: any, origin: Function, args: any[]) => {
let result = origin.apply(context, args)
Logger.d(`FunctionApiTest functionTest replace, args is ${args.toString()}`)
return result - 1
},
afterAdvice: (context: any, args: any[]) => {
Logger.d(`FunctionApiTest functionTest after,context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
returnAdvice: (context: any, result: any, args: any[]) => {
Logger.d(`FunctionApiTest functionTest return,context exist: ${context != null && context != undefined},
args is ${args.toString()},result is ${result}`)
}
})
FunctionApiTest.functionTest((value1: number, value2: number) => {
let result = value1 + value2
Logger.d(`FunctionApiTest original high function execute,10+20= is ${result}`)
return result
})
}
4.3.hook多target场景
// 定义模拟源Target MultiTargetApiTest.ts
export class MultiTargetApiTest {
public name: string
constructor(builder) {
if (arguments.length <= 0 || builder == undefined) {
builder = new MultiTargetApiTest.Builder()
}
this.name = builder.name
}
static get Builder() { // 静态属性访问器
class Builder {
public name: string
constructor() {
this.name = ''
}
// 注意内部包含this对Builder自身的引用,所以,当函数调用时不能直接(),推荐apply
build(): MultiTargetApiTest {
return new MultiTargetApiTest(this);
}
setName(name): Builder {
this.name = name
return this
}
}
return Builder
}
}
// Hook.ts
// hook多target
export function hookMultiTarget() {
// hook多target场景
Logger.d('===================hook多target场景=============================')
NovaHook.hookMethod({
target: MultiTargetApiTest,
member: 'Builder',
childMember: 'build',
beforeAdvice: (context: any, ...args) => {
Logger.d(`MultiTargetApiTest Builder.build before, context exist: ${context != null && context != undefined},
args exist: ${args != null && args != undefined}`)
const builderContext = context as InstanceType<typeof MultiTargetApiTest.Builder>
builderContext.setName('1111')
},
replaceAdvice: (context: any, origin: Function, args: any[]) => {
const builderContext = context as InstanceType<typeof MultiTargetApiTest.Builder>
Logger.d(`MultiTargetApiTest Builder.build replace, before name is ${builderContext.name}`)
Logger.d(`MultiTargetApiTest Builder.build replace, args exist: ${args != null && args != undefined}}`)
let result = origin.apply(context, args) as InstanceType<typeof MultiTargetApiTest>
result.name = '22222'
return result
},
afterAdvice: (context: any, args: any[]) => {
Logger.d(`MultiTargetApiTest Builder.build after,context exist: ${context != null && context != undefined},
args exist: ${args != null && args != undefined}`)
},
returnAdvice: (context: any, result: any, args: any[]) => {
const multiTargetApiTest = result as InstanceType<typeof MultiTargetApiTest> // MultiTargetApiTest.Builder
Logger.d(`MultiTargetApiTest Builder.build return,context exist: ${context != null && context != undefined},
args exist: ${args != null && args != undefined},result's name is ${multiTargetApiTest.name}`)
}
})
let multiTargetApiTest1 = new MultiTargetApiTest
.Builder()
.setName('testBuilder1')
.build()
Logger.d(`multiTargetApiTest name is ${multiTargetApiTest1.name}`)
let multiTargetApiTest2 = new MultiTargetApiTest
.Builder()
.setName('testBuilder2')
.build()
Logger.d(`multiTargetApiTest name is ${multiTargetApiTest2.name}`)
}
4.4.hook仅修改源Target实际参数场景
// ModifyParamApiTest
export class ModifyParamApiTest {
static changedParam(param: string) {
Logger.d(`testParam param is ${param}`)
}
}
// Hook.ts
export function hookModifyRealParam() {
Logger.d('===================仅修改实际参数场景=============================')
NovaHook.hookMethod({
target: ModifyParamApiTest,
member: 'changedParam',
exceptionAdvice: (msg) => {
Logger.e(`抛异常了!${msg}`)
},
beforeAdvice: (context: any, args: any[]) => {
Logger.d(`ModifyParamApiTest readOnlyTest before, context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
return ['已被修改:hook-test']
}
})
ModifyParamApiTest.changedParam('test')
}
4.5.hook函数的writeable为false的的场景
此hook方式如果采取普通hook方式会抛非常,必须要插件hook
界说源Target
// ReadOnlyApiTest.ts
export class ReadOnlyApiTest {
/**
* 只读,测试代码插装hook
*/
readOnlyTest(name: string) {
Logger.d(`read only method,name is ${name}`)
}
readOnlyTest2(name: string) {
Logger.d(`read only method,name is ${name}`)
}
init() {
Object.defineProperty(Object.getPrototypeOf(this), 'readOnlyTest', {
writable: false,
configurable: false,
value: this.readOnlyTest
})
Object.defineProperty(Object.getPrototypeOf(this), 'readOnlyTest2', {
writable: false,
configurable: false,
value: this.readOnlyTest2
});
}
}
export let readOnlyApiTest = new ReadOnlyApiTest()
4.5.1.普通hook
// Hook.ts
export function hookThrowException() {
// hook read-only的target的场景,异常通知
Logger.d('===================hook read-only的target的场景,异常通知=============================')
readOnlyApiTest.init()
NovaHook.hookMethod({
target: ReadOnlyApiTest,
member: 'readOnlyTest2',
exceptionAdvice: (msg) => {
Logger.e(`抛异常了!${msg}`)
},
beforeAdvice: (context: any, args: any[]) => {
Logger.d(`ReadOnlyApiTestProxy readOnlyTest2 before, context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
replaceAdvice: (context: any, origin: Function, args: any[]) => {
let result = origin(args)
Logger.d(`ReadOnlyApiTestProxy readOnlyTest2 replace, args is ${args.toString()}`)
return result
},
afterAdvice: (context: any, args: any[]) => {
Logger.d(`ReadOnlyApiTestProxy readOnlyTest2 return,context exist: ${context != null && context != undefined},
args is ${args.toString()}`)
},
returnAdvice: (context: any, result: any, args: any[]) => {
Logger.d(`ReadOnlyApiTestProxy readOnlyTest2 return,context exist: ${context != null && context != undefined},
args is ${args.toString()},result is ${result}`)
}
})
readOnlyApiTest.readOnlyTest2("test")
}
4.5.2.插件hook
[*]在proxylib里界说ReadOnlyApiTestProxy.ts
// 1.定义ReadOnlyApiTestProxy
export class ReadOnlyApiTestProxy {
/**
* 读写proxy,代理只读为读写
*/
readWriteTest(name: string) {
hilog.debug(0, 'Aspect_Plus', `read and write method,name is ${name}`)
}
}
export let readOnlyApiTestProxy = new ReadOnlyApiTestProxy()
[*]hookPluginConfig.txt配置hook规则实现替换
-hook ./src/main/ets
-keep ./src/main/ets/hook
-keep ../proxylib/src/main/ets/proxy
-replace readOnlyApiTest.readOnlyTest( readOnlyApiTestProxy.readWriteTest(
3.hook并调用源函数
// HookReadOnlyApiTest.ts
import { readOnlyApiTest } from "../hook/ReadOnlyApiTest"
export function hookReadOnlyApi(){
readOnlyApiTest.readOnlyTest("test")
}
// Hook.ts
export function hookReadOnlyMethod() {
Logger.d('===================hook read-only的target的场景,代码插装=============================')
readOnlyApiTest.init()
hookReadOnlyApi()
}
4.6.hook 返回值范例为源码级非export修饰的类或定名空间场景
以 hook fileIo.openSync 为例
[*]在proxylib里界说FileIoProxy.ts
import { fileIo } from "@kit.CoreFileKit"
import { Global } from "./Global";
import { FileProxy } from "./FileProxy";
/**
* FileIo代理
*/
export class FileIoProxy {
// .()无参数、apply正常,call只有一个参数,是把两个参数拼接起来,中间用","分隔开。
public static openSync(...args): FileProxy {
if (args == null || args.length < 1) {
return
}
let path: string = args
let mode = null
if (args.length == 2) {
mode = args
}
// let parentPath = path.slice(0, path.lastIndexOf('/'));
let fileName = path.slice(path.lastIndexOf('/') + 1);
// 重定向后的路径
let ROOT_PATH = Global.getInstance().uiAbilityContext.filesDir;
return fileIo.openSync(`${ROOT_PATH}/${fileName}`, mode)
}
}
由于fileIo.openSync函数返回的File,我们业务逻辑里需要这个File,而源码层面没有暴漏给我们,以是我们无法导包使用,故参考File接口界说FileProxy接口
[*]界说FileProxy接口
import { AsyncCallback } from "@kit.BasicServicesKit";
/**
* File代理
*/
export interface ZFFileProxy {
/**
* @type { number }
* @readonly
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 9
*/
/**
* @type { number }
* @readonly
* @syscap SystemCapability.FileManagement.File.FileIO
* @crossplatform
* @since 10
*/
/**
* @type { number }
* @readonly
* @syscap SystemCapability.FileManagement.File.FileIO
* @crossplatform
* @atomicservice
* @since 11
*/
readonly fd: number;
/**
* File path
*
* @type { string }
* @readonly
* @throws { BusinessError } 13900005 - I/O error
* @throws { BusinessError } 13900042 - Unknown error
* @throws { BusinessError } 14300002 - Invalid uri
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 10
*/
readonly path: string;
/**
* File name
*
* @type { string }
* @readonly
* @throws { BusinessError } 13900005 - I/O error
* @throws { BusinessError } 13900042 - Unknown error
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 10
*/
readonly name: string;
/**
* Get parent path of file.
*
* @returns { string } Return the parent path of file.
* @throws { BusinessError } 13900005 - I/O error
* @throws { BusinessError } 13900042 - Unknown error
* @throws { BusinessError } 14300002 - Invalid uri
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 11
*/
getParent(): string;
/**
* Lock file with blocking method.
*
* @param { boolean } exclusive - whether lock is exclusive.
* @returns { Promise<void> } The promise returned by the function.
* @throws { BusinessError } 13900004 - Interrupted system call
* @throws { BusinessError } 13900008 - Bad file descriptor
* @throws { BusinessError } 13900020 - Invalid argument
* @throws { BusinessError } 13900034 - Operation would block
* @throws { BusinessError } 13900042 - Unknown error
* @throws { BusinessError } 13900043 - No record locks available
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 9
*/
lock(exclusive?: boolean): Promise<void>;
/**
* Lock file with blocking method.
*
* @param { AsyncCallback<void> } callback - Return the callback function.
* @throws { BusinessError } 13900004 - Interrupted system call
* @throws { BusinessError } 13900008 - Bad file descriptor
* @throws { BusinessError } 13900020 - Invalid argument
* @throws { BusinessError } 13900034 - Operation would block
* @throws { BusinessError } 13900042 - Unknown error
* @throws { BusinessError } 13900043 - No record locks available
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 9
*/
lock(callback: AsyncCallback<void>): void;
/**
* Lock file with blocking method.
*
* @param { boolean } exclusive - whether lock is exclusive.
* @param { AsyncCallback<void> } callback - Return the callback function.
* @throws { BusinessError } 13900004 - Interrupted system call
* @throws { BusinessError } 13900008 - Bad file descriptor
* @throws { BusinessError } 13900020 - Invalid argument
* @throws { BusinessError } 13900034 - Operation would block
* @throws { BusinessError } 13900042 - Unknown error
* @throws { BusinessError } 13900043 - No record locks available
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 9
*/
lock(exclusive: boolean, callback: AsyncCallback<void>): void;
/**
* Try to lock file with returning results immediately.
*
* @param { boolean } exclusive - whether lock is exclusive.
* @throws { BusinessError } 13900004 - Interrupted system call
* @throws { BusinessError } 13900008 - Bad file descriptor
* @throws { BusinessError } 13900020 - Invalid argument
* @throws { BusinessError } 13900034 - Operation would block
* @throws { BusinessError } 13900042 - Unknown error
* @throws { BusinessError } 13900043 - No record locks available
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 9
*/
tryLock(exclusive?: boolean): void;
/**
* Unlock file.
*
* @throws { BusinessError } 13900004 - Interrupted system call
* @throws { BusinessError } 13900008 - Bad file descriptor
* @throws { BusinessError } 13900020 - Invalid argument
* @throws { BusinessError } 13900034 - Operation would block
* @throws { BusinessError } 13900042 - Unknown error
* @throws { BusinessError } 13900043 - No record locks available
* @syscap SystemCapability.FileManagement.File.FileIO
* @since 9
*/
unlock(): void;
}
[*]HookPluginConfig.txt配置hook规则实现替换
-replace fileIo.openSync( FileIoProxy.openSync(
[*]hook及调用
// HookFileApi.ts
import fileIo from "@ohos.file.fs";
import { Global } from "proxylib";
import Logger from "aoplib/src/main/com/wp/aop/utils/Logger";
export function testFile() {
let path = Global.getInstance().uiAbilityContext.cacheDir + '/test.txt'
Logger.d(`文件源路径:${path}`)
let file = fileIo.openSync(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY)
fileIo.writeSync(file.fd, '写入的测试文本a')
fileIo.closeSync(file.fd)
Logger.d(`文件路径重定向至:${file.path}`)
Logger.d('文本写入成功!')
}
// 模拟以下自定义的openSync不会被hook
// export function testFile(path: string) {
// fileIo.openSync(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY)
// }
// class fileIo {
// static openSync(path: string, mode?: number) {
// let str:string='skdfroms';
// str.split('from').trim()
// // Logger.d(`path`)
// }
// }
// Hook.ts
export function hookReturnValueNotExport() {
Logger.d('===================返回值类型为源码级非export修饰的类或命名空间场景,代码插装=============================')
testFile()
}
5.github仓库开源地址
https://github.com/MarsToken/NovaHook
6.参考资料
[*]《JavaScript高级步伐编程设计》
[*]AspectPro框架
[*]鸿蒙官方开发文档
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]