Runtime在iOS开发中的实际应用

Stella981
• 阅读 352

运行时的文章一直被同学们热炒,当然现在面试中也都喜欢问道,当大伙说的头头是道时候,可到真正的项目中几乎局限只会关联对象或者MethodSwizzling奉为神剑到处挥砍,开发毕竟不能纸上谈兵,实践出真知,介绍目前在项目中runtime的具体使用,真切希望和各位同学探讨。
##1 关联对象(AssociatedObject )
  Catagory主要为已经存在的类(主要是系统类)扩展新的方法,关联对象是runtime在开发中应用的最广泛,其主要用于为Catagory的对象增加属性。
Runtime在iOS开发中的实际应用

Runtime在iOS开发中的实际应用
关于分类的介绍可以查看美团技术团队写的深入理解Objective-C:Category
####1.1 为什么catagory 无法设置属性

struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;
    char *class_name                                         OBJC2_UNAVAILABLE;
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。主要因为方法定义都在objc_class中管理的,不管如何增删方法,都不影响类实例的内存布局,创建一个对象必然会分配一块内存区域,包含了isa指针和所有的成员变量。假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。
####1.2 相关函数

//为一个实例对象添加一个关联对象,由于是C函数只能使用C字符串,这个key就是关联对象的名称,value为具体的关联对象的值,policy为关联对象策略,与我们自定义属性时设置的修饰符类似
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//通过key和实例对象获取关联对象的值
id objc_getAssociatedObject(id object, const void *key);
//删除实例对象的关联对象
void objc_removeAssociatedObjects(id object);

(1)key值
  关于前两个函数中的 key 值是我们需要重点关注的一个点,这个 key 值必须保证是一个对象级别(为什么是对象级别?看完下面的章节你就会明白了)的唯一常量。一般来说,有以下三种推荐的 key 值:
声明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作为 key 值;
声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作为 key 值;
用 selector ,使用 getter 方法的名称作为 key 值。
static char kAssociatedObjectKey;

但是还有更简单的方法, 可以使用selector:

或者直接使用_cmd:_ cmd在Objective-C的方法中表示当前方法的selector, 正如同self表示调用当前方法的对象(类)一样.

(2) 关联规则 objc_AssociationPolicy policy和property使用的修饰符神似,具体含义也与property修饰符相同。
Runtime在iOS开发中的实际应用

objc_AssociationPolicy

modifier

OBJC_ASSOCIATION_ASSIGN

assign

OBJC_ASSOCIATION_RETAIN_NONATOMIC

nonatomic, strong

OBJC_ASSOCIATION_COPY_NONATOMIC

nonatomic, copy

OBJC_ASSOCIATION_RETAIN

atomic, strong

OBJC_ASSOCIATION_COPY

atomic, copy

(3)objc_removeAssociatedObjects函数实际运用很少,它会移除一个对象的所有关联对象,将该对象恢复成“原始”状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给 objc_setAssociatedObject 函数传入 nil 来移除某个已有的关联对象。
####1.4 category关联对象的大体原理
isa 结构体中的标记位 has_assoc 标记为 true,表示当前对象有关联对象,关联对象并不是成员变量,关联对象是由一个全局哈希表存储的键值对中的值。

##2 对象关系映射(ORM)
通过逆向APP会发现目前对象转模型这块目前主要用的是MJExtension和YYModel,老项目一般是MJExtension,新崛起的项目转到了YYModel上。利用runtime 我们可以实现json数据的直接转换成对象模型,或者把模型通过映射拼接成晦涩的sql语句,间接实现了对象存储到sqlite数据库
Runtime在iOS开发中的实际应用

Runtime在iOS开发中的实际应用
其中ORM主要涉及到一下方法:
获取属性列表

获取成员变量列表

获取方法列表

##3 热修复(HotfixPatch)
苹果审核一直被开发者吐槽的,一是苹果审核的严格,各种理由反反复复被打回去欲哭无泪,二是审核周期长,在2017年之前苹果审核的周期一般都在三天,如果是新应用甚至需要一周以上,如果碰上圣诞节苹果放假我们这边是一般都不会提交审核,于是JSPatch 为代表的热修复技术被开发者推崇,通过逆向中国市面上有头有脸的iOS应用,我发现几乎都使用JSPath或者JSPath的变种。以至于苹果发邮件禁止使用热修复时 整个JSPath的Issues被炸锅了。热修复主要做的是替换现有的方法,或者增加新方法,需要对消息发送和转发有一定的理解。

####3.1 消息转发_objc_msgForward

