热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

自定义iOS注解

1.项目背景注解源自于java,是在JDK5时引入的新特性,注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的

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、.target-action 可以被hook,自定义的其他方法hook会失败,但经销商项目中使用时暂未发现该问题
    III. 读取plist文件 \\较麻烦
    IV. 单例强行处理 \\太low
    结合上述方案,我们选择在OC类中添加swift类方法的注解
  3. aspects hook  有缺陷
    1、不能多次hook 在基类中被继承的方法
    2、hook 本方法 在block中不支持延时处理 非前插后插  (已解决)

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

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

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

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

       5、swift target-action 可以被hook,自定义的其他方法hook有可能会失败,但暂未在经销商项目中复现。

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

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


推荐阅读
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
  • 本文将带您了解Cocos家族的不同版本和分支,特别是Cocos Creator的发展历程及其核心特性,帮助初学者快速入门。 ... [详细]
  • 本文详细介绍了macOS系统的核心组件,包括如何管理其安全特性——系统完整性保护(SIP),并探讨了不同版本的更新亮点。对于使用macOS系统的用户来说,了解这些信息有助于更好地管理和优化系统性能。 ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • andr ... [详细]
  • Hadoop入门与核心组件详解
    本文详细介绍了Hadoop的基础知识及其核心组件,包括HDFS、MapReduce和YARN。通过本文,读者可以全面了解Hadoop的生态系统及应用场景。 ... [详细]
  • 配置Windows操作系统以确保DAW(数字音频工作站)硬件和软件的高效运行可能是一个复杂且令人沮丧的过程。本文提供了一系列专业建议,帮助你优化Windows系统,确保录音和音频处理的流畅性。 ... [详细]
  • C语言入门精选教程与书籍推荐
    本文精选了几本适合不同水平学习者的C语言书籍,从基础入门到进阶提高,帮助读者全面掌握C语言的核心知识和技术。 ... [详细]
  • 我在项目中发现设置了 GCC_NO_COMMON_BLOCKS = NO 的配置项,位于 Apple LLVM 编译器 3.1 的代码生成设置中。 ... [详细]
  • 如何在Notepad++中执行Python代码
    Notepad++是一款功能丰富的文本编辑器,不仅支持多种编程语言的语法高亮显示,还提供了便捷的代码执行功能。本文将详细介绍如何在Notepad++中配置并运行Python代码。 ... [详细]
  • 如何高效创建和使用字体图标
    在Web和移动开发中,为什么选择字体图标?主要原因是其卓越的性能,可以显著减少HTTP请求并优化页面加载速度。本文详细介绍了从设计到应用的字体图标制作流程,并提供了专业建议。 ... [详细]
  • RecyclerView初步学习(一)
    RecyclerView初步学习(一)ReCyclerView提供了一种插件式的编程模式,除了提供ViewHolder缓存模式,还可以自定义动画,分割符,布局样式,相比于传统的ListVi ... [详细]
  • 使用Python在SAE上开发新浪微博应用的初步探索
    最近重新审视了新浪云平台(SAE)提供的服务,发现其已支持Python开发。本文将详细介绍如何利用Django框架构建一个简单的新浪微博应用,并分享开发过程中的关键步骤。 ... [详细]
  • 本文详细介绍了如何在Ubuntu系统中下载适用于Intel处理器的64位版本,涵盖了不同Linux发行版对64位架构的不同命名方式,并提供了具体的下载链接和步骤。 ... [详细]
  • 汇编语言等号伪指令解析:探究其陡峭的学习曲线
    汇编语言以其独特的特性和复杂的语法结构,一直被认为是编程领域中学习难度较高的语言之一。本文将探讨汇编语言中的等号伪指令及其对初学者带来的挑战,并结合社区反馈分析其学习曲线。 ... [详细]
author-avatar
小白也坚强_177
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有