12、iOS组件化
iOS组件化及架构设计
关于组件化
网上组件化的文章很多。很多文章一提到组件化,就会说解耦,一说到解耦就会说路由或者runtime。好像组件化 == 解耦 == 路由/Runtime,然而这是一个非常错误的观念。持有这一观点的人,没有搞清楚在组件化中什么是想要结果,什么是过程。
组件化和解耦
大家不妨先思考两个问题:
1、为何要进行组件化开发?
2、各个组件之间是否一定需要解耦?
采用组件化,是为了组件能单独开发,单独开发是结果。要让组件能单独开发,组件必须职责单一,职责单一需要用到重构和解耦的技术,所以重构和解耦是过程。那解耦是否是必须的过程?不一定。比如UIKit,我们用这个系统组件并没有使用任何解耦手段。问题来了,UIKit苹果可以独立开发,我们使用它为什么没用解耦手段?答案很简单,UIKit没有依赖我们的代码所以不用解耦。
PS:我这里不纠结组件、服务、模块、框架的概念,网上对这些概念的定义五花八门,实际上把简单的事说复杂了。我这里只关心一件事,这一部分代码能否独立开发,能就叫组件,不能我管你叫什么
我们之所以要解耦才能独立开发,通常是出现了循环依赖。这时候当然可以无脑的用路由把两个组件的耦合解开,也可以独立开发。然而,这样做只是把强引用改成了弱引用,代码还是烂代码。站在重构的角度来说,A、B组件循环依赖就是设计有问题,要么应该重构A、B让依赖单向;要么应该抽离一个共用组件C,让A、B组件都只依赖于C。
如果我们每个组件都只是单向依赖其他组件,各个组件之间也就没有必要解耦。再换个角度说,如果一个组件职责不单一,即使跟其他组件解耦了,组件依然不能很好的工作。如何解耦只是重构过程中可选手段,代码设计的原则如依赖倒置、接口隔离、里氏替换,都可以指导我们写出好的组件。
所以在组件化中重要的是让组件职责单一,职责单一的重要标志之一就是没有组件间的循环依赖。
架构图
一般来讲,App的组件可以分为三层,上层业务组件、中层UI组件、底层SDK组件
同一层之间的组件互相独立,上层的组件耦合下层的组件。一般来讲,底层SDK组件和中层UI组件都是独立的功能,不会出现同层耦合。
架构图
业务组件解耦
上层业务组件之间的解耦,采用依赖注入的方式实现。每个模块都声明一个自己依赖的协议,在App集成方里去实现这些协议。
我之前的做法是每个模块用协议提供自己对外的能力,其他模块通过协议来访问它。这样做虽然也可以解耦,但是维护成本很高,每个模块都要去理解其他模块。同时也引入了其他模块自己用不到的功能,不符合最小依赖的原则。
使用依赖注入,APP集成方统一去管理各个模块的依赖,每个模块也能单独编译,是业务层解耦的最佳实践。
包管理
要解除循环依赖,引入包管理技术cocoapods会让我们更有效率。pod不允许组件间有循环依赖,若有pod install时就会报错。
cocoapods,提供私有pod repo,使用时把自己的组件放在私有pod repo里,然后在Podfile里直接通过pod命令集成。一个组件对应一个私有pod,每个组件依赖自己所需要的三方库。多个组件联合开发的时候,可以再一个podspec里配置子模块,这样在每个组件自己的podspec里,只需要把子模块里的pod依赖关系拷贝过去就行了。
在多个组件集成时会有版本冲突的问题。比如登录组件(L)、广告组件(A)都依赖了埋点组件(O),L依赖O的1.1版本,A依赖O的1.2版本,这时候集成就会报错。为了解决这个错误,在组件间依赖时,不写版本号,版本号只在APP集成方写。即podfile里引用所有组件,并写上版本号,.podspec里不写版本号。
这样做既可以保证APP集成方的稳定性,也可以解决组件依赖的版本冲突问题。这样做的坏处是,所有组件包括App集成方,在使用其他组件时,都必须使用其他组件最新的API,这会造成额外的升级工作量。如果不想接受组件升级最新api的成本,可以私有化一个三方库自己维护。
组件开发完毕后告诉集成方,目前的组件稳定版本是多少,引用的三方库稳定版本集成方自己去决定
推荐的组件版本号管理方式
另一种版本管理的方式,是在podspec里写依赖组件的版本号,podfile里不写组件依赖的版本,然后通过内部沟通来解决版本冲突的问题。我认为虽然也能做,但有很多弊端。
1.作为App集成方,没办法单独控制依赖的三方库版本。三方库升级会更复杂
2.每个依赖的三方库,都应该做了完整的单元测试,才能被集成到App中。所以正确的逻辑不是组件内测试过三方库没问题就在组件内写死版本号,而是这个三方库经过我们测试后,可以在我们系统中使用XX版本。
3.在工程中就没有一个地方能完整知道所有的pod组件,而App集成方有权利知道这一点
4.沟通成本高
不推荐的方式
顺便说一句,基础组件库可以通过pod子模块单独暴露独立功能,较常用。
以上,就是组件化的所有东西。你可能会奇怪,解耦在组件化过程中有什么用。答案是解耦是为了更好的实现组件的单一职责,解耦的作用在架构设计中谈。需要再次强调,组件化 ≠ 解耦。
如果非要给组件化下一个定义,我的理解是:
组件化意味着重构,目的是让每个组件职责单一。在结构上,每个组件都最小依赖它所需要的东西。
关于架构设计
在我看来,iOS客户端架构主要为了解决两个问题,一是解决大型项目分组件开发的效率的问题,二是解决单进程App的稳定性的问题。
设计到架构设计的都是大型App,小型App主要是业务的堆叠。很多公司在业务初期都不会考虑架构,在业务发展到一定规模的时候,才会重新审视架构混乱带来的开发效率和业务稳定性瓶颈。这时候就会引入组件化的概念,我们常常面临的是对已有项目的组件化,这一过程会异常困难。
组件拆分原则
对老工程的组件拆分,我的办法是,从底层开始拆。SDK> 模块 > 业务 。如果App没有SDK可以抽离,就从模块开始拆,不要为了抽离SDK而抽离。常见的误区是,大家一拿到代码就把公共函数提出来作为共用框架,起的名字还特别接地气,如XXCommon。
事实上,这种框架型SDK,是最鸡肋的组件,原因是它实用性很小,无非就是减少了点冗余代码。而且在架构能力不强的情况下,它很容易变成“垃圾堆”,什么东西都想往里面放,后面越来越庞大。所以,开始拆分架构的时候,尽量以业务优先,比如先拆分享模块。
如果两个组件中有共同的函数,前期不要想着提出来,改个名字让它冗余是更好的办法。如果共同耦合的是一个静态库,可以利用动态库的隔离性封装静态库,具体方法可以网上找。
响应式
基础组件常常要在系统启动时初始化,或者接受App生命周期时间。这就引出了个问题,如何给appDelegate瘦身?比如我们现在有两个基础组件A、B,他们都需要监听App生命周期事件,传统的做法是,A、B两个组件都提供一些函数在appDelegate中调用。但这样做的坏处是,如果某一天我不想引入B组件了,还得去改appDelegate代码。理想的方式是,基础组件的使用不需要在appDelegate里写代码
为了实现基础组件与appDelegate分离,得对appDelegate改造。首先得提出一个观点,苹果的appDelegate设计的有问题,它在用代理模式解决观察者模式的问题。在《设计模式》中,代理模式的设计意图定义是:为其他对象提供一种代理以控制对这个对象的访问。反过来看appDelegate你会发现,它大部分代理函数都没有办法控制application,如applicationDidBecomeActive。applicationDidBecomeActive这种事件常常需要多个处理者,这种场景用观察者模式更适合。而openURL需要返回BOOL值,才需要使用代理模式。App生命周期事件虽然可以用监听通知获取,但用起来不如响应式监听信号方便。
基于响应式编程的思想,我写了一个TLAppEventBus,提供属性来监听生命周期事件。我并不喜欢庞大的ReactiveObjectC,所以我通过category实现了简单的响应式,用户只需要监听需要的信号即可。在TLAppEventBus里,我默认提供了8个系统事件用来监听,如果有其他的系统事件需要监听,可以使用扩展的方法,给TLAppEventBus添加属性(见文末Demo)。
路由
对于Appdelegate中的openURL的事件,苹果使用代理模式并没有问题,但我们常常需要在openURL里面写if-else区分事件的处理者,这也会造成多个URL处理模块耦合在Appdelegate中。我认为appdelegate中的openURL应该用路由转发的方式来解耦。
openURL代理需要同步返回处理结果,但网上开源的路由框架能同步返回结果的。所以我这边实现了一个能同步返回结果的路由TLRouter,同时支持了注册scheme。注册scheme这一特性,在第三方分享的场景下会比较有用(见文末Demo)。
另外,网上大部分方案都搞错了场景。以蘑菇街的路由方案为例(好像iOS的路由就是他们提出来的?),蘑菇街认为路由主要有两个作用,一是发送数据让路由接收者处理,二是返回对象让路由发送者继续处理。我不禁想问,这是路由吗?不妨先回到URL的定义
URL: 统一资源标识符(Uniform Resource Locator,统一资源定位符)是一个用于标识某一互联网资源名称的字符串
openURL就是在访问资源,在浏览器中,openURL意味着打开一个网页,openURL的发起者并不关心打开的内容是什么,只关心打开的结果。所以苹果的openURL Api 就只返回了除了结果YES/NO,没有返回一个对象。所以,我对openURL这一行为定义如下
openURL:访问资源,返回是否访问成功
那把蘑菇街的路由,返回的对象改成BOOL值就可以了么?我认为还不够。对于客户端的路由,使用的实际上是通知的形式在解耦,带来的问题是路由的注册代码散落在各地,所以路由方案必须要配路由文档,要不然开发者会不知道路由在干嘛。
有没有比文档更好的方式呢?我的思路是:用schema区分路由职责
系统的openURL只干了两件事:打开App和打开网页
[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"weixin://"]]; // 打开App
[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"https://www.baidu.com"]];//打开网页
两者的共性是页面切换。所以我这边设计的路由openURL,只扩充了controller跳转的功能,比如打开登录页
[TLRouter openURL:@"innerJump://account/login"];
只扩充了controller跳转的功能好处是让路由的职责更单一,同时也更符合苹果对openURL的定义。工程师在看到url schema的时候就知道他的作用,避免反复查看文档。
对于数据的传递,我认为不应该用路由的方式。相比路由,通过依赖注入传入信号是更好的选择。
App配置
有时候我们需要组件的跨App复用,在App集成组件时,能够不改代码只改配置是最理想的方式。使用组件+plist配置是一个方案,具体做法是把A组件的配置放在A.plist中,在A组件内写死要读取A.plist。
以配置代替硬编码,防止对代码的侵入,是一个很好的思路。设想一下,如果我们可以通过配置在决定App是否使用组件、也可通过配置来改变组件和app所需的参数,那运维可以代替app开发来出包,这对效率和稳定性都会有提升。为了实现这一效果,我使用了OC的runtime来动态注册组件。需要在didfinishLaunch初始化的组件,可以实现代理 - (void)initializeWhenLaunch; 这样,自动初始化函数,就可以通过runtime+plist里配置的class name自动初始化。组件需要初始化的代码,可以在自己的initializeWhenLaunch里做。
由于路由只扩充了controller跳转的功能,所以路由注册这一行为也可进行一次抽象,把不同的部分放在plist配置文件,相同的放到runtime里做。这样做还有个好处是,程序内的路由跳转在一个plist里可以都可以看到
appdelegate改造后示例
iOS解耦工具Tourelle
Tourelle,是根据上面的思路写的一个开源项目 https://github.com/zhudaye12138/Tourelle,可以通过pod集成 pod 'Tourelle'。下面介绍一下他的使用方式
TLAppEventBus
TLAppEventBus通过接收系统通知来获取app生命周期事件,收到生命周期事件后改变对应属性的值。默认提供了didEnterBackground等八个属性,可以使用响应式函数来监听
- (void)observeWithBlock:(TLObservingBlock)block;
[TLAppEventBus.shared.didBecomeActive observeWithBlock:^(idnewValue) {
//do some thing
}];
需要注意,如果在其它地方使用observeWithBlock,需要设置属性的owner,否则没有办法监听到。这里不用单独设置是因为在TLAppEventBus里已设置好
TLAppEventBus使用前需要调用 - (void)start; 如果需要监听更多的事件,可以调用 - (void)startWithNotificationMap:(NSDictionary *)map;
NSMutableDictionary defaultMap = [NSMutableDictionary dictionaryWithDictionary:[TLAppEventBus defaultNotificationMap]]; //获取默认map
[defaultMapsetObject:KDidChangeStatusBarOrientation forKey:UIApplicationWillChangeStatusBarOrientationNotification]; //添加新的事件
[TLAppEventBus.shared startWithNotificationMap:defaultMap];//开启EventBus
添加新事件需要用分类添加TLAppEventBus的属性,添加后就可正常使用了
-(void)setDidChangeStatusBarOrientation:(NSNotification)didChangeStatusBarOrientation {
objc_setAssociatedObject(self, (__bridge const void )KDidChangeStatusBarOrientation , didChangeStatusBarOrientation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSNotification)didChangeStatusBarOrientation {
returnobjc_getAssociatedObject(self, (__bridge const void )KDidBecomeActive);
}
TLRouter
路由支持两种注册方式,一种只写schema,一种写url路径
[TLRouter registerURL:@"wx1234567://" hander:^(TLRouterURL routeURL, void (^callback)(BOOL result)) {
//do something
}]//注册schema
[TLRouter registerURL:@"InnerJump://account/login" hander:^(TLRouterURL routeURL, void (^callback)(BOOL result)) {
//do something
}]//注册url路径
支持同步 & 异步获取返回值,其中异步转同步内部通过semaphore实现
+(void)openURL:(NSString)url callback:(void(^)(BOOLresult))callback;
+(BOOL)openURL:(NSString)url;
另外openURL除了支持url中带参数,也支持参数放在字典中
+(BOOL)openURL:(NSString)url param:(NSDictionary *)param;
TLAppLaunchHelper
TLAppLaunchHelper有两个函数,一个用来初始化组件。该函数会读取AutoInitialize.plist中的classes,通过runtime + 自动初始化协议完成初始化
-(void)autoInitialize;
AutoInitialize.plist
另一个函数用来自动注册路由,该函数会读取AutoRegistURL.plist完成路由注册。其中controller代表类名,params代表默认参数,如果openURL传的参数与默认参数不符合,路由会报错
-(void)autoRegistURL;
AutoRegistURL.plist
路由注册时,并不决定controller跳转的方式。注册者只是调用presentingSelf方法,跳转方式由controller中presentingSelf方法决定。
-(BOOL)presentingSelf {
UINavigationController *rootVC = (UINavigationController *) APPWINDOW.rootViewController;
if(rootVC) {
[rootVCpushViewController:self animated:YES];
returnYES;
}
return NO;
}
耦合检测工具
针对既有代码的组件化重构,我这边开发了一个耦合检测工具,目前只支持OC。
耦合检测工具的原理是这样:工具认为工程中一级文件夹由组件构成,比如A工程下面有aa、bb、cc三个文件夹,aa、bb、cc就是三个待检测的组件。耦合检测分三步,第一步通过正则找到组件内.h文件中所有关键字(包括函数、宏定义和类)。第二步通过找到的组件内关键字,再通过正则去其它组件的.m中找是否使用了该组件的关键字,如果使用了,两个组件就有耦合关系。第三步,输出耦合检测报告
代码:开源中....
总结
本文给出了组件化的定义:组件化意味着重构,目的是让每个组件职责单一以提升集成效率。包管理技术Pod是组件化常用的工具,iOS组件依赖及组件版本号确定,都可以用pod实现。整个iOS工程的组件通常分为3层,业务组件、模块组件和SDK组件。在老工程重构时,优先抽离SDK组件,切记不要写XXCommon让它变成垃圾堆。
关于解耦的技术,appldegate适合用观察者模式替换代理模式,路由只用来做controller之间的跳转,上层业务组件的解耦靠依赖注入而不是全用路由。工程的组件和路由都可通过runtime + 配置的形式自动注册,这样做维护和集成都会很方便。
Demo地址:https://github.com/zhudaye12138/Tourelle
13、imageNamed与imageWithContentOfFile
使用imageNamed:加载图片
加载到内存中后,会一直停留在内存中,不会随着对象销毁而销毁
加载进图片后,占用的内存归系统管理,我们无法管理
相同的图片不会重新加载
加载到内存中后,占据内存空间较大
使用 imageWithContentOfFile:加载图片
加载到内存中后,占据内存空间比较小
相同的图片会被重复加载到内存中
对象销毁的时候,加载到内存中得图片会被一起销毁
结论:
如果图片较小,并且频繁使用的图片,使用imageName:来加载图片(按钮图片/主页图片/占位图)
如果图片较大,并且使用次数较少,使用 imageWithContentOfFile:来加载(相册/版本新特性)
14、block的本质
本质1:https://www.jianshu.com/p/e9ae4585cc44
本质2: https://www.jianshu.com/p/08d300e7056c
MRC 下会age=1231259958,很奇怪的值,调用copy后值正常
void test() {
int age = 10;
block = [^{
NSLog(@"age=%d", age);
} copy]; // 调用一下copy方法
}
因为对block进行copy操作后,block从栈区被复制到了堆区,它的成员变量age也随之被复制到了堆区,这样test函数执行完之后,它的栈区被销毁并不影响block,因此能得出正确的输出
总结一下ARC环境下自动进行copy操作的情况一共有以下几种:
- block作为函数返回值时。
- 将block赋值给__strong指针时。
- block作为Cocoa API中方法名含有usingBlock的方法参数时。
- GCD中的API。
Auto变量(局部变量,过了作用域就会释放)
Static局部静态变量
1.捕获-auto变量:值传递
2.捕获-static变量:指针传递
这里我们可以看到结构体多了一个指针类型的成员变量int *a,然后在构造函数中,将传递过来的&a,赋值给这个指针变量。也就是说,在_main_block_impl_0这个结构体中多了一个成员变量,这个成员变量是指针,指向a这个变量。所以当a变量的值发生变化时,能够得到最新的值
3.捕获-全局变量:
可以看到,这个地方在调用的时候是直接调用的全局变量height和weight, 所以block并不会捕获全局变量
4.变量捕获-self变量:self作为参数传递,数据局部变量会被捕获
思考:为什么对于不同类型的变量,block的处理方式不同呢?
这是由变量的生命周期决定的。对于auto变量,当作用域结束时,会被系统自动回收,而block很可能是在超出auto变量作用域的时候去执行,如果之前没有捕获auto变量,那么后面执行的时候,auto变量已经被回收了,得不到正确的值。对于static局部变量,它的生命周期不会因为作用域结束而结束,所以block只需要捕获这个变量的地址,在执行的时候通过这个地址去获取变量的值,这样可以获得变量的最新的值。而对于全局变量,在任何位置都可以直接读取变量的值。
思考:为什么对于auto变量block捕获的是数值而对于static局部变量捕获的是地址?
还是由变量的生命周期决定的,对于auto变量,当作用域结束时,会被系统自动回收,地址就会变成空的,造成坏地址访问。对于static局部变量,它的生命周期不会因为作用域结束而结束,所以block只需要捕获这个变量的地址,在执行的时候通过这个地址去获取变量的值。
思考: static局部变量生命周期什么时候结束?
说明:
在局部变量的说明前再加上static说明符就构成静态局部变量。例如:static int a,b; static float array[5]={1,2,3,4,5};
静态局部变量属于静态存储方式,它具有以下特点:
(1)静态局部变量在函数内定义,但不象自动变量那样,当调用时就存在,退出函数时就消失。静态局部变量始终存在着,也就是说它的生存期为整个源程序。
(2)静态局部变量的生存期虽然为整个源程序,但是其作用域仍与自动变量相同,即只能在定义该变量的函数内使用该变量。退出该函数后,尽管该变量还继续存在,但不能使用它。
(3)允许对构造类静态局部量赋初值。若未赋以初值,则由系统自动赋以0值。
(4)对基本类型的静态局部变量若在说明时未赋以初值,则系统自动赋予0值。而对自动变量不赋初值,则其值是不定的。根据静态局部变量的特点,可以看出它是一种生存期为整个源程序的量。虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值。因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用局部静态变量为宜。
补充:静态全局变量
全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。应予以注意。
15、消息转发的实际应用
1、forwardingTargetForSelector
//
// WXProtocolConformer.m
// LivePlayer
//
// Created by Bo Liu on 2018/11/30.
//
#import "WXProtocolConformer.h"
@interface WXProtocolConformer ()
@property (nonatomic, copy) NSSet *protocolSet;
@property (nonatomic, strong)Protocol *pro;
@end
@implementation WXProtocolConformer
+ (id)protocolArray:(NSSet *)array protocol:(Protocol *)protocol {
return [[super alloc]initWithProtocolArray:array protocol:protocol];
}
- (id)initWithProtocolArray:(NSSet *)array protocol:(Protocol *)protocol {
self.pro = protocol;
self.protocolSet = array;
if (![self hasConformer]) {
return nil;
}
return self;
}
- (BOOL)hasConformer {
for (id obj in self.protocolSet) {
if ([obj conformsToProtocol:self.pro]) {
return YES;
}
}
return NO;
}
#pragma mark - Forward methods
- (BOOL)respondsToSelector:(SEL)selector {
BOOL responds = NO;
BOOL isMandatory = NO;
struct objc_method_description methodDescription = [self methodDescriptionForSelector:selector isMandatory:&isMandatory];
if (isMandatory) {
responds = YES;
}
else if (methodDescription.name != NULL) {
responds = [self checkIfAttachedObjectsRespondToSelector:selector];
}
return responds;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
//是否是required方法
BOOL isMandatory = NO;
struct objc_method_description methodDescription = [self methodDescriptionForSelector:selector isMandatory:&isMandatory];
if (methodDescription.name == NULL) {
[super forwardInvocation:anInvocation];
return;
}
BOOL someoneResponded = NO;
[self forwardInvocation:anInvocation forSelector:selector someoneResponded:&someoneResponded];
// if (isMandatory && !someoneResponded) {
// [super forwardInvocation:anInvocation];
// }
}
- (void)forwardInvocation:(NSInvocation *)anInvocation forSelector:(SEL)selector someoneResponded:(BOOL*)someoneResponded {
for (id object in self.protocolSet) {
if ([object conformsToProtocol:self.pro]) {
if ([object respondsToSelector:selector]) {
[anInvocation invokeWithTarget:object];
*someoneResponded = YES;
}
}
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSMethodSignature * theMethodSignature;
BOOL isMandatory = NO;
struct objc_method_description methodDescription = [self methodDescriptionForSelector:selector isMandatory:&isMandatory];
if (methodDescription.name == NULL) {
return nil;
}
theMethodSignature = [NSMethodSignature signatureWithObjCTypes:methodDescription.types];
return theMethodSignature;
}
- (struct objc_method_description)methodDescriptionForSelector:(SEL)selector isMandatory:(BOOL *)isMandatory {
struct objc_method_description method = {NULL, NULL};
method = [self methodDescriptionInProtocol:self.pro selector:selector isMandatory:isMandatory];
if (method.name == NULL) {
unsigned int count = 0;
Protocol * __unsafe_unretained * list = protocol_copyProtocolList(self.pro, &count);
for (NSUInteger i = 0; i < count; i++) {
Protocol * aProtocol = list[i];
if ([NSStringFromProtocol(aProtocol) isEqualToString:@"NSObject"]) continue;
method = [self methodDescriptionInProtocol:aProtocol selector:selector isMandatory:isMandatory];
if (method.name != NULL) {
break;
}
}
free(list);
}
return method;
}
- (struct objc_method_description)methodDescriptionInProtocol:(Protocol *)protocol selector:(SEL)selector isMandatory:(BOOL *)isMandatory {
struct objc_method_description method = {NULL, NULL};
method = protocol_getMethodDescription(protocol, selector, YES, YES);
if (method.name != NULL) {
*isMandatory = YES;
return method;
}
method = protocol_getMethodDescription(protocol, selector, NO, YES);
if (method.name != NULL) {
*isMandatory = NO;
}
return method;
}
- (BOOL)checkIfAttachedObjectsRespondToSelector:(SEL)selector {
for (id object in self.protocolSet) {
if ([object respondsToSelector:selector]) {
return YES;
}
}
return NO;
}
@end
16、分类方法调用顺序
1、多个分类同名方法,只调用一次,跟编译顺序有关,只调用最前的
2、同一个的分类,例如Peson,Peson+A,Peson+B, 调用cry方法,只打印A的
3、Person子类Son,重写cry,此时优先掉用子类Son的cry方法,如果子类未实现,参考第2步的顺序
总结同名方法的执行顺序:
子类 > 分类 > 主类
+load方法的执行顺序
2021-10-27 19:36:08.485455+0800 test[3464:139907] ------+[Father load]
2021-10-27 19:36:08.486064+0800 test[3464:139907] ------+[Son load]
2021-10-27 19:36:08.486163+0800 test[3464:139907] ------+[Father(PersonB) load]
2021-10-27 19:36:08.486286+0800 test[3464:139907] ------+[Father(PersonA) load]
+initialize方法的调用逻辑
2021-10-27 19:38:07.088787+0800 test[3499:141743] ------+[Father load]
2021-10-27 19:38:07.089335+0800 test[3499:141743] ------+[Son load]
2021-10-27 19:38:07.089420+0800 test[3499:141743] ------+[Father(PersonB) load]
2021-10-27 19:38:07.089509+0800 test[3499:141743] ------+[Father(PersonA) load]
2021-10-27 19:38:07.290267+0800 test[3499:141743] ------+[Father(PersonA) initialize]
2021-10-27 19:38:07.290427+0800 test[3499:141743] -------[Father(PersonA) cry]
2021-10-27 19:38:07.290532+0800 test[3499:141743] ------+[Son initialize]
2021-10-27 19:38:07.290628+0800 test[3499:141743] -------[Son cry]
17、深入理解分类category
https://tech.meituan.com/2015/03/03/diveintocategory.html
18、Weak的底层实现
https://www.jianshu.com/p/f331bd5ce8f8
19、iOS组件化-路由设计思路
https://www.jianshu.com/p/76da56b3bd55
20、获取堆栈信息使用PLCrashReporter.
https://www.jianshu.com/p/0ac8d4e2f60c
21、卡顿检测方案分析与总结
https://www.jianshu.com/p/ea36e0f2e7ae
22、TCP、UDP和HTTP区别详解
http:是用于www浏览的一个协议。tcp:是机器之间建立连接用的到的一个协议。
1、TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层。在网络层有IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。在传输层中有TCP协议与UDP协议。在应用层有FTP、HTTP、TELNET、SMTP、DNS等协议。因此,HTTP本身就是一个协议,是从Web服务器传输超文本到本地浏览器的传送协议。
2、HTTP协议是建立在请求/响应模型上的。首先由客户建立一条与服务器的TCP链接,并发送一个请求到服务器,请求中包含请求方法、URI、协议版本以及相关的MIME样式的消息。服务器响应一个状态行,包含消息的协议版本、一个成功和失败码以及相关的MIME式样的消息。HTTP/1.0为每一次HTTP的请求/响应建立一条新的TCP链接,因此一个包含HTML内容和图片的页面将需要建立多次的短期的TCP链接。一次TCP链接的建立将需要3次握手。另外,为了获得适当的传输速度,则需要TCP花费额外的回路链接时间(RTT)。每一次链接的建立需要这种经常性的开销,而其并不带有实际有用的数据,只是保证链接的可靠性,因此HTTP/1.1提出了可持续链接的实现方法。HTTP/1.1将只建立一次TCP的链接而重复地使用它传输一系列的请求/响应 消息,因此减少了链接建立的次数和经常性的链接开销。
3、结论:虽然HTTP本身是一个协议,但其最终还是基于TCP的。不过,目前,有人正在研究基于TCP+UDP混合的HTTP协议。
具体介绍
IP (网际协议)
在网络通信中,网络组件的寻址对信息的路由选择和传输来说是相当关键的。相同网络中的两台机器间的消息传输有各自的技术协定。LAN 是通过提供6字节的唯一标识符(“MAC”地址)在机器间发送消息的。SNA 网络中的每台机器都有一个逻辑单元及与其相应的网络地址。DECNET、AppleTalk 和 Novell IPX 均有一个用来分配编号到各个本地网和工作站的配置。
HTTP是超文本传输协议,是客户端浏览器或其他程序与Web服务器之间的应用层通信协议。在Internet上的Web服务器上存放的都是超文本信息, 客户机需要通过HTTP协议传输所要访问的超文本信息。HTTP包含命令和传输信息,不仅可用于Web访问,也可以用于其他因特网/内联网应用系统之间的通信,从而实现各类应用资源超媒体访问的集成
TCP (传输控制协议)
通过序列化应答和必要时重发数据包,TCP 为应用程序提供了可靠的传输流和虚拟连接服务。TCP 主要提供数据流转送,可靠传输,有效流控制,全双工操作和多路传输技术。可查阅 TCP 部分获取更多详细资料。
至于HTTP协议,它是TCP协议族中的一种。使用TCP80端口
HTTP是应用层协议,TCP是传输层协议!
数据包在网络传输过程中,HTTP被封装在TCP包内!!
- TCP/UDP
面向连接的TCP
“面向连接”就是在正式通信前必须要与对方建立起连接。比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。
TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,我们这里只做简单、形象的介绍,你只要做到能够理解这个过程即可。
我们来看看这三次对话的简单过程:
- 主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;
- 主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;
- 主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。
三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。
TCP协议能为应用程序提供可靠的通信连接,使一台计算机发出的字节流无差错地发往网络上的其他计算机,对可靠性要求高的数据通信系统往往使用TCP协议传输数据。
我们来做一个实验,用计算机A(安装Windows 2000 Server操作系统)从“网上邻居”上的一台计算机B拷贝大小为8,644,608字节的文件,通过状态栏右下角网卡的发送和接收指标就会发现:虽然是 数据流是由计算机B流向计算机A,但是计算机A仍发送了3,456个数据包,如图2所示。这些数据包是怎样产生的呢?因为文件传输时使用了TCP/IP协 议,更确切地说是使用了面向连接的TCP协议,计算机A接收数据包的时候,要向计算机B回发数据包,所以也产生了一些通信量。
如果事先用网络监视器监视网络流量,就会发现由此产生的数据流量是9,478,819字节,比文件大小多出10.96%(如图3所示),原因不仅在于数据包和帧本身占用了一些空间,而且也在于TCP协议面向连接的特性导致了一些额外的通信量的产生。
面向非连接的UDP协议
“面向非连接”就是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。这与现在风行的手机短信非常相似:你在发短信的时候,只需要输入对方手机号就OK了。
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!
UDP 适用于一次只传送少量数据、对可靠性要求不高的应用环境。比如,我们经常使用“ping”命令来测试两台主机之间TCP/IP通信是否正常,其实 “ping”命令的原理就是向对方主机发送UDP数据包,然后对方主机确认收到数据包,如果数据包是否到达的消息及时反馈回来,那么网络就是通的。例如, 在默认状态下,一次“ping”操作发送4个数据包。大家可以看到,发送的数据包数量是4包,收到的也是4包(因为对方主机收到后会发回一 个确认收到的数据包)。这充分说明了UDP协议是面向非连接的协议,没有建立连接的过程。正因为UDP协议没有连接的过程,所以它的通信效果高;但也正因为如此,它的可靠性不如TCP协议高。QQ就使用UDP发消息,因此有时会出现收不到消息的情况。
23、《招聘一个靠谱的iOS》面试题参考答案(上)
http://www.cocoachina.com/articles/12872
24、为什么用copy修饰符
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xe83c0f43daff68b3'
*** First throw call stack:
//@property (nonatomic) NSString *useId; log=//123456
//@property (nonatomic) NSString *useId; crash,因为声明时是copy修饰符,是不可变的,所以会提示找不到appendString方法
self.useId = [[NSMutableString alloc] initWithString:@"123"];
[(NSMutableString *)self.useId appendString:@"456"];
NSLog(@"useId===%@",self.useId);
copy 此特质所表达的所属关系与 strong 类似。然而set方法并不保留新值,而是将其“拷贝” (copy)。
当属性类型为 NSString 时,经常用此特质来保护其封装性,因为传递给set方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么set完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。
25、Block中修改局部变量的值为什么必须声明为__block类型
Block从栈拷贝到堆
1.被执行copy方法
2.作为方法返回值
3.将Block赋值给附有__strong修饰符的id类型的类或者Blcok类型成员变量时
4.在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递的时候.
在ARC下:似乎已经没有栈上的block了,要么是全局的,要么是堆上的
在非ARC下:存在这栈、全局、堆这三种形式。
(访问局部变量未使用__block修饰时报错是苹果刻意为之,因为作用域的问题,参考下面的解释)
-
对于普通的auto局部变量(栈变量),Block捕获时,将值拷贝进Block用结构体的成员变量中。因此后续对局部变量的改变就再也影响不了Block内部。
-
对于__block修饰的局部变量,Block捕获时,记录了该变量的地址。所以后续该变量的值改变了,block调用时,通过地址获取到的值仍然是最新的值。
-
说明
- 考虑到篇幅,没有介绍Block捕获__block局部变量的转换后的C++源代码。但是其本质和捕获局部静态变量是一致的,都是在Block用结构体中记录下了该变量的地址。
- Block捕获__block局部变量的值的转换后C++代码会比,上述捕获静态局部变量的代码复杂很多。在后续的文章《Block捕获__block局部变量的底层原理》中有介绍Block捕获__block局部变量的底层原理。
2.3 底层思考
-
参考《Objective-C 高级编程 iOS与OS X多线程和内存管理》后续章节对Blocks的实现,我们可以知道,Blocks生成的结构体会捕获所用到的变量。
-
内存指示图
1.png -
对于局部变量,Blocks默认捕获的是这个局部变量的值(即图中的MemoryObj变量), 可以通过对MemroyObj这个地址上的内容进行修改(本质是运用了C语言的*运算符)
-
而添加了__block说明符,则Blocks捕获的是这个局部变量的内存地址,即Memroy值(C语言中使用&操作取得一个变量的地址),这样Blocks在内部就可以通过对Memory上的数据对修改(*memroy = xxx),且可以影响到Blocks外部。
2.png -
没有用__block修饰的局部变量,在Blocks内部捕获了,即使修改了也没有任何意义(外部不受影响),所以编译器当初就设计了这个编译报错,避免产生不可预知的bug。
-
鉴于篇幅和结构,这里没有介绍Block捕获__block修饰的变量的C++代码情况,关于该知识,可参考下一篇文章《Block捕获__block局部变量的底层原理》。
-
程序员声明的block,编译器会生成对应的Objective-C对象(本质就是一个结构体,由于带有isa指针,吻合Objective-C对象的定义,因此是一个Objective-C对象)。
-
该对象种记录了block对应的函数指针,以及存储了block捕获的变量。因此后续调用block时,实质上是调用了该Objective-C对象记录的函数指针,并传递了参数(block对象本身指针self,block捕获的变量)。
网友评论