这个是ios开发中最常见的crash,当前对象找不到这个方法,实际上苹果 调用doesNotRecognizeSelector方法的时候,是给了我们三次机会的。就是我们常说的消息转发,
举一个栗子,我在工作中项目出现了差错,本着挽救同志的目的,领导让我立即马上提供一次挽回的方法,如果我给力这个危机到此没了,但是我跪了搞不定,领导就问谁可以解决,这是老王站了出来,如果老王接盘搞定了这个危机那也没事了,但是老王也没有解决 领导就会找小李啊或者小张处理,如果大家都沉默无法解决, 那就项目彻底破产啦。oc中消息转发差不多就是这样的。

第一次机会允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。

如果仍没实现,继续下面的动作。

调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。

通过 -methodSignatureForSelector: 消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回 nil ,Runtime 则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。
NSInvocation 是一个消息体的封装,包括selector 以及参数等信息。因此JSPatch通过NSInvocation来创建消息
Runtime在iOS开发中的实际应用

NSInvocation可以实现传递多个参数。

##4 私有变量的修改
主要是利用class_copyIvarList获取当前类的所有属性,主要为了获取私有变量然后利用KVC修改对象的属性。
通过打印UITextField的属性,获取到变量名称为_placeholderLabel,可以修改placeholder字体颜色。

KVC 修改属性值

一般上面写法用的很少,尽快替换了方法还是有很多坑等着
一般我们的用法直接KVC 替换系统原有的变量

##5 面向切面编程(AOP)
主要利用Method Swizzling 在不破话原有的代码,将独立的功能模块剥离出来,实现代码注入。
####5.1 Method Swizzling

最重要的是需要理解selector, method, implementation 三者之间关系:在运行时,类(Class)维护了一个消息分发列表来解决消息的正确发送。每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键值是这个方法的名字 selector(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。 Method swizzling 修改了类的消息分发列表使得已经存在的 selector 映射了另一个实现 implementation,同时重命名了原生方法的实现为一个新的 selector。
Runtime在iOS开发中的实际应用
Method Swizzling需要注意的是:
(1)应该总在+load中执行,+load会在类初始加载时调用,和+initialize比较+load能保证在类的初始化过程中被加载。
(2) dispatch_once中执行:swizzling会改变全局状态,所以在运行时采取一些预防措施,使用dispatch_once就能够确保代码不管有多少线程都只被执行一次。这将成为method swizzling的最佳实践。

####5.2日志打印 快速熟悉项目。
  程序猿是跳槽率偏高的职业,如果去新公司做新项目还好说,一旦需要接手老项目的维护,商业项目可不是我们平常写的Demo的代码量,那代码中的逻辑结构瞬间会让新入职的小伙伴们懵逼,通过通过拦截点击事件,可以快速的熟悉代码的逻辑。
Runtime在iOS开发中的实际应用

####5.3处理通用逻辑
  比如在一些界面我们需要用户登录才能查看,最笨的办法实在实在需要的ViewController 添加判断登录的逻辑。下面这张截图是从Github的找到的利用AOP处理用户登录的代码,当然这个用继承基础类去写也是不错的,暂且不要在意写法的好坏 最起码我们程序开发提供了新的思路。
Runtime在iOS开发中的实际应用

####5.4Crash的防范
OC中容器类在空值nil 和数组越界都会直接导致我们app 的crash 我们一种处理方式是利用Category增加新方法中判断值是否为空或者越界,对于新工程我们使用大家约定使用容器的category还好说,

但是对于老项目 我们难道需要修改所有的容器类方法?我们可以使用切面来修改。

当然这种用法 我个人是持中立态度的,因为可以瞬间把我们代码所犯的错误处理的风平浪静,但是让我有一种掩耳盗铃的感觉,我们的问题和错误根源还在的,不断的错误叠加只会让我们代码变得危机重重,同时AOP的crash处理是无痛无感知的,一旦我们运用在第三方的静态库实际上我们就会侵入被人工程的代码,被人的代码被篡改都不知情的,这个需要谨慎使用。

##6 逆向开发
  逆向开发主要集中在iOS越狱方面,逆向开发可以让我们在iOS开发中打开另一扇门,对于大部门开发者来说很少接触这个领域,我也是在工作中才接触到iOS的越狱,逆向开发的基础就是利用Method Swizzling,不管是现在热门的THEOS还是iOSOpenDev都是Method Swizzling的封装,点击iOSOpenDev使用的CaptainHook就可以看到都是Method Swizzling 各种方法。

本文同步分享在 博客“羊羽”(other)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
技术小男生 技术小男生
4个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi/etc/profile2:按字母键i进入编辑模式,在最底部添加内容:JAVAHOME/opt/jdk1.8.0152CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jarPATH$JAVAHOME/bin:$PATH3:生效配置
光头强的博客 光头强的博客
4个月前
Java面向对象试题
1、请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
Jacquelyn38 Jacquelyn38
1年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
小森森 小森森
4个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
helloworld_34035044 helloworld_34035044
7个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue