• 自定义iOS注解


    1. 项目背景

    注解源自于java,是在 JDK5 时引入的新特性,注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。注解类型定义指定了一种新的类型,一种特殊的接口类型。 在关键词 interface 前加 @ 符号也就是用 @interface 来区分注解的定义和普通的接口声明。

    注解的好处:

    I.减少重复代码的书写,相同逻辑统一处理,降低出错率

    II.复杂逻辑清晰化

    III.降低代码耦合

    但是在iOS中并没有注解的概念,鉴于注解的这些好处,就有了自定义iOS注解的想法。

    2. 注解方案的实现思路

    要模拟注解的过程,需要解决:

    1. 不影响以前有的业务。

    2. 在被注解的源代码实现里面能方便的获取注解内容,可以理解为被注解的代码,在编译期间能自动生成一段代码在被注解类里面,或者我们需要建立一个“被注解者”与“注解代码”的对应关系。

    2.1 方案1

    基于正则匹配,然后生成对应框架代码,加上自己OC实现的自定义规则,配合扫描结果关系表,来模拟注解的过程;

    阿里的OCAnnotation(仓库地址:GitHub - alibaba/OCAnnotation: A light-weighted framework empowering Objective-C with annotation feature.)就是以方这个方案实现的,工程包括看一套ruby脚本,需要让其嵌入到我们的目标工程的build script里面。在我们编译期间将执行该脚本,该脚本将会扫码我们所有的源代码,并按规则生成对应的模板OC文件,该文件为一个配置对应关系,可理解为一个hashmap字典。

    说白了就是,通过在编译期间,调用正则匹配脚本,扫码并获取注解与目标对象之间的关系(类,方法,属性)。并且把这个对应关系保存到一个字典里面去,这个字典以头文件,是ruby脚本扫码结束后自动创建的OC文件。当我们把这OC文件导入进去目标工程,在启动后马上加载进入内存,作为全局可访问数据,然后我们就可以使用该全局数据【配置表】和 我们自己定义的规则,来达到运行期间的注解校验。当然该工程有很大缺陷是,每次编译都要扫码源代码,虽然作者做了缓存,还有就是不支持framework.在组件化遍地开花的今天,这也很尴尬!

    备注:为了解决“被注解者”与“注解代码”的桥梁问题,还有一种办法是生成注解对象的类别,如当前目标是被注解者,那么久生成改其类别扩展,并导入工程预编译中。这样的“类代码插庄”,也能间接的让我们获取到类外的注解内容。当然这个也一样存在编译期间扫描代码的问题,而且如果注解多,还会增加代码量。

    2.2 方案2

    基于类似FB的编译可配置来模拟的,用到__attribute((used, section("__DATA,"#sectname" “)))

    目前beehive组件化框架也使用了此类方案[GitHub - alibaba/BeeHive: BeeHive is a solution for iOS Application module programs, it absorbed the Spring Framework API service concept to avoid coupling between modules.]

    通过参考beehive组件化框架,我们最终选择编译期配置的方案,该方案可拆分为三个部分实现:

    第一步:__attribute__机制在编译期插桩

    第二步,运行时 从Mach-o的section data段 取出数据

    第三步,针对性解析处理

    3. 项目使用指南

    以NeedLogin注解为例

    以宏定义形式,仅需在需要使用注解的方法前面添加

    @NeedLoginOCAnnotation(GlobalModuleRouter, jumpToAccreditViewController)

    swift类使用

    // 检测报告 关注按钮

    @NeedLoginAnnotation(CheckReportViewController, collectAction, CheckReportModule)

    第一个参数为类名,第二个参数为方法名,第三个参数为模块名(swift类),OC类传OC

    比如首页中跳转录入车源方法:

    如上图所示,原有代码仅可支持是否登录判断,未登录拉起登录页,已登录直接跳转录入车源。添加注解后,登录判断逻辑就不需要了,不仅支持是否登录判断,还支持未登录用户登录成功后自动跳转录入车源。

    4. 实现原理

    第一步:__attribute__机制在编译期插桩

    __attribute__是在C, C++, Objective-C语言中使用的编译指令,一般以__attribute__(xxx)的形式出现在代码中,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程。

    关于Attribute的语法描述见官方文档 Attribute Syntax:Attribute Syntax (Using the GNU Compiler Collection (GCC))

    used

    Used的作用是告诉编译器,我声明的这个符号是需要保留的。被used修饰以后,意味着即使函数没有被引用,在Release下也不会被优化。如果不加这个修饰,那么Release环境链接器会去掉没有被引用的段。具体的描述可以看gun的官方文档。

    section

    通常情况下,编译器会将对象放置于DATA段的data或者bss节中。但是,有时我们需要将数据放置于特殊的节中,此时section可以达到目的。例如,BeeHive中就把module注册数据存在__DATA数据段里面的"BeehiveMods"section中。

    section通常用于修饰全局变量。

    __attribute__的更多使用示例可参考FBTweak

    编译器提供了我们一种__attribute__((section("xxx段,xxx节")的方式让我们将一个指定的数据储存到我们需要的节当中。

    第二步,读取section中的值

    现在来了解如何将存储在特殊section中的数据读出。

    其中void initProphet()使用了__attribute__((constructor))修饰,

    constructor / destructor

    顾名思义,构造器和析构器,加上这两个属性的函数会在分别在可执行文件(或 shared library)load 和 unload 时被调用,可以理解为在 main() 函数调用前和 return 后执行:

    constructor 和 +load 都是在 main 函数执行前调用,但 +load 比 constructor 更加早一丢丢,因为 dyld(动态链接器,程序的最初起点)在加载 image(可以理解成 Mach-O 文件)时会先通知 objc runtime 去加载其中所有的类,每加载一个类时,它的 +load 随之调用,全部加载完成后,dyld 才会调用这个 image 中所有的 constructor 方法

    所以 constructor 是一个干坏事的绝佳时机:

    所有 Class 都已经加载完成,main 函数还未执行

    无需像 +load 还得挂载在一个 Class 中

    若有多个 constructor 且想控制优先级的话,可以写成 attribute((constructor(101))),里面的数字越小优先级越高,1 ~ 100 为系统保留。

    void initProphet()函数的实现体里使用了_dyld_register_func_for_add_image函数,现在看看该函数的作用。

    _dyld_register_func_for_add_image:这个函数是用来注册回调,当dyld链接符号时,调用此回调函数。在dyld加载镜像时,会执行注册过的回调函数;当然,我们也可以使用下面的方法注册自定义的回调函数,同时也会为所有已经加载的镜像执行回调:

    通过调用TTPReadConfiguration函数,我们就可以拿到之前注册到TTPNeedLogin特殊段里面的字典参数,该函数返回字符串的数组示例:[“{\”cls\":\""#cls"\",\"sel\":\""#sel"\",\"module\":\""#module"\"}, …]。

    5. 遇到问题

    1. 常量名唯一性。编译期通过attribute机制插桩是通过定义全局c++变量实现的,那么就有可能出现重名的问题,原来的想法是以类名+方法名作为变量名,但是带参的方法名有冒号(:),变量名不能存在冒号,后将类名+行号+计数 拼接成变量名, 可保持唯一性。
    2. Swift类中不能使用宏定义
      I. 再写一套Swift注解,属性包装器结合单例 判断是否aspects已hook。\\因 定义时 不执行初始化方法 放弃该方案
      II. OC类中添加swift类方法的注解 \\有问题,使用需谨慎    
      1、类名 需加模块名.    
      2、方法名 与swift中定义的名称不同,需使用生成的OC方法名,所以需要添加@objc;
      3、由于Swift 类型的成员或者方法在编译时就已经决定,添加@objc修饰符并不意味着这个方法或者属性会变成动态派发,Swift 依然可能会将其优化为静态调用。所以这里我们需要使用dynamic修饰符,使待hook的swift方法具备运行时特性。
    3. III. 读取plist文件 \\较麻烦
      IV. 单例强行处理 \\太low
      结合上述方案,我们选择在OC类中添加swift类方法的注解
    4. aspects hook  有缺陷
      1、不能多次hook 在基类中被继承的方法
      2、hook 本方法 在block中不支持延时处理 非前插后插  (已解决)

    利用NSInvocation系统类,通过原方法的方法签名、参数、调用对象,在block中再次调用原方法。

           3、仅支持hook实例方法 (已解决)

                 根据对象方法的调用机制,我们知道类方法存放在其元类中,类对象的ISA指针指向元类,就可通过类对象获取其元类,通过去hook元类的实例方法的方式,实现了对类方法的hook。

           4、 类方法与实例方法重名只能被hook一个

    注解中,虽可以实现对类方法和实例方法的区分,但是考虑到类方法与实例方法重名只能被hook一个 ,所以注解库中优先hook实例方法,若为实现实例方法,再去hook类方法,所以使用时需注意。

    注:翻阅了很多文章,具体链接也不记得了,仅以记录

  • 相关阅读:
    电机带宽的形象理解
    设计模式-代理模式
    HashMap设计原理与实现(下篇)200行带你写自己的HashMap!!!
    Java标识符和关键字
    Git 从了解到精通(2)分支管理及代码冲突和Stashing
    2023.10.10 浪费生命
    渗透测试学习day2
    竞赛选题 深度学习交通车辆流量分析 - 目标检测与跟踪 - python opencv
    【TypeScript】枚举类型和泛型的详细介绍
    java125-简单异常处理
  • 原文地址:https://blog.csdn.net/qq_25303213/article/details/126891328