从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空
- 判断是否为元类
class_isMetaClass(objcL);
参考截图:
1、聊天页面
2、会话列表
Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App
我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:
1.一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。
2.避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。
3.通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect
4.证书的同步更新,管理,在新电脑能够迅速具备项目打包环境。
如果你也有上述需求,那我相信Fastlane是一个好的选择。
一、安装xcode命令行工具
xcode-select --install,如果没有安装,会弹出对话框,点击安装。
如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates表示已经安装
二、安装Fastlane
sudo gem install fastlane -NV或是brew cask install fastlane我这里使用gem安装的
安装完了执行fastlane --version,确认下是否安装完成和当前使用的版本号。
三、初始化Fastlane
cd到你的项目目录执行
fastlane init
这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。
如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。
接着会提示你输入开发者账号和密码。
[20:48:55]: Please enter your Apple ID developer credentials
[20:48:55]: Apple ID Username:
登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。
如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。
四、文件系统
初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。
其中metadata和screenshots分别对应App元数据和商店应用截图。
Appfile主要存放App的apple_id team_id app_identifier等信息
Deliverfile中为发布的配置信息,一般情况用不到。
Fastfile是我们最应该关注的文件,也是我们的工作文件。
Fastfile
之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。
打包到蒲公英
这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。
蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。
打开终端输入fastlane add_plugin pgyer
更多信息查看蒲公英文档
新建一个lane
desc "打包到pgy"
lane :test do |options|
gym(
clean:true, #打包前clean项目
export_method: "ad-hoc", #导出方式
scheme:"shangshaban", #scheme
configuration: "Debug",#环境
output_directory:"./app",#ipa的存放目录
output_name:get_build_number()#输出ipa的文件名为当前的build号
)
#蒲公英的配置 替换为自己的api_key和user_key
pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
end
这样一个打包到蒲公英的lane就完成了。
option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台
执行
在工作目录的终端执行
fastlane test desc:测试打包
然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置。
其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解
其他的一些小提示
1.可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来。
2.如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。
3.如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。
4.如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据 skip_screenshots:true,#不上传屏幕截图
关于fastlane的一些想法
其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!
摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/59725c52e0fa
一.OC对象的内存管理机制?
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
二.内存区域分布
在iOS开发过程中,为了合理的分配有限的内存空间,将内存区域分为五个区,由低地址向高地址分类分别是:代码区、常量区、全局静态区、堆、栈。
代码段 -- 程序编译产生的二进制的数据
常量区 -- 存储常量数据,通常程序结束后由系统自动释放
全局静态区 -- 全局区又可分为未初始化全局区:.bss段和初始化全局区:data段。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,在程序结束后有系统释放。
堆(heap) -- 程序运行过程中,动态分配的内存
栈(stack) -- 存放局部变量,临时变量
三.堆区和栈取的区别
按管理方式分
对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
按分配方式分
堆是动态分配和回收内存的,没有静态分配的堆
栈有两种分配方式:静态分配和动态分配
静态分配是系统编译器完成的,比如局部变量的分配
动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动 态分配也由系统编译器进行释放,不需要程序员手动管理
四.怎么保证多人开发进行内存泄露的检查
1.使用Analyze进行代码的静态分析
2.使用leaks 进行内存泄漏检测
3.使用一些三方工具(DoraemonKit/WithMLeaksFinder)
五.内存泄漏可能会出现的几种原因?
第一种可能:第三方框架不当使用;
第二种可能:block循环引用;
第三种可能:delegate循环引用;
第四种可能:NSTimer循环引用
第五种可能:非OC对象内存处理
第六种可能:地图类处理
第七种可能:大次数循环内存暴涨
六.什么是Tagged Pointer?
1.从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
2.使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
3.当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
七.copy和mutableCopy区别
八.AutoreleasePoolPage的结构?以及如何 push 和 pop 的
AutoreleasePool(自动释放池)其实并没有自身的结构,他是基于多个AutoreleasePoolPage(一个C++类)以双向链表组合起来的结构; 可以通过 push操作添加对象,pod 操作弹出对象,以及通过 release 操作释放对象;
调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
id *next指向了下一个能存放autorelease对象地址的区域
九.Autoreleasepool 与 Runloop 的关系
主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理
iOS在主线程的Runloop中注册了2个Observer
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
十.什么是多线程?
多线程是指实现多个线程并发执行的技术,进而提升整体处理性能。
同一时间,CPU 只能处理一条线程,多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换)如果 CPU 调度线程的时间足够快, 就造成了多线程并发执行的假象
主线程的栈区 空间大小为1M,非常非常宝贵
子线程的栈区 空间大小为512K内存空间
优势
充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“并行计算”状态
弊端
新线程会消耗内存控件和cpu时间,线程太多会降低系统运行性能。
十一.iOS的多线程方案有哪几种?
十二,讲一下GCD
GCD(Grand Central Dispatch), 又叫做大中央调度, 它对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口, 使用更加高效,也是苹果推荐的使用方式.
GCD 的队列
1.并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效
2.串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务),按照FIFO顺序执行.
同步和异步任务
GCD多线程经常会使用 dispatch_sync和dispatch_async函数向指定队列添加任务,分别是同步和异步
同步指阻塞当前线程,既要等待添加的耗时任务块Block完成后,函数才能返回,后面的代码才能继续执行
异步指将任务添加到队列后,函数立即返回,后面的代码不用等待添加的任务完成后即可执行,异步提交无法确定任务执行顺序
相关常用函数使用:
1.dispatch_after使用 (通过该函数可以让提交的任务在指定时间后开始执行,也就是延迟执行;)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"10秒后开始执行")
});
2.dispatch_group_t (组调度)的使用 (组调度可以实现等待一组操都作完成后执行后续任务.)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求3
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
3.dispatch_semaphore (信号量)如何使用?
用于控制最大并发数 可以防止资源抢夺
与他相关的共有三个函数,分别是:
dispatch_semaphore_create, // 创建最大并发数
dispatch_semaphore_wait。 // -1 开始执行 (0则等待)
dispatch_semaphore_signal, // +1
4.dispatch_barrier_(a)sync使用?
一个dispatch barrier 允许在一个并发队列中创建一个同步点。当在并发队列中遇到一个barrier, 他会延迟执行barrier的block,等待所有在barrier之前提交的blocks执行结束。 这时,barrier block自己开始执行。 之后, 队列继续正常的执行操作。
十三.什么是NSOperation?
1.NSOperation是基于GCD的上封装,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强
例如:
可以加入操作依赖控制执行顺序,设置操作队列最大并发数,取消操作等
2.NSOperation如何实现操作依赖
通过任务间添加依赖,可以为任务设置执行的先后顺序。接下来通过一个案例来展示设置依赖的效果。
NSOperationQueue *queue=[[NSOperationQueue alloc] init];
//创建操作
NSBlockOperation *operation1=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第1次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation2=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第2次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation3=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第3次操作,线程:%@",[NSThread currentThread]);
}];
//添加依赖
[operation1 addDependency:operation2];
[operation2 addDependency:operation3];
//将操作添加到队列中去
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
十四.在项目什么时候选择使用 GCD,什么时候选 择 NSOperation
项目中使用 NSOperation 的优点是 NSOperation 是对线程的高度抽象,在项目中使 用它,会使项目的程序结构更好,子类化 NSOperation 的设计思路,是具有面向对 象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中 使用。
项目中使用 GCD 的优点是 GCD 本身非常简单、易用,对于不复杂的多线程操 作,会节省代码量,而 Block 参数的使用,会是代码更为易读,建议在简单项目中 使用。
区别,以及各自的优势
GCD是纯C语⾔言的API,NSOperationQueue是基于GCD的OC版本封装
GCD只⽀支持FIFO的队列列,NSOperationQueue可以很⽅方便便地调整执⾏行行顺 序、设 置最⼤大并发数量量
NSOperationQueue可以在轻松在Operation间设置依赖关系,⽽而GCD 需要写很 多的代码才能实现
NSOperationQueue⽀支持KVO,可以监测operation是否正在执⾏行行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld)
GCD的执⾏行行速度⽐比NSOperationQueue快 任务之间不不太互相依赖:GCD 任务之间 有依赖\或者要监听任务的执⾏行行情况:NSOperationQueue
十五.线程安全的处理手段有哪些,线程锁都有哪些?
1.加锁
2.同步执行
线程锁 (我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁 应运而生。)
1.OSSpinLock (自旋锁)
注:苹果爸爸已经在iOS10.0以后废弃了这种锁机制,使用os_unfair_lock 替换,顾名思义能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题.
2.os_unfair_lock(自旋锁)
3.dispatch_semaphore (信号量)
4.pthread_mutex(互斥锁)
5.NSLock(互斥锁、对象锁)
6.NSCondition(条件锁、对象锁)
7.NSConditionLock(条件锁、对象锁)
8.NSRecursiveLock(递归锁、对象锁)
9.@synchronized(条件锁)
10.pthread_mutex(recursive)(递归锁)
注.递归锁可以被同一线程多次请求,而不会引起死锁。即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。这主要是用在循环或递归操作中
性能图
十六.HTTPS连接过程简述
1.客户端向服务端发起 https 请求
2.服务器(需要申请 ca 证书),返回证书(包含公钥)给客户端
3.客户端使用根证书验证 服务器证书的有效性,进行身份确认
4.客户端生成对称密钥,通过公钥进行密码,发送给服务器
5.服务器使用私钥进行 解密,获取对称密钥
6.双方使用对称加密的数据进行通信
十七.http 与https区别
HTTPS和HTTP的区别主要为以下四点:
1.https协议需要到ca申请证书,一般免费证书很少,需要交费。
2.http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4.http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全
十八.什么是DNS?DNS劫持问题?
域名系统(DomainNameSystem,缩写:DNS)是[互联网]的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问[互联网]
DNS劫持又称(域名劫持), 是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。
解决办法: 使用HTTPDNS
十九.网络七层是什么?
OSI模型有7层结构,每层都可以有几个子层。 OSI的7层从上到下分别是 7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层 ;其中高层(即7、6、5、4层)定义了应用程序的功能,下面3层(即3、2、1层)主要面向通过网络的端到端的数据流。
1.应用层
网络服务与最终用户的一个接口。
协议有:HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP
2.表示层
数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层)
格式有,JPEG、ASCll、DECOIC、加密格式等
3 .会话层
建立、管理、终止会话。(在五层模型里面已经合并到了应用层)
对应主机进程,指本地主机与远程主机正在进行的会话
4.传输层
定义传输数据的协议端口号,以及流控和差错校验。
协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层
5.网络层
进行逻辑地址寻址,实现不同网络之间的路径选择。
协议有:ICMP IGMP IP(IPV4 IPV6) ARP RARP
6.数据链路层
建立逻辑连接、进行硬件地址寻址、差错校验 [2] 等功能。(由底层网络定义协议)
将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。
7.物理层
建立、维护、断开物理连接。(由底层网络定义协议)
二十.项目中网络层如何做安全处理
1.尽量使用https
2.不要传输明文密码
3.Post并不比Get安全
4.不要使用301跳转
5.http请求都带上MAC
6.http请求使用临时密钥
7.AES使用CBC模式
ios加固,ios代码混淆,ios代码混淆工具,iOS源码混淆产品是一款纯离线的源码加密工具,主要用于保护iOS项目中的核心代码,避免因逆向工程或破解,造成核心技术被泄漏、代码执行流程被分析等安全问题。该加密工具和普通编译器相似,基于项目源代码可将Objective-C、Swift、C、C++代码编译成二进制代码,不同之处在于,加密工具在编译时,能够对代码采取混淆、字符串加密等安全措施。从而避免攻击者通过IDA Pro等逆向工具反编译二进制代码,分析业务代码执行流程,进一步篡改或窃取核心技术。
概述
本文主要介绍iOS源码混淆产品之Xcode插件的使用方式,阅读者需具备iOS开发经验,否则使用可能存在困难。
安装插件
v13.0.2-20190703及其之前的版本为替换clang编译器的模式,之后版本为切换Xcode -> Toolchains的模式,后者可以在Xcode中快速切换编译器。
Xcode插件通过执行python install.py 命令安装编译器,使用完成后执行 python uninstal.py 即可卸载编译器。如下图:
(备注:如果有多个Xcode版本,请修改configuration.txt文件中Xcode默认的路径。)
执行安装会提示输入密码,输入电脑开机密码即可,Xcode插件安装成功后会有Install Success提示,如下图:
引入头文件
将include目录下的KiwiOBF.h头文件拷贝到iOS项目中,并在需的地方进行引用即可。
添加KIWIOBF标签
对需要进行混淆保护的函数,添加KIWIOBF标签,以告知编译器该函数需要进行混淆编译。如下图:
设置参数
全编译器有默认混淆参数,如不能满足需求,可以自定义配置参数
加密参数说明
iOS项目的混淆参数在 Other C Flags,Other C++ Flags,Other Swift Flags中设置,如下图:
卸载插件
Xcode插件:执行 python uninstall.py 即可卸载编译器。
友情告知地址,ios代码混淆,ios加固:https://www.kiwisec.com/product/compiler-ios.html
转自:https://www.jianshu.com/p/7fdb4544c916
收起阅读 »一.列举出延迟调用的几种方法?
1.performSelector方法
[self performSelector:@selector(Delay) withObject:nil afterDelay:3.0f];
2.NSTimer定时器
[NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:@selector(Delay) userInfo:nil repeats:NO];
3.sleepForTimeInterval
[NSThread sleepForTimeInterval:3.0f];
4.GCD方式
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self Delay];
});
- (void)Delay {
NSLog(@"执行");
}
二.NSCache 和NSDictionary 区别?
NSCache可以提供自动删减缓存功能,而且保证线程安全,与字典不同,不会拷贝键。
NSCache可以设置缓存上限,限制对象个数和总缓存开销。定义了删除缓存对象的时机。这个机制只对NSCache起到指导作用,不会一定执行。
NSPurgeableData搭配NSCache使用,可以自动清除数据。
只有那种“重新计算很费劲”的数据才值得放入缓存。
三.NSArray 和 NSSet区别
NSSet和NSArray功能性质一样,用于存储对象,属于集合。
NSSet属于 “无序集合”,在内存中存储方式是不连续
NSArray是 “有序集合” 它内存中存储位置是连续的。
NSSet,NSArray都是类,只能添加对象,如果需要加入基本数据类型(int,float,BOOL,double等),需要将数据封装成NSNumber类型。
由于NSSet是用hash实现的所以就造就了它查询速度比较快,但是我们不能把某某对象存在第几个元素后面之类的有关下标的操作。
四.什么是分类?
分类: 在不修改原有类代码的情况下,可以给类添加方法
Categroy 给类扩展方法,或者关联属性, Categroy底层结构也是一个结构体:内部存储这结构体的名字,那个类的分类,以及对象和类方法列表,协议,属性信息
通过Runtime加载某个类的所有Category数据
把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面
将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
五.为什么说OC是一门动态语言?
动态语言:是指程序在运行时可以改变其结构,新的函数可以被引进,已有的函数可以被删除等在结构上的变化
动态类型语言: 就是类型的检查是在运行时做的。
OC的动态特性可从三方面:
动态类型(Dynamic typing):最终判定该类的实例类型是在运行期间
动态绑定(Dynamic binding):在运行时确定调用的方法
动态加载(Dynamic loading):在运行期间加载需要的资源或可执行代码
六.什么是动态绑定?
动态绑定 将调用方法的确定也推迟到运行时。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去,这就是动态绑定。
在编译时,方法的 调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,
七.什么是谓词?
谓词(NSPredicate)是OC针对数据集合的一种逻辑帅选条件,类似一个过滤器,简单实实用代码如下:
Person * p1 = [Person personWithName:@"alex" Age:20];
Person * p2 = [Person personWithName:@"alex1" Age:30];
Person * p3 = [Person personWithName:@"alex2" Age:10];
Person * p4 = [Person personWithName:@"alex3" Age:40];
Person * p5 = [Person personWithName:@"alex4" Age:80];
NSArray * persons = @[p1, p2, p3, p4, p5];
//定义谓词对象,谓词对象中包含了过滤条件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age < 30"];
//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];
八.什么是类工厂方法?
类工厂方法就是用来快速创建对象的类方法, 他可以直接返回一个初始化好的对象,具备以下特征:
一定是类方法
返回值需要是 id/instancetype 类型
规范的方法名说说明类工厂方法返回的是一个什么对象,一般以类名首字母小写开始;
比如系统 UIButton 的buttonWithType 就是一个类工厂方法:
// 类工厂方法
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
// 使用
+ UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
九.简要说明const,宏,static,extern区分以及使用?
1.const
const常量修饰符,经常使用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量。
- const 作用:限制类型
- 使用const修饰基本变量, 两种写法效果一致 , b都是只读变量
const int b = 5;
int const b = 5;
- 使用const修饰指针变量的变量
第一种: const int *p = &a 和 int const *q = &a; 效果一致,*p 的值不能改,p 的指向可以改;
第二种: int * const p = &a; 表示 p 的指向不能改,*p 的值可以改
第三种:
const int * const p = &a; *p 值和 p 的指向都不能改
const 在*左边, 指向可变, 值不可变
const 在*的右边, 指向不可变, 值可变
const 在*的两边, 都不可变
2.宏
* 基本概念:宏是一种批量处理的称谓。一般说来,宏是一种规则或模式,或称语法替换 ,用于说明某一特定输入(通常是字符串)如何根据预定义的规则转换成对应的输出(通常也是字符串)。这种替换在预编译时进行,称作宏展开。编译器会在编译前扫描代码,如果遇到我们已经定义好的宏那么就会进行代码替换,宏只会在内存中copy一份,然后全局替换,宏一般分为对象宏和函数宏。 宏的弊端:如果代码中大量的使用宏会使预编译时间变长。
const与宏的区别?
* 编译检查 宏没有编译检查,const有编译检查;
* 宏的好处 定义函数,方法 const不可以;
* 宏的坏处 大量使用宏,会导致预编译时间过长
3.static
* 修饰局部变量: 被static修饰局部变量,延长生命周期,跟整个应用程序有关,程序结束才会销毁,被 static 修饰局部变量,只会分配一次内存
* 修饰全局变量: 被static修饰全局变量,作用域会修改,也就是只能在当前文件下使用
4.extern
声明外部全局变量(只能用于声明,不能用于定义)
常用用法(.h结合extern联合使用)
如果在.h文件中声明了extern全局变量,那么在同一个类中的.m文件对全局变量的赋值必须是:数据类型+变量名(与声明一致)=XXXX结构。并且在调用的时候,必须导入.h文件。代码如下:
.h
@interface ExternModel : NSObject
extern NSString *lhString;
@end
.m
@implementation ExternModel
NSString *lhString=@"hello";
@end
调用的时候:例如:在viewController.m中调用,则可以引入:ExternModel.h,否则无法识别全局变量。当然也可以通过不导入头文件的方式进行调用(通过extern调用)。
十.id类型, nil , Nil ,NULL和NSNULL的区别?
id类型: 是一个独特的数据类型,可以转换为任何数据类型,id类型的变量可以存放任何数据类型的对象,在内部处理上,这种类型被定义为指向对象的指针,实际上是一个指向这种对象的实例变量的指针; id 声明的对象具有运行时特性,既可以指向任意类型的对象
nil 是一个实例对象值;如果我们要把一个对象设置为空的时候,就用nil
Nil 是一个类对象的值,如果我们要把一个class的对象设置为空的时候,就用Nil
NULL 指向基本数据类型的空指针(C语言的变量的指针为空)
NSNull 是一个对象,它用在不能使用nil的场合
十一.C和 OC 如何混编&&Swift 和OC 如何调用?
1.xcode可以识别一下几种扩展名文件:
.m文件,可以编写 OC语言 和 C 语言代码
.cpp: 只能识别C++ 或者C语言(C++兼容C)
.mm: 主要用于混编 C++和OC代码,可以同时识别OC,C,C++代码
2.Swift 调用 OC代码
需要创建一个 Target-BriBridging-Header.h 的桥文件,在乔文件导入需要调用的OC代码头文件即可
3.OC 调用 Swift代码
直接导入 Target-Swift.h文件即可, Swift如果需要被OC调用,需要使用@objc 对方法或者属性进行修饰
十二.OC与 JS交互方式有哪些?
1.通过拦截URL
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = request.URL.absoluteString;
if ([url rangeOfString:@"需要跳转源生界面的URL判断"].location != NSNotFound) {
//跳转原生界面
return NO;
}
return YES;
}
2.使用MessageHandler(WKWebView)
当JS端想传一些数据给iOS.那它们会调用下方方法来发送.
window.webkit.messageHandlers.<方法名>.postMessage(<数据>)上方代码在JS端写会报错,导致网页后面业务不执行.可使用try-catch执行.
那么在OC中的处理方法如下.它是WKScriptMessageHandler的代理方法.name和上方JS中的方法名相对应.
- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;
3.JavaScriptCore (UIWebView)
使用三方库WebViewJavascriptBridge,可提供 js 调OC,以及OC掉JS
1. 设置 webViewBridge
_bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.webView];
[_bridge setWebViewDelegate:self];
2. 注册handler方法,需要和 前段协商好 方法名字,是供 JS调用Native 使用的。
[_bridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
// OC调用
NSString *scanResult = @"http://www.baidu.com";
// js 回调传参
responseCallback(scanResult);
}];
3. OC掉用JS
[_bridge callHandler:@"testJSFunction" data:@"一个字符串" responseCallback:^(id responseData) {
NSLog(@"调用完JS后的回调:%@",responseData);
}];
4.OC调用JS代码
// 直接运行 使用
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];
// 使用JavaScriptCore框架
#import
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//获取webview中的JS内容
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSString *runJS = @"执行的JS代码";
//准备执行的JS代码
[context evaluateScript:runJS];
}
十三.编译过程做了哪些事情
Objective,Swift都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。Objective,Swift二者的编译都是依赖于Clang + LLVM. OC和Swift因为原理上大同小异,知道一个即可!
1.iOS编译 不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。
2.编译器前端 :编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行
3.编译器后端 :编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。LVVM优化器会进行BitCode的生成,链接期优化等等,LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码。
十四.Category的实现原理&&使用场合&&Class Extension的区别
1.Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
2.在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
使用场合:
在不修改原有类代码的情况下,为类添对象方法或者类方法
或者为类关联新的属性
分解庞大的类文件
添加实例方法
添加类方法
添加协议
添加属性
关联成员变量
区别
Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中。
十五.Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构)
可以通过 runtime 动态的关联属性
十六.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
十七.initialize方法如何调用,以及调用时机
当类第一次收到消息的时候会调用类的initialize方法
是通过 runtime 的消息机制 objc_msgSend(obj,@selector()) 进行调用的
优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的
十八.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
load 是类加载到内存时候调用, 优先父类->子类->分类
initialize 是类第一次收到消息时候调用,优先分类->子类->父类
同级别和编译顺序有关系
load 方法是在 main 函数之前调用的
十九.什么是Runtime?平时项目中有用过么?
Objective-C runtime是一个运行时库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码,类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用。通过了解runtime以及源码,可以更加深入的了解OC其特性和原理
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
平时编写的OC代码,底层都是转换成了Runtime API进行调用
具体应用
利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题
二十.讲一下 OC 的消息机制
1.OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
2.objc_msgSend底层有3大阶段 消息发送(当前类、父类中查找)、动态方法解析、消息转发
消息发送流程
当我们的一个 receiver(实例对象)收到消息的时候, 会通过 isa 指针找到 他的类对象, 然后在类对象方法列表中查找 对应的方法实现,如果 未找到,则会通过 superClass 指针找到其父类的类对象, 找到则返回,未找打则会一级一级往上查到,最终到NSObject 对象, 如果还是未找到就会进行动态方法解析
类方法调用同上,只不过 isa 指针找到元类对象;
动态方法解析机制&&消息转发机制流程
当我们发送消息未找到方法实现,就会进入第二步,动态方法解析: 代码实现如下
// 动态方法绑定- 实例法法调用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
Method method = class_getInstanceMethod(self, @selector(test));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 类方法调用
+(BOOL) resolveClassMethod:(SEL)sel....
未找到动态方法绑定,就会进行消息转发阶段
// 快速消息转发- 指定消息处理对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [Student new];
}
return [super forwardingTargetForSelector:aSelector];
}
// 标准消息转发-消息签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(run))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//内部逻辑自己处理
}
答案摘自作者:iOS猿_员
原贴链接:https://www.jianshu.com/p/4aaf45c11082
收起阅读 »TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。面向连接意味着两个使用TCP的进程(一个客户和一个服务器)在交换数据之前必须先建立好连接,然后才能开始传输数据。建立连接时采用客户服务器模式,其中主动发起连接建立的进程叫做客户(Client),被动等待连接建立的进程叫做服务器(Server)。
TCP提供全双工的数据传输服务,这意味着建立了TCP连接的主机双方可以同时发送和接收数据。这样,接收方收到发送方消息后的确认可以在反方向的数据流中进行捎带。“端到端”的TCP通信意味着TCP连接发生在两个进程之间,一个进程发送数据,只有一个接收方,因此TCP不支持广播和组播。
TCP连接面向字节流,字节流意味着用户数据没有边界,例如,发送进程在TCP连接上发送了2个512字节的数据,接收方接收到的可能是2个512字节的数据,也可能是1个1024字节的数据。因此,接收方若要正确检测数据的边界,必须由发送方和接收方共同约定,并且在用户进程中按这些约定来实现。
二、UDP
UDP与TCP位于同一层,但与TCP不同
UDP协议提供的是一种无连接的、不可靠的传输层协议,只提供有限的差错检验功能。
它在IP层上附加了简单的多路复用功能,提供端到端的数据传输服务。
设计UDP的目的是为了以最小的开销在可靠的或者是对数据可靠性要求不高的环境中进行通信,
由于无连接,UDP支持广播和组播,这在多媒体应用中是非常有用的。
三、IP协议
IP(网际)协议是TCP/IP模型的核心,也是网络层最重要的协议。
网际层接收来自网络接口层的数据包,并将数据包发送到传输层;相反,也将传输层的数据包传送到网络接口层。
IP协议主要包括无连接数据报传送,数据报路由器选择以及差错处理等功能。
由于网络拥挤、网络故障等问题可能导致数据报无法顺利通过传输层。IP协议具有有限的报错功能,不能有效处理数据报延迟,不按顺序到达和数据报出错,所以IP协议需要与另外的协议配套使用,包括地址解析协议ARP、逆地址解析协议RARP、因特网控制报文协议ICMP、因特网组管理协议IGMP等。
IP数据包中含有源地址(发送它的主机地址)和目的地址(接收它的主机地址)。
IP协议对于网络通信而言有着重要的意义。由于网络中的所有计算机都安装了IP软件,使得许许多多的局域网构成了庞大而严密的通信系统,才形成了如今的Internet。其实,Internet并非一个真实存在的网络,而是一个虚拟网络,只不过是利用IP协议把世界上所有愿意接入Internet的计算机局域网络连接起来,使之能够相互通信。
链接:https://www.jianshu.com/p/b8b2220a8bd0
在APP的开发中,我们难免会遇到这种情况,一层层的打开下一级控制,这时,我们再想回到原始控制器时,一级级返回不太现实,所以我们需要一种方法,来一次性返回首页
从App的rootViewController开始,找到所有presentedController,然后逆序dismiss这些Controller,最后pop to rootViewController就可以了。
- (void)backToHomePage
{
UIWindow *window = [(AppDelegate *)[UIApplication sharedApplication].delegate window];
UIViewController *presentedController = nil;
UIViewController *rootController = [window rootViewController];
if ([rootController isKindOfClass:[UITabBarController class]]) {
rootController = [(UITabBarController *)rootController selectedViewController];
}
presentedController = rootController;
//找到所有presented的controller,包括UIViewController和UINavigationController
NSMutableArray *presentedControllerArray = [[NSMutableArray alloc] init];
while (presentedController.presentedViewController) {
[presentedControllerArray addObject:presentedController.presentedViewController];
presentedController = presentedController.presentedViewController;
}
if (presentedControllerArray.count > 0) {
//把所有presented的controller都dismiss掉
[self dismissControllers:presentedControllerArray topIndex:presentedControllerArray.count - 1 completion:^{
[self popToRootViewControllerFrom:rootController];
}];
} else {
[self popToRootViewControllerFrom:rootController];
}
}
- (void)dismissControllers:(NSArray *)presentedControllerArray topIndex:(NSInteger)index completion:(void(^)(void))completion
{
if (index < 0) {
completion();
} else {
[presentedControllerArray[index] dismissViewControllerAnimated:NO completion:^{
[self dismissControllers:presentedControllerArray topIndex:index - 1 completion:completion];
}];
}
}
- (void)popToRootViewControllerFrom:(UIViewController *)fromViewController
{
//pop to root
if ([fromViewController isKindOfClass:[UINavigationController class]]) {
[(UINavigationController *)fromViewController popToRootViewControllerAnimated:YES];
}
if (fromViewController.navigationController) {
[fromViewController.navigationController popToRootViewControllerAnimated:YES];
}
}
参考这个思路可以做一些其他非常规页面跳转,跳转到我们想要跳转的指定界面去
原文链接:https://blog.csdn.net/yinyignfenlei/article/details/86167245
收起阅读 »一、数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t
objc_object(id)
isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关
objc_class (class) 继承自objc_object
isa指针,共用体isa_t
struct cache_t {
struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
mask_t _mask;//分配用来缓存bucket的总数
mask_t _occupied;//表明目前实际占用的缓存bucket的个数
}
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
复制代码
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
复制代码
Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
复制代码
struct method_t {
SEL name; //名称
const char *types;//返回值和参数
IMP imp; //函数体
}
复制代码
二、 对象,类对象,元类对象
类对象存储实例方法列表等信息。
元类对象存储类方法列表等信息。
void objc_msgSend(void /* id self, SEL op, ... */ )
void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
复制代码
四、消息转发
+ (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
+ (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
复制代码
- (void)doesNotRecognizeSelector:(SEL)aSelector
这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来+ (void)load
{
Method test = class_getInstanceMethod(self, @selector(test));
Method otherTest = class_getInstanceMethod(self, @selector(otherTest));
method_exchangeImplementations(test, otherTest);
}
应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息
动态添加方法
class_addMethod(self, sel, testImp, "v@:");
void testImp (void)
{
NSLog(@"testImp");
}
@dynamic 动态方法解析
动态运行时语言将函数决议推迟到运行时
编译时语言在编译期进行函数决议
[obj foo]和objc_msgSend()函数之间有什么关系?
objc_msgSend()是[obj foo]的具体实现。在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。
runtime是如何通过selector找到对应的IMP地址的?
缓存查找-->当前类查找-->父类逐级查找
能否向编译后的类中增加实例变量?
不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。
但可以向动态添加的类中增加实例变量。
一、字符串反转
给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh
- (void)charReverse
{
NSString * string = @"hello,world";
NSLog(@"%@",string);
NSMutableString * reverString = [NSMutableString stringWithString:string];
for (NSInteger i = 0; i < (string.length + 1)/2; i++) {
[reverString replaceCharactersInRange:NSMakeRange(i, 1) withString:[string substringWithRange:NSMakeRange(string.length - i - 1, 1)]];
[reverString replaceCharactersInRange:NSMakeRange(string.length - i - 1, 1) withString:[string substringWithRange:NSMakeRange(i, 1)]];
}
NSLog(@"reverString:%@",reverString);
//C
char ch[100];
memcpy(ch, [string cStringUsingEncoding:NSUTF8StringEncoding], [string length]);
//设置两个指针,一个指向字符串开头,一个指向字符串末尾
char * begin = ch;
char * end = ch + strlen(ch) - 1;
//遍历字符数组,逐步交换两个指针所指向的内容,同时移动指针到对应的下个位置,直至begin>=end
while (begin < end) {
char temp = *begin;
*(begin++) = *end;
*(end--) = temp;
}
NSLog(@"reverseChar[]:%s",ch);
}
复制代码
二、链表反转
反转前:1->2->3->4->NULL
反转后:4->3->2->1->NULL
/** 定义一个链表 */
struct Node {
NSInteger data;
struct Node * next;
};
- (void)listReverse
{
struct Node * p = [self constructList];
[self printList:p];
//反转后的链表头部
struct Node * newH = NULL;
//头插法
while (p != NULL) {
//记录下一个结点
struct Node * temp = p->next;
//当前结点的next指向新链表的头部
p->next = newH;
//更改新链表头部为当前结点
newH = p;
//移动p到下一个结点
p = temp;
}
[self printList:newH];
}
/**
打印链表
@param head 给定链表
*/
- (void)printList:(struct Node *)head
{
struct Node * temp = head;
printf("list is : ");
while (temp != NULL) {
printf("%zd ",temp->data);
temp = temp->next;
}
printf("\n");
}
/** 构造链表 */
- (struct Node *)constructList
{
//头结点
struct Node *head = NULL;
//尾结点
struct Node *cur = NULL;
for (NSInteger i = 0; i < 10; i++) {
struct Node *node = malloc(sizeof(struct Node));
node->data = i;
//头结点为空,新结点即为头结点
if (head == NULL) {
head = node;
}else{
//当前结点的next为尾结点
cur->next = node;
}
//设置当前结点为新结点
cur = node;
}
return head;
}
复制代码
三、有序数组合并
将有序数组 {1,4,6,7,9} 和 {2,3,5,6,8,9,10,11,12} 合并为
{1,2,3,4,5,6,6,7,8,9,9,10,11,12}
- (void)orderListMerge
{
int aLen = 5,bLen = 9;
int a[] = {1,4,6,7,9};
int b[] = {2,3,5,6,8,9,10,11,12};
[self printList:a length:aLen];
[self printList:b length:bLen];
int result[14];
int p = 0,q = 0,i = 0;//p和q分别为a和b的下标,i为合并结果数组的下标
//任一数组没有达到s边界则进行遍历
while (p < aLen && q < bLen) {
//如果a数组对应位置的值小于b数组对应位置的值,则存储a数组的值,并移动a数组的下标与合并结果数组的下标
if (a[p] < b[q]) result[i++] = a[p++];
//否则存储b数组的值,并移动b数组的下标与合并结果数组的下标
else result[i++] = b[q++];
}
//如果a数组有剩余,将a数组剩余部分拼接到合并结果数组的后面
while (++p < aLen) {
result[i++] = a[p];
}
//如果b数组有剩余,将b数组剩余部分拼接到合并结果数组的后面
while (q < bLen) {
result[i++] = b[q++];
}
[self printList:result length:aLen + bLen];
}
- (void)printList:(int [])list length:(int)length
{
for (int i = 0; i < length; i++) {
printf("%d ",list[i]);
}
printf("\n");
}
复制代码
四、HASH算法
- (void)hashTest
{
NSString * testString = @"hhaabccdeef";
char testCh[100];
memcpy(testCh, [testString cStringUsingEncoding:NSUTF8StringEncoding], [testString length]);
int list[256];
for (int i = 0; i < 256; i++) {
list[i] = 0;
}
char *p = testCh;
char result = '\0';
while (*p != result) {
list[*(p++)]++;
}
p = testCh;
while (*p != result) {
if (list[*p] == 1) {
result = *p;
break;
}
p++;
}
printf("result:%c",result);
}
复制代码
五、查找两个子视图的共同父视图
思路:分别记录两个子视图的所有父视图并保存到数组中,然后倒序寻找,直至找到第一个不一样的父视图。
- (void)findCommonSuperViews:(UIView *)view1 view2:(UIView *)view2
{
NSArray * superViews1 = [self findSuperViews:view1];
NSArray * superViews2 = [self findSuperViews:view2];
NSMutableArray * resultArray = [NSMutableArray array];
int i = 0;
while (i < MIN(superViews1.count, superViews2.count)) {
UIView *super1 = superViews1[superViews1.count - i - 1];
UIView *super2 = superViews2[superViews2.count - i - 1];
if (super1 == super2) {
[resultArray addObject:super1];
i++;
}else{
break;
}
}
NSLog(@"resultArray:%@",resultArray);
}
- (NSArray *)findSuperViews:(UIView *)view
{
UIView * temp = view.superview;
NSMutableArray * result = [NSMutableArray array];
while (temp) {
[result addObject:temp];
temp = temp.superview;
}
return result;
}
复制代码
六、求无序数组中的中位数
中位数:当数组个数n为奇数时,为(n + 1)/2,即是最中间那个数字;当n为偶数时,为(n/2 + (n/2 + 1))/2,即是中间两个数字的平均数。
首先要先去了解一些几种排序算法:iOS排序算法
思路:
extension
是在编译器决定的,它就是类的一部分,在编译期和头文件里的 @interface
和 实现文件里的 @implementation
形成一个完整的类,它伴随类的的产生而产生,随着类的消亡而消亡。extension
一般用来隐藏类的私有信息,必须有类的源码才可以为一个类添加 extension
。所以无法为系统的类添加 extension
。category
是在运行期决定的,category
是无法添加实例变量的,extension
是可以添加的。我们首先来看以下 category
的基本使用:
// Person+Eat.h
#import "Person.h"
@interface Person (Eat)
- (void)eatBread;
+ (void)eatFruit;
@property (nonatomic, assign) int count;
@end
// Person+Eat.m
#import "Person+Eat.h"
@implementation Person (Eat)
- (void)eatBread {
NSLog(@"eatBread");
}
+ (void)eatFruit {
NSLog(@"eatFruit");
}
@end
复制代码
Person
的分类,专门实现吃这个功能NSCopying
和 NSCoding
count
属性我们通过 clang
编译器来观察一下在编译期这些代码的本质是什么?
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MyClass.m -o MyClass-arm64.cpp
复制代码
编译之后,我们可以发现 category
的本质是结构体 category_t
,无论我们创建了多少个 category
最终都会生成 category_t
这个结构体,并且 category
中的方法、属性、协议都是存储在这个结构体里的。也就是说在编译期,分类中成员是不会和类合并在一起的。
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
复制代码
name
:类的名字cls
:类instanceMethods
:category
中所有给类添加的实例方法的列表classMethods
:category
中所有给类添加的类方法的列表protocols
:category
中实现的所有协议的列表instanceProperties
:category
中添加的所有属性从 category
的定义中可以看到我们可以 添加实例方法,添加类方法,可以实现协议,可以添加属性。
不可以添加实例变量
我们继续研究下面的编译后的代码:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatBread", "v16@0:8", (void *)_I_Person_Eat_eatBread}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatFruit", "v16@0:8", (void *)_C_Person_Eat_eatFruit}}
};
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"count","Ti,N"}}
};
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat,
};
复制代码
_OBJC_$_CATEGORY_Person_$_Eat
结构体变量中的值,就是分别对应 category_t
的成员,第1个成员就是类名,因为我们声明了实例方法,类方法,遵守了协议,定义了属性,所以我们的结构体变量中这些都会有值。_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat
结构体表示实例方法列表,里面包含了 eatBread
实例方法_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat
结构体包含了 eatFruit
类方法_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat
结构体包含了 NSCoping
和 NSCoding
协议_OBJC_$_PROP_LIST_Person_$_Eat
结构体包含了 count
属性在研究完编译时期的 category
后,我们进而研究运行时期的 category
在 objc-runtime-new.mm
的源码中,我们可以最终找到如何将 category
中的方法列表,属性列表,协议列表添加到类中。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if ( ) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
复制代码
rwe->methods.attachLists(mlists, mcount);
rwe->protocols.attachLists(protolists, protocount);
rwe->properties.attachLists(proplists, propcount);
以上三个函数就是把 category
中的方法、属性和协议列表添加到类中的函数。
继续查看 attchLists
函数的实现:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
复制代码
memmove
和 memcpy
。memmove
函数的作用是移动内存,将之前的内存向后移动,将原来的方法列表往后移memcpy
函数的作用是内存的拷贝,将 category
中的方法列表复制到上一步移出来的位置。从上述源码中,可以发现 category
的方法并没有替换原来类已有的方法,如果 category
和原来类中都有某个同名方法,只不过 category
中的方法被放到了新方法列表的前面,在运行时查找方法的时候是按照顺序查找的,一旦找到该方法,就不会向下继续查找了,产生了 category
会覆盖原类方法的假象。
所以我们在
category
定义方法的时候都要加上前缀,以避免意外的重名把类本身的方法”覆盖“掉。
category
中存在同名的方法,运行时最终调用哪个方法是由编译器决定的,最后一个参与编译的方法将会先被调用。1.解析Info.plist
加载相关信息,例如如闪屏 沙箱建立、权限检查
2.Mach-O加载
如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为attribute((constructor))的C函数
加载类扩展(Category)中的方法
3.程序执行
调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching
影响启动性能的因素
main()函数之前耗时的影响因素
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load越多,启动越慢
main()函数之后耗时的影响因素
执行main()函数的耗时
执行applicationWillFinishLaunching的耗时
rootViewController及其childViewController的加载、view及其subviews的加载
优化
纯代码方式而不是storyboard加载首页UI。
对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载。
对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。
二.单例的利弊
优点:
1:一个类只被实例化一次,提供了对唯一实例的受控访问。
2:节省系统资源
3:允许可变数目的实例。
缺点:
1:一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”。
2:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
3:滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
三.TCP和UDP的区别于联系
TCP为传输控制层协议,为面向连接、可靠的、点到点的通信;
UDP为用户数据报协议,非连接的不可靠的点到多点的通信;
TCP侧重可靠传输,UDP侧重快速传输
四.TCP连接的三次握手
第一次握手:客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;
第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开 TCP 连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)
五.假如Controller太臃肿,如何优化?
1.将网络请求抽象到单独的类中
方便在基类中处理公共逻辑;
方便在基类中处理缓存逻辑,以及其它一些公共逻辑;
方便做对象的持久化。
2.将界面的封装抽象到专门的类中
构造专门的 UIView 的子类,来负责这些控件的拼装。这是最彻底和优雅的方式,不过稍微麻烦一些的是,你需要把这些控件的事件回调先接管,再都一一暴露回 Controller。
3.构造 ViewModel
借鉴MVVM。具体做法就是将 ViewController 给 View 传递数据这个过程,抽象成构造 ViewModel 的过程
4.专门构造存储类
专门来处理本地数据的存取。
5.整合常量
六.对程序性能的优化你有什么建议?
1.使用复用机制
2.尽可能设置 View 为不透明
3.避免臃肿的 XIB 文件
4.不要阻塞主线程
5.图片尺寸匹配 UIImageView
6.选择合适的容器
7.启用 GZIP 数据压缩
8.View 的复用和懒加载机制
9、缓存
服务器的响应信息(response)。图片。计算值。比如:UITableView 的 row heights。
10.关于图形绘制
11.处理 Memory Warnings
在 AppDelegate 中实现 - [AppDelegate applicationDidReceiveMemoryWarning:] 代理方法。
在 UIViewController 中重载 didReceiveMemoryWarning 方法。
监听 UIApplicationDidReceiveMemoryWarningNotification 通知。
12.复用高开销的对象
13.减少离屏渲染(设置圆角和阴影的时候可以选用绘制的方法)
14.优化 UITableView
通过正确的设置 reuseIdentifier 来重用 Cell。
尽量减少不必要的透明 View。
尽量避免渐变效果、图片拉伸和离屏渲染。
当不同的行的高度不一样时,尽量缓存它们的高度值。
如果 Cell 展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的 response。
使用 shadowPath 来设置阴影效果。
尽量减少 subview 的数量,对于 subview 较多并且样式多变的 Cell,可以考虑用异步绘制或重写 drawRect。
尽量优化 - [UITableView tableView:cellForRowAtIndexPath:] 方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。
选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
对于 rowHeight、sectionFooterHeight、sectionHeaderHeight 尽量使用常量。
15.选择合适的数据存储方式
在 iOS 中可以用来进行数据持有化的方案包括:
NSUserDefaults。只适合用来存小数据。
XML、JSON、Plist 等文件。JSON 和 XML 文件的差异在「选择正确的数据格式」已经说过了。
使用 NSCoding 来存档。NSCoding 同样是对文件进行读写,所以它也会面临必须加载整个文件才能继续的问题。
使用 SQLite 数据库。可以配合 FMDB 使用。数据的相对文件来说还是好处很多的,比如可以按需取数据、不用暴力查找等等。
使用 CoreData。也是数据库技术,跟 SQLite 的性能差异比较小。但是 CoreData 是一个对象图谱模型,显得更面向对象;SQLite 就是常规的 DBMS。
16.减少应用启动时间
快速启动应用对于用户来说可以留下很好的印象。尤其是第一次使用时。
保证应用快速启动的指导原则:
尽量将启动过程中的处理分拆成各个异步处理流,比如:网络请求、数据库访问、数据解析等等。
避免臃肿的 XIB 文件,因为它们会在你的主线程中进行加载。重申:Storyboard 没这个问题,放心使用。
注意:在测试程序启动性能的时候,最好用与 Xcode 断开连接的设备进行测试。因为 watchdog 在使用 Xcode 进行调试的时候是不会启动的。
17.使用 Autorelease Pool (内存释放池)
18.imageNamed 和 imageWithContentsOfFile
七.使用drawRect有什么影响?
drawRect方法依赖Core Graphics框架来进行自定义的绘制
缺点:它处理touch事件时每次按钮被点击后,都会用setNeedsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了
这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制
八.基于CTMediator的组件化方案,有哪些核心组成?
假如主APP调用某业务A,那么需要以下组成部分:
CTMediator类,该类提供了函数 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
这个函数可以根据targetName生成对象,根据actionName构造selector,然后可以利用performSelector:withObject:方法,在目标上执行动作。
业务A的实现代码,另外要加一个专门的类,用于执行Target Action
类的名字的格式:Target_%@,这里就是Target_A。
这个类里面的方法,名字都以Action_开头,需要传参数时,都统一以NSDictionary*的形式传入。
CTMediator类会创建Target类的对象,并在对象上执行方法。
业务A的CTMediator扩展
扩展里声明了所有A业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。
在扩展的实现里,对Target, Action需要通过硬编码进行指定。由于扩展的负责方和业务的负责方是相同的,所以这个不是问题。
九.为什么CTMediator方案优于基于Router的方案?
Router的缺点:
在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。
在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。
在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。
为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的
CTMediator的优点:
调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。
组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。
方便传递各种类型的参数。
十.内存的使用和优化的注意事项
重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用;
尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;
不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多;
选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。
延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。
数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。
处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。
避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要;
使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;
正确选择图片加载方式:UIImage加载方式
摘自作者:iOS猿_员
原贴链接:https://www.jianshu.com/p/4b4bd4e3feff
一. Runloop和线程的关系?
1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建
2.runloop在第一次获取时创建,在线程结束时销毁.
runloop 的运行逻辑就是 do-while 循环下运用观察者模式(或者说是消息发送),根据7种状态的变化,处理事件输入源和定时器。如下图
runloop的应用:
1.NSTimer在子线程开启一个定时器;控制定时器在特定模式下执行
2.imageView的显示
3.performSelector
4.常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
5.自动释放池
二.自动释放池什么时候释放?
第一次创建:启动runloop时候
最后一次销毁:runloop退出的时候
其他时候的创建和销毁:当runloop即将睡眠时销毁之前的释放池,重建一个新的
三.什么时候使用weak关键字,和assign的区别?
1.arc中有可能出现循环引用的地方,比如delegate属性
2.自定义IBOutlet空间属性一般也是使用weak
区别:weak表明一种非持有关系,必须用于oc对象;assign用于基本数据类型
weak修饰的指针默认是nil,如果用assign修饰对象,在对象被销毁时,会产生野指针,容易发生崩溃
四.objc中向一个nil对象发送消息将会发生什么?
在oc中向nil发送消息是完全有效的,只是在运行时不会有任何作用,如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil),如果向一个nil对象发送消息,首先寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误.
五.runtime如何实现weak变量的自动置nil
runtime对注册的类,会进行布局,对于weak对象会放入一个hash表中,用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil
六.runtime如何通过selector找到对应的IMP地址?
每一个类对象都有一个方法列表,方法列表中记录着方法名称.方法实现.参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现
七.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
1.不能向编译后得到的类中增加实例变量,可以向运行时创建的类中添加实例变量
原因: (1)因为编译后的类已经注册在runtime中,类结构中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同事runtime会调用class_setIvarLayout 或者class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量
(2)运行时创建的的类可以添加实例变量,调用class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上
八.kvo的实现原理?
当你观察一个对象时,一个新的类会被动态创建,这个类继承自该对象的原本的类,并重写了被观察属性的setter方法,重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值得更改,最后通过isa混写,把这个对象的isa指针(isa指针告诉runtime系统这个对象的类是什么)指向这个新创建的子类,对象就神奇的变成了新创建子类的实例
实现原理如下图:
九.谈谈你对kvc的理解
kvc可以通过key直接访问对象属性,或者给对象的属性赋值,这样可以在运行时动态访问或者改变对象的属性值
当调用setValue:属性值forKey:@"name"的代码时,底层执行机制如下:
1.程序优先调用set
2.如果没有找到setName:方法,kvc机制会检查+(Bool)accessInstanceVariablesDiretly方法,有没有返回yes,默认是返回yes,如果重写了该方法切返回NO的话,那么这一步会直接执行setValue:forUnderfindKey:方法, 如果返回Yes,那么kvc机制会搜索该类里面有没有名为
3.如果该类即没有set
4.和上面一样,如果该类即没有set:方法,也没有_和_is成员变量,kvc机制会继续搜索和is的成员变量,再给它赋值
5.如果以上列出的方法和成员变量都不存在,系统将执行setValue:forUnderfindKey:方法,默认是抛出异常.
十.Notification和KVO的区别
1.KVO提供一种机制,当指定的被观察对象属性被修改后,kvo会自动通知响应的观察者
2.通知:是一种广播机制,在事件发生的时候,通过通知中心对象,一个对象能够为所关心这个事件发生的对象发送消息,两者都是观察者模式,不同在于kvo是被观察者直接发送消息给观察者,是对象间的直接交互.通知则是两者都和通知中心对象交互,对象之间不知道彼此
3.本质区别,底层原理不一样,kvo基于runtime,通知则有个通知中心来进行通知
十一.如果让你设计一个通知中心,设计思路
1.创建通知中心单例类,并在里面有一个保存全局NSDictionary
2.对于注册通知的类,将注册通知名作为key,执行的方法和类,以及一些参数作为一个数组为值
3.发送通知可以调用通知中心,通过字典key(通知名)找到对应的类和方法进行调用传值
十二.atomic和nonatomic区别,以及作用?
atomic与nonatom的主要区别就是系统自动生成的getter/setter方法不一样 ,atomic系统自动生成的getter/setter方法会进行加锁操作,nonatomic系统自动生成的getter/setter方法不会进行加锁操作
atomic不是线程安全的
系统生成的getter/setter方法会进行加锁操作,注意:这个锁仅仅保证了getter和setter存取方法的线程安全.因为getter/setter方法有加锁的缘故,故在别的线程来读写这个属性之前,会先执行完当前操作
atomic可以保证多线程访问时候,对象是未被其他线程销毁(比如:如果当一个线程正在get或set时,又有另一个线程同时在进行release操作,可能会直接crash)
十三.说一下静态库和动态库之间的区别
静态库:以.a 和 .framework为文件后缀名。链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。
动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存
静态库.a 和 framework区别.a 主要是二进制文件,不包含资源,需要自己添加头文件.framework 可以包含头文件+资源信息
十四.遇到过BAD_ACCESS的错误吗?你是怎样调试的?
BAD_ACCESS 报错属于内存访问错误,会导致程序崩溃,错误的原因是访问了野指针(悬挂指针)。
常规操作如下:
设置全局断点快速定位问题代码所在行。
开启僵尸对象诊断
Analyze分析
重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object。
Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer
十五.说一下iOS 中的APNS,远程推送原理?
Apple push Notification Service,简称 APNS,是苹果的远程消息推送,原理如下:
iOS 系统向APNS服务器请求手机端的deviceToken
App 接收到手机端的 deviceToken,然后传给 App 对应的服务器.
App 服务端需要发送推送消息时, 需要先通过 APNS 服务器
然后根据对应的 deviceToken 发送给对应的手机
十六.UITableView的优化
1.重用cell
2.缓存行高(在请求到数据的时候提前计算好行高,用字典缓存好高度)
3.加载网络图片,使用异步加载,并缓存,下载的图片根据显示大小切成合适大小的图,查看大图时再显示大图,服务端最好处理好大图和小图,延时加载,当滚动很快时避免频繁请求,可通过runloop设置defultMode状态下渲染请求
4.局部刷新,减少全局刷新
5.渲染,尽量不要使用透明图层,将cell的opaque值设为Yes,背景色和子View不要使用透明色,减少阴影渐变,圆角等
6.少用addSubview给cell动态添加子View,初始化时直接设置好,通过hidden控制显示隐藏,布局在初始化直接布局好,避免cell的重新布局
7.按需加载cell,滚动很快时,只加载范围内的cell,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后定制n行加载,按需加载,提高流畅性方法如下
8.遇到复杂界面,需要异步绘制,给自定义的cell添加draw方法,在方法中利用GCD异步绘制,或者直接重写drawRect方法,此外,绘制cell不建议使用UIView,建议使用CALayer,UIView的绘制是建立在CoreGraphic上的,使用的是cpu,CALayer使用的是core Animation,CPU.GPU通吃.由系统决定使用哪一个,view的绘制使用的自下向上的一层层的绘制,然后渲染layer处理的是Texure,利用GPU的Texure Cache和独立的浮点数计算单元加速纹理的处理,GPU不喜欢透明,所以绘图一定要弄成不透明,对于圆角和阴影截一个伪透明的小图绘制上去,在layer回调里一定只做绘图,不做计算
cell被重用时,内部绘制的内容并不会自动清除,因此需要调用setNeedsDisplay或者setNeedsDisplayLayInRect:方法
十七.离屏渲染
下面的情况或操作会引发离屏渲染:
1.为图层设置遮罩(layer.mask)
2.将图层的layer.masksToBounds/view.clipsToBounds属性设置为ture
3.将图层layer,allowsGroupOpacity属性设置为Yes和layer.opacity小于1.0
4.给图层设置阴影(layer.shadow)
5.为图层设置layer.shouldRasterize=Yes(光栅化)
6.具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层(圆角,抗锯齿)
7.使用CGContext在drawRect:方法中绘制大部分情况会导致离屏渲染,甚至是一个空的实现
优化方案
圆角优化:使用CAShapeLayer和UIBezierPath设置圆角;直接中间覆盖一张为圆形的透明图片
shadow优化:使用shadowPath指定layer阴影效果路径,优化性能
使用异步进行layer渲染(Facebook开源异步绘制框架AsncDisplayKit)
设置layer的opaque值为Yes,减少复杂图层合成
尽量使用不包含透明(alpha)通道的图片资源
尽量设置layer的大小为整型值
十八.UIView和CALayer区别
1.UIView可以响应事件,CALayer不可以,UIView继承自UIResponder,在UIResponder中定义了处理各种事件的事件传递接口。而CALayer直接继承NSObject,并没有相应的处理事件接口。
2.一个CALayer的frame是由它的anchorPoint(锚点),position,bounds,和transform共同决定,而一个view的frame只是简单的返回layer的frame,同样view的center和bounds也是返回layer的一些属性
3..UIView主要是对显示内容的管理,而CALayer主要是侧重显示内容的绘制。UIView是CALayer的CALayerDelegate。
4.每一个view内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的CALayer提供,两者都有树状层级结构,layer内部有subLayers,view内部有subviews。
5.两者最明显的区别是view可以接受并处理事件,而Layer不可以。View是Layer的代理Delegate。
十九.iOS应用程序生命周期
ios程序启动原理(过程):如下:
二十.view视图生命周期
今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误:
Android sdkmanager tool not found
(/Users/xx/android-sdk/tools/bin/sdkmanager).
Try re-installing or updating your Android SDK,
visit https://flutter.io/setup/#android-setup for detailed instructions.
PS:VScode和AS都要记得装flutter插件,AS还要另外装dart插件
前言
今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在stackoverflow
找到了解决方案,废话不多说,直接看解决方案。
正文
1. 构建WKWebView
self.webView = [[WKWebView alloc] init];
// 创建请求
NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.jianshu.com"]];
// 加载网页
[self.webView loadRequest:request];
self.webView.scrollView.scrollEnabled = NO;
self.webView.scrollView.bounces = NO;
self.webView.scrollView.showsVerticalScrollIndicator = NO;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
// 将webView添加到界面
[self.contentView addSubview:self.webView];
2. cell高度适应WKWebView的内容
cell.webView.navigationDelegate = self;
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[webView evaluateJavaScript:@"document.body.offsetHeight" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 计算webView高度
self.webViewCellHeight = [result doubleValue];
// 刷新tableView
[self.tableView reloadData];
}];
}
3. 解决加载空白问题
原因:由于WKWebView采用的lazy加载模式,所在的scrollView的滚动被禁用,导致被嵌套的WKCompositingView不进行数据加载。
详细细节请参考:WKWebView刷新机制小探
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 判断webView所在的cell是否可见,如果可见就layout
NSArray *cells = self.tableView.visibleCells;
for (UITableViewCell *cell in cells) {
if ([cell isKindOfClass:[TraitWebViewCell class]]) {
TraitWebViewCell *webCell = (TraitWebViewCell *)cell;
[webCell.webView setNeedsLayout];
}
}
}
struct NSObject_IMPL {
Class isa;
};
在64位机中,一个isa占8个字节,在32位机中,一个isa占4个字节(当然苹果后面的机型都是64位的,这里我们着重讲解64位机)
int main(int argc, char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
//定义一个objc
NSObject *objc = [[NSObject alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([NSObject class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));
}
}
其打印结果为:
objc打印结果
我们先来认识一下class_getInstanceSize、malloc_size的区别
1.class_getInstanceSize:是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)即实例对象的大小->结构体内存对齐
2.创建的对象【至少】需要的内存大小不考虑malloc函数的话,内存对齐一般是以【8】对齐
3.#import <objc/runtime.h>使用这个函数时倒入runtime运行时
malloc_size:堆空间【实际】分配给对象的内存大小 -系统内存对齐
struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
};
也就是说,继承关系,子类直接将父类的isa引用进来
@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;
}
@end
其底层C++编译结构体就变成了
struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
};
//定义一个objc
Student *objc = [[Student alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([Student class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));
2020-09-08 12:35:27.158568+0800 OC底层[1549:79836] tu-24
2020-09-08 12:35:27.159046+0800 OC底层[1549:79836] tu-32
由于创建对象的时候,内存是以8对齐,上面我们讲到一个对象里面包含了一个isa占8个字节,对于student来说它有四个成员变量,isa,age,no,tc,共占8+4+4+4=20字节,但是由于内存以8对齐的原因,我们看到的输出是24,
所以class_getInstanceSize在计算实例大小的时候就是24,其白色区域表示空出了四个字节
@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSArray *array;
@end
其在底层C++编译就变成了
struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
NSString _name;
NSArray _array;
};
默认的会将属性生成的_name添加进结构体中,计算相应的大小
崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x14df0bd0> was mutated while being enumerated.'
当程序出现这个提示的时候,是因为你一边便利数组,又同时修改这个数组里面的内容,导致崩溃,网上的方法如下:
NSMutableArray * arrayTemp = xxx;
NSArray * array = [NSArray arrayWithArray: arrayTemp];
for (NSDictionary * dic in array) {
if (condition){
[arrayTemp removeObject:dic];
}
}
这种方法就是在定义一个一模一样的数组,便利数组A然后操作数组B
今天终于找到了一个更快接的删除数组里面的内容以及修改数组里面的内容的方法:
NSMutableArray *tempArray = [[NSMutableArray alloc]initWithObjects:@"12",@"23",@"34",@"45",@"56", nil];
[tempArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj isEqualToString:@"34"]) {
*stop = YES;
if (*stop == YES) {
[tempArray replaceObjectAtIndex:idx withObject:@"3333333"];
}
}
if (*stop) {
NSLog(@"array is %@",tempArray);
}
}];
利用block来操作,根据查阅资料,发现block便利比for便利快20%左右,这个的原理是这样的:
找到符合的条件之后,暂停遍历,然后修改数组的内容
转自:https://www.cnblogs.com/rglmuselily/p/6249015.html
苹果最近废弃了UIWebView的使用,所以要把工程中引用UIWebView的地方全换掉,不然每次提交审核都会发警告邮件,如下:
ITMS-90809: Deprecated API Usage - App updates that use UIWebView will no longer be accepted as of December 2020\. Instead, use WKWebView for improved security and reliability. Learn more https://developer.apple.com/documentation/uikit/uiwebview
After you’ve corrected the issues, you can upload a new binary to App Store Connect.
1.自己代码中使用了UIWebView控件。
2.第三方库中使用:
1). README.md等资源文件中使用。
这些文件是没有引入项目的,要在pod库里找到相应的库文件夹,然后 show in finder便能找到。
2). 第三方库的注释里有使用UIWebView字眼。
3). 第三方framewok、.a文件等包含UIWebView,都是二进制文件(Binary file),这种情况只能等第三方库更新SDK了。
3.工程的一些本地配置里包含了UIWebView
搜索结果: UserInterfaceState.xcuserstate matches
1.UserInterfaceState.xcuserstate是什么?
该文件为xcode默认自带文件,是xcode的配置信息,git会用这个文件记录下来。 比如:手动删除此文件,退出xcode后重启xcode,此文件会自动创建并跟踪, git push的时候一般忽略此文件
解决:
场景1:
直接搜索替换成WKWebView即可
场景2:
注释和README文件里使用的UIWebView字眼应该是没影响的。至于frameword和.a文件中包含的引用只能等第三方库更新了,例如Twitter的SDK。
场景3:
xcode的配置信息文件,对于打出来的包应该页没啥影响。为了保险起见,还是删掉此文件,然后让Xcode重新生成一个新的。
检测项目中是否包含UIWebView
1.打开终端,cd + 把项目的工程文件所在文件夹拖入终端(即 得到项目的工程文件所在的路径)
2.输入以下命令(注意最后有个点号,而且点号和 UIWebView 之间必须有一个空格):
grep -r UIWebView .
3.以上操作都正确的话,会马上出现工程中带有 UIWebView 的文件的列表(包括在工程中无法搜索到的 .a 文件中对UIWebView 的引用),如下:
替换TwitterKit
在pod文件中,把 pod 'TwitterKit' 替换为 pod 'TwitterKit5'
进入TwitterSDK的github地址 https://github.com/twitter-archive/twitter-kit-ios/issues/120,可以看到如下信息:
我们在开发中,常常遇到这样的问题,点击某一个TabBar后,本TabBar上的控制器页面数据不刷新,原因是因为在App启动之后,第一次点击本TabBar后页面已经走了viewDidLoad,所以除了重新启动不会再次走viewDidLoad,如果把请求方法写在viewDidLoad中,当然不会再次触发啦,但是,苹果早考虑到这个问题,不用咱们写通知事件什么的,废话有点多了,看代码详解:
首先需要在本控制器签订TabBar的协议
UITabBarControllerDelegate
一定要看清楚协议,如果警报 Assigning to 'id<UITabBarControllerDelegate> _Nullable' from incompatible type 'RCFollowOrderViewController *const __strong'那么就证明你的协议签成了UITabBarDelegate
在viewDidLoad 请求一次
[self requestdata];
协议方法:
//点击的时候触发的方法
-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController{
if (self.tabBarController.selectedIndex==1) {
[self requestdata];
}
}
//防止同一个页面一直点击tabbar 的方法
-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController{
UIViewController *tbselect=tabBarController.selectedViewController;
if([tbselect isEqual:viewController]){
returnNO;
}
returnYES;
}
如果想要点击TabBar一次,就刷新一次界面,就不写防止重复点击的代理方法,试下效果,搞定!
昨天领导反馈app微信分享到朋友圈出现“未验证应用”的提示信息。通过追踪找到了解决办法。
问题的原因
由于苹果iOS 13系统版本安全升级,为此openSDK在1.8.6版本进行了适配。 1.8.6版本支持Universal Links方式跳转,对openSDK分享进行合法性校验。
PS:现在openSDK出了最新的版本1.8.7,新增了自检函数checkUniversalLinkReady:,可以帮助开发者排查SDK接入过程中遇到的问题,在哪一步出错了(共7步,见接入文档https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4)。
问题的解决办法
1.配置Universal Links
1)常见并编辑一个名为apple-app-site-association,无需后缀名,务必符合标准的json格式,格式如下:
{
"applinks": {
"apps":[],
"details": [
{
"appID": "你的app的teamID + Bundle Identifier",
"paths": ["*"]
}
]
}
}
2)将apple-app-site-association文件发给服务器端的同事,让他上传到域名的根目录下或者.well-known的子目录下(这里的域名必须要是可访问的域名,由服务器端的同事给到)。
2.在app里面配置通用链接
1)首先检查一下Xcode-Targets-Signing&Capabilities 是否有Associated Domains,如果没有,需要去开发者账号在identifer里选择跟当前Xcode所用bundle identifier相同的那一组,进去之后,将Associated Domains前面的方框打上勾,如果已经打勾了,配置如下:
2)实现AppDelegate里支持通用链接的实现方法
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray
return [WXApi handleOpenUniversalLink:userActivity delegate:self];
}
3)修改微信注册方法
由[WXApi registerApp:kAppid]改为[WXApi registerApp:kAppid universalLink:kUniversalLinks],这里的universal links为第一步第2)里服务器同事给的链接地址
4)配置info.plist
这里如果是从旧版更新WechatOpenSDK1.8.6/7版本的话,需要在这个里面调用微信里的这个方法,并且在Xcode中,选择你的工程设置项,选中“TARGETS”一栏,在 “info”标签栏的“LSApplicationQueriesSchemes“添加weixin和weixinULAPI
5) 微信开放平台配置Universal links
需要把服务器同事给的地址填写到app iOS信息Universal Links,同时app的下载地址也一定要填写app在app store的地址,填写好后保存,开放平台需要审核,可能要等一段时间Universal Links才能生效,我就是昨天下午设置好没生效,今天早上来才生效。
3.检查是否配置好的Universal links是否生效
生效的标准结合这两个文档https://docs.qq.com/doc/DZHNvSGJLS3pqbHNl的步骤和https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4提供的自检过程
4.如果以上步骤都已经完成,并且自检正确,再分享出去,就不会再出现“未验证的应用”字样了。
PS:上周五(8-7)按照上述步骤,不会出现“未验证的应用”字样,到了周一(8-11)分享,发现又会出现,本人手机系统iOS13.5.1,微信7.0.14;换了同事的手机iOS13.1.2,微信7.0.14发现她的就正常,这个问题需要持续关注,个人觉得是微信sdk的bug;打包上线后,等到用户用一段时间后,新版本放量上去让整体错误率下降到90%以下才会从未验证应用中移除,问题得到彻底解决。
最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。
主要实现图中所示的功能,话不多少搞起!
第一次写大神们多包涵呀
下面这个方法计算传进来的字符串数组,实现每个字符串长度的计算,并做换行判断,每一行存进统计数组中
//将标签数组根据type以及其他参数进行分组装入数组
- (void)disposeTags:(NSArray *)aryName aryCount:(NSArray *)aryCount{
NSMutableArray *tags = [NSMutableArray new];//纵向数组
NSMutableArray *subTags = [NSMutableArray new];//横向数组
float originX = _tagOriginX;
for (NSString *tagTitle in aryName) {
NSUInteger index = [aryName indexOfObject:tagTitle];
//计算每个tag的宽度
CGSize contentSize = [tagTitle fdd_sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(self.frame.size.width-_tagOriginX*2, MAXFLOAT)];
NSMutableDictionary *dict = [NSMutableDictionary new];
dict[@"tagTitle"] = tagTitle;//标签标题
dict[@"tagCount"] =aryCount[index];
dict[@"viewWith"] = [NSString stringWithFormat:@"%f",contentSize.width+_tagSpace+30];//标签的宽度
if (index == 0) {
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
} else {
if (originX + contentSize.width > self.frame.size.width-_tagOriginX*2) {
//当前标签的X坐标+当前标签的长度>屏幕的横向总长度则换行
[tags addObject:subTags];
//换行标签的起点坐标初始化
originX = _tagOriginX;
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
subTags = [NSMutableArray new];
[subTags addObject:dict];
} else {
//如果没有超过屏幕则继续加在前一个数组里
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
}
}
if (index +1 == aryName.count) {
//最后一个标签加完将横向数组加到纵向数组中
[tags addObject:subTags];
disposeAry = tags;
}
//标签的X坐标每次都是前一个标签的宽度+标签左右空隙+标签距下个标签的距离
originX += contentSize.width+_tagHorizontalSpace+_tagSpace+30;
}
}
下面这个方法是计算字符串长度的封装方法,只限字符串计算,用的话可以直接搬走
#pragma mark - 扩展方法
@implementation NSString (FDDExtention)
- (CGSize)fdd_sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
CGSize resultSize;
if ([self respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSDictionary *attributes = @{ NSFontAttributeName:font };
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin;
NSStringDrawingContext *context;
[invocation setArgument:&size atIndex:2];
[invocation setArgument:&options atIndex:3];
[invocation setArgument:&attributes atIndex:4];
[invocation setArgument:&context atIndex:5];
[invocation invoke];
CGRect rect;
[invocation getReturnValue:&rect];
resultSize = rect.size;
} else {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(sizeWithFont:constrainedToSize:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(sizeWithFont:constrainedToSize:)];
[invocation setArgument:&font atIndex:2];
[invocation setArgument:&size atIndex:3];
[invocation invoke];
[invocation getReturnValue:&resultSize];
}
return resultSize;
}
字符串长度计算完成了,下面就可以进行所有字符串排列的高度了。其实这个蛮简单的 ,统计数组有多少个元素就代表标签排布有多少行。
//获取处理后的tagsView的高度根据标签的数组
(float)getDisposeTagsViewHeight:(NSArray *)ary {
float height = 0;
if (disposeAry.count > 0) {
height = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);
}
return height;
}
下面这个将标签加载到view上了,并实现赋值。
-(void)setTagAryName:(NSArray *)tagAryName aryCount:(NSArray *)aryCount delegate:(id)delegate{
_tagDelegate=delegate;
[self disposeTags:tagAryName aryCount:aryCount];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, 0, 200, 30)];
label.text=@"事件类别统计:";
label.textColor=_titleColor;
label.textAlignment=NSTextAlignmentLeft;
label.adjustsFontSizeToFitWidth=YES;
[self addSubview:label];
//遍历标签数组,将标签显示在界面上,并给每个标签打上tag加以区分
for (NSArray *iTags in disposeAry) {
NSUInteger i = [disposeAry indexOfObject:iTags];
for (NSDictionary *tagDic in iTags) {
NSUInteger j = [iTags indexOfObject:tagDic];
NSString *tagTitle = tagDic[@"tagTitle"];
float originX = [tagDic[@"originX"] floatValue];
float viewWith = [tagDic[@"viewWith"] floatValue];
NSString *count = tagDic[@"tagCount"];
UIView *NCView=[[UIView alloc]initWithFrame:CGRectMake(originX, _tagOriginY+i*(_tagHeight+_tagVerticalSpace), viewWith, _tagHeight)];
[self addSubview:NCView];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame=CGRectMake(0, 0, viewWith-30, _tagHeight);
button.layer.borderColor = _borderColor.CGColor;
button.layer.borderWidth = _borderWidth;
button.layer.masksToBounds = _masksToBounds;
button.layer.cornerRadius = _cornerRadius;
button.titleLabel.font = [UIFont systemFontOfSize:_titleSize];
[button setTitle:tagTitle forState:UIControlStateNormal];
[button setTitleColor:_titleColor forState:UIControlStateNormal];
[button setBackgroundImage:_normalBackgroundImage forState:UIControlStateNormal];
[button setBackgroundImage:_highlightedBackgroundImage forState:UIControlStateHighlighted];
button.tag = i*iTags.count+j;
[button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(viewWith-30, 0, 30, _tagHeight)];
label.text=[NSString stringWithFormat:@"X%@",count];
label.textAlignment=NSTextAlignmentCenter;
label.adjustsFontSizeToFitWidth=YES;
label.textColor=_titleColor;
[NCView addSubview:button];
[NCView addSubview:label];
}
}
self.countLabel=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, [self getDisposeTagsViewHeight:disposeAry], 100, 30)];
self.countLabel.text=[NSString stringWithFormat:@"总计:%ld次",_times];
self.countLabel.textAlignment=NSTextAlignmentLeft;
self.countLabel.adjustsFontSizeToFitWidth=YES;
self.countLabel.textColor=_titleColor;
[self addSubview:self.countLabel];
if (disposeAry.count > 0) {
float contentSizeHeight = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);
self.contentSize = CGSizeMake(self.frame.size.width,contentSizeHeight);
}
if (self.frame.size.height <= 0) {
self.frame = CGRectMake(CGRectGetMinX([self frame]), CGRectGetMinY([self frame]), CGRectGetWidth([self frame]), [self getDisposeTagsViewHeight:disposeAry]+30);
}
}
说了这么多,如何调用呢? 那么亮点来了 只需要传进来数组就实现图中的功能。
//计算出全部展示的高度,让maxHeight等于计算出的高度即可,初始化不需要设置高度
NSArray *tagAryCount=@[@"10",@"8",@"7",@"9",@"2",@"4",@"5",@"3",@"4"];
NSArray* tagAryName = @[@"黑色玫瑰",@"比尔沃吉特",@"钢铁烈阳",@"德玛西亚",@"祖安",@"巨神峰",@"雷瑟守备祖安",@"诺克萨斯暗影岛",@"弗雷尔卓德"];
ZHLbView * tagsView = [[ZHLbView alloc] initWithFrame:CGRectMake(0, 200, self.view.frame.size.width, 0)];
[tagsView setTagAryName:tagAryName aryCount:tagAryCount delegate:self];
[self.view addSubview:tagsView];
补充,在demo编写的过程中 ,遇到了一些小问题,就是scrollView如果是VC的第一个子视图的话 其总是要有一个64高度的空白区。
类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用
@interface Person : NSObject
-(void)perMethod;
+(void)perEat;
@end
@implementation Person
-(void)perMethod{
}
+(void)perEat{
}
@end
/// student继承自Person
@interface Student : Person
-(void)StudentMethod;
+(void)StudentEat;
@end
@implementation Student
-(void)StudentMethod{
}
+(void)StudentEat{
}
Student *student = [[Student alloc]init];
[student StudentMethod]
Student *student = [[Student alloc]init];
[student perMethod];当子类调用父类的实例方法的时候,子类的class类对象的superclass指针指向父类,直至基类(NSObject)找到方法并执行(注意,这里指的是实例方法,也就是减号方法)
三.元类对象的superclass 指针
当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用
[Student perEat];
总的来说,isa,superclass的的关系可以用一副经典的图来表示
OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)
NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];
NSLog(@"instance----%p %p",objc1,objc2);
instance实例对象存储的信息:
1.isa指针
Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);
打印结果
2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个
类对象存储的信息:
1.isa指针
2.superclass指针
3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)
1.元类对象的获取
Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);
打印指针地址
NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0
获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个
Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];
无论class几次,它返回的始终是类对象
2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140
元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)
从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空
将原来的load方法换成initialize
相信大家在想什么叫第一次接收消息了,我们回到main()
从输出结果可以看到没有任何关于initialize的打印,程序直接退出
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
}
return 0;
}
运行结果:
2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0
多次向TCPerson发送消息的输出结果
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}
输出结果:
2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0
我们再来看看继承关系中,initialize的调用
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCStudent alloc];
}
return 0;
}
输出结果:
2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}
输出结果:
020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0
#import "TCStudent.h"
@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}
输出结果:
2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0
#import "TCStudent.h"
@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"
@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"
@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end
输出结果:
2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0
#import "TCPerson.h"
@implementation TCPerson
(void)load{
}
@end
#import "TCPerson TCtest1.h"
@implementation TCPerson (TCtest1)
(void)load{
}
@end
#import "TCPerson TCTest2.h"
@implementation TCPerson (TCTest2)
(void)load{
}
@end
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
@end
@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
@end
@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
@end
可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出结果:
从输出结果我们可以抛光,三个负载方法都被调用
#import
NS_ASSUME_NONNULL_BEGIN
@interface TCPerson : NSObject
+ (void)test;
@end
NS_ASSUME_NONNULL_END
#import "TCPerson.h"
@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
+ (void)test{
NSLog(@"TCPerson +test");
}
@end
分类重写测试
#import "TCPerson+TCtest1.h"
@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest1) +test1");
}
@end
#import "TCPerson+TCTest2.h"
@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest2) +test2");
}
@end
#import
#import "TCPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
}
return 0;
}
我们打印TCPerson的类方法
void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
printMethodNamesOfClass(object_getClass([TCPerson class]));
}
return 0;
}
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用
我们查看call_class_loads()方法
static void call_class_loads(void)
{
int I;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
其通过的是load_method_t函数指针直接调用
函数指针直接调用
其分类load方法调用也是一样
UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车
,视频收藏
等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。
tableView.editing = YES
。// 取消
[self.tableView setEditing:YES animated:NO];
UITableViewDelegate
中的- tableview:editingStyleForRowAtIndexPath:
方法,在里面返回删除模式。如果不实现,默认返回的就是删除模式。-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 删除
return UITableViewCellEditingStyleDelete;
}
UITableViewDelegate
中的- tableview:commitEditingStyle:editing StyleForRowAtIndexPath:
方法。只要实现此方法,即默认实现了系统横扫出现删除按钮的删除方法。-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
//只要实现这个方法,就实现了默认滑动删除!!!!!
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// 删除数据
[self _deleteSelectIndexPath:indexPath];
}
}
-tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:
方法。-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return @"删除";
}
删除模式
无效,反而成为多选模式
。tableView.allowsMultipleSelection = NO;
tableView.allowsSelectionDuringEditing = NO;
tableView.allowsMultipleSelectionDuringEditing = NO;
编辑
按钮的bug。一、OpenGL中的坐标系
1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)
2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形
3. OpenGL的投影方式有:
透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果
正投影:只能用来设置平面图形
4.坐标系的分类:
① 惯性坐标系:没有什么参考价值,只要用来平移到世界坐标系
② 世界坐标系:大环境中的位置,系统的绝对坐标
③ 物体空间坐标系(局部空间):局部空间中的位置
④ 摄像机坐标系:观察空间(观察者)
5.坐标系之间的变换
① 物体坐标系,模型变换
② 转换到世界坐标系,视变换
③ 转换到观察者坐标系/摄像机坐标系,投影变换
④ 转换到裁剪坐标系,透视除法
⑤ 转换到规范化设备坐标,视口变换
⑥ 转换到屏幕坐标
注:① - ④ 是可以由开发者自定义完成的,⑤ - ⑥是有系统OpenGL来完成的
二、着色器的渲染流程
1.顶点数据
2.顶点着色器:接收顶点数据,单独处理每个顶点
3.细分着色器:
① 可选:描述物体形状,在管线中生成新的饿几何平面模型,生成最终形态
② 组分控制着色器/细分计算着色器:对所有的图形进行修改几何图元类型或者放弃所有图像
4.几何着色器
5.图元设置
6.剪切:剪切窗口之外的绘制
7.光栅化:输入图元的数学描述,转化为与屏幕对应位置像素片元,简称光栅化
8.片元着色器:片元着色器以及深度值,然后传递到片元测试和混合模块
9.效果
三、补充几个知识点
1.图片的渲染流程:
① GPU解码图片
② GPU纹理混合、顶点计算、像素点填充计算、渲染到帧缓冲区
③ 时钟信号:垂直同步、水平同步
④ iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制
2.MVP矩阵:
① model:物体的一些变化
② view:观察
③ projection:3D矩阵
3.强制解压缩:对图片进行重新绘制,得到一张新的压缩后的位图,其中用到的最核心的函数是:GCBitmapContextCreate
收起阅读 »在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案:
plist文件(序列化)
preference(偏好设置)
NSKeyedArchiver(归档)
SQLite3
FMDB
CoreData
每个APP的沙盒下面都有相似目录结构,如图
下面的代码得到的是应用程序目录的路径,在该目录下有三个文件夹:Documents、Library、temp以及一个.app包!该目录下就是应用程序的沙盒,应用程序只能访问该目录下的文件夹!!!
NSString *path = NSHomeDirectory();
1、Documents 目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。
2、AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
3、Library 目录:这个目录下有两个子目录:
Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.
Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。
4、tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。
// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
// 获取Documents目录路径
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library的目录路径
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// 获取Caches目录路径
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取tmp目录路径
NSString *tmpDir = NSTemporaryDirectory();
//获取应用程序程序包中资源文件路径的方法
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"png"];
UIImage *appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];
可以被序列化的类型只有如下几种:
NSArray; //数组
NSMutableArray; //可变数组
NSDictionary; //字典
NSMutableDictionary; //可变字典
NSData; //二进制数据
NSMutableData; //可变二进制数据
NSString; //字符串
NSMutableString; //可变字符串
NSNumber; //基本数据
NSDate; //日期
数据存储与读取的实例:
/**
写入数据到plist
*/
- (void)writeToPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"写入数据地址%@",path);
NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
NSArray *array = @[@"123", @"王佳佳", @"iOS"];
//序列化,把数组存入plist文件
[array writeToFile:fileName atomically:YES];
NSLog(@"写入成功");
}
/**
从plist读取数据
@return 读出数据
*/
- (NSArray *)readFromPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"读取数据地址%@",path);
NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
//反序列化,把plist文件数据读取出来,转为数组
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
return result;
}
存储时使用writeToFile:atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。
Preference通常用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。
数据存储与读取的实例:
- (void)writeToPreference{
//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向偏好设置中写入内容
[userDefaults setObject:@"wangjiajia" forKey:@"name"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
NSString *path = NSHomeDirectory();
}
- (void)readFromPreference{
//获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//读取偏好设置
NSString *name = [userDefaults objectForKey:@"name"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
}
使用偏好设置对数据进行保存,它保存的时间是不确定的,会在将来某一时间自动将数据保存到 Preferences 文件夹下,如果需要即刻将数据存储,使用 [defaults synchronize]。
Preference(偏好设置)
和plist文件(序列化)
都是保存在plist
文件中,但是plist文件(序列化)
操作读取时需要把整个plist文件都进行读取,而Preference(偏好设置)
可以直接通过key-value
单个读取。
要使用归档,其归档对象必须实现NSCoding协议
。
NSCoding协议声明的两个方法都必须实现。
encodeWithCoder:用来说明如何将对象编码到归档中。
initWithCoder:用来说明如何进行解档来获取一个新对象。
数据存储与读取的实例:
/**
归档
*/
- (void)keyedArchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.name = @"wangjiajia";
[NSKeyedArchiver archiveRootObject:person toFile:file];
}
/**
解档
*/
- (void)keyedUnarchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
NSLog(@"name:%@",person.name);
}
}
以下代码块将会介绍sqlite3.h
中主要的API。
/* 打开数据库 */
int sqlite3_open(
const char *filename, /* 数据库路径(UTF-8) */
sqlite3 **pDb /* 返回的数据库句柄 */
);
/* 执行没有返回的SQL语句 */
int sqlite3_exec(
sqlite3 *db, /* 数据库句柄 */
const char *sql, /* SQL语句(UTF-8) */
int (*callback)(void*,int,char**,char**), /* 回调的C函数指针 */
void *arg, /* 回调函数的第一个参数 */
char **errmsg /* 返回的错误信息 */
);
/* 执行有返回结果的SQL语句 */
int sqlite3_prepare_v2(
sqlite3 *db, /* 数据库句柄 */
const char *zSql, /* SQL语句(UTF-8) */
int nByte, /* SQL语句最大长度,-1表示SQL支持的最大长度 */
sqlite3_stmt **ppStmt, /* 返回的查询结果 */
const char **pzTail /* 返回的失败信息*/
);
/* 关闭数据库 */
int sqlite3_close(sqlite3 *db);
处理SQL返回结果的一些API
#pragma mark - 定位记录的方法
/* 在查询结果中定位到一条记录 */
int sqlite3_step(sqlite3_stmt *stmt);
/* 获取当前定位记录的字段名称数目 */
int sqlite3_column_count(sqlite3_stmt *stmt);
/* 获取当前定位记录的第几个字段名称 */
const char * sqlite3_column_name(sqlite3_stmt *stmt, int iCol);
# pragma mark - 获取字段值的方法
/* 获取二进制数据 */
const void * sqlite3_column_blob(sqlite3_stmt *stmt, int iCol);
/* 获取浮点型数据 */
double sqlite3_column_double(sqlite3_stmt *stmt, int iCol);
/* 获取整数数据 */
int sqlite3_column_int(sqlite3_stmt *stmt, int iCol);
/* 获取文本数据 */
const unsigned char * sqlite3_column_text(sqlite3_stmt *stmt, int iCol);
由于其他API相对来说比较简单,这里就只给出执行有返回结果的SQL语句的实例
/* 执行有返回值的SQL语句 */
- (NSArray *)executeQuery:(NSString *)sql{
NSMutableArray *array = [NSMutableArray array];
sqlite3_stmt *stmt; //保存查询结果
//执行SQL语句,返回结果保存在stmt中
int result = sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL);
if (result == SQLITE_OK) {
//每次从stmt中获取一条记录,成功返回SQLITE_ROW,直到全部获取完成,就会返回SQLITE_DONE
while( SQLITE_ROW == sqlite3_step(stmt)) {
//获取一条记录有多少列
int columnCount = sqlite3_column_count(stmt);
//保存一条记录为一个字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
//获取第i列的字段名称
const char *name = sqlite3_column_name(stmt, i);
//获取第i列的字段值
const unsigned char *value = sqlite3_column_text(stmt, i);
//保存进字典
NSString *nameStr = [NSString stringWithUTF8String:name];
NSString *valueStr = [NSString stringWithUTF8String:(const char *)value];
dict[nameStr] = valueStr;
}
[array addObject:dict];//添加当前记录的字典存储
}
sqlite3_finalize(stmt);//stmt需要手动释放内存
stmt = NULL;
NSLog(@"Query Stmt Success");
return array;
}
NSLog(@"Query Stmt Fail");
return nil;
}
在使用数据库存储时主要存在以下步骤。
1、创建数据库
2、创建数据表
3、数据的“增删改查”操作
4、关闭数据库
在使用sqlite3时,数据表的创建及数据的增删改查都是通过sql语句实现的。下面是一些常用的SQL语句。
创建表:
create table 表名称(字段1,字段2,……,字段n,[表级约束])[TYPE=表类型];
插入记录:
insert into 表名(字段1,……,字段n) values (值1,……,值n);
删除记录:
delete from 表名 where 条件表达式;
修改记录:
update 表名 set 字段名1=值1,……,字段名n=值n where 条件表达式;
查看记录:
select 字段1,……,字段n from 表名 where 条件表达式;
FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便。
FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。
FMResultSet :执行查询一个FMDatabase结果集。
FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。
创建数据库
打开数据库、关闭数据库
执行更新的SQL语句
执行查询的SQL语句
/**
创建数据库
*/
- (void)createDatabase{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
NSLog(@"数据库路径:%@",filePath);
/**
1. 如果该路径下已经存在该数据库,直接获取该数据库;
2. 如果不存在就创建一个新的数据库;
3. 如果传@"",会在临时目录创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
4. 如果传nil,会在内存中临时创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
*/
self.database = [FMDatabase databaseWithPath:filePath];
}
/* 打开数据库,成功返回YES,失败返回NO */
- (BOOL)open;
/* 关闭数据库,成功返回YES,失败返回NO */
- (BOOL)close;
在FMDB里除了查询操作,其他数据库操作都称为更新。而更新操作FMDB给出以下四种方法。
/* 1. 直接使用完整的SQL更新语句 */
[self.database executeUpdate:@"insert into mytable(num,name,sex) values(0,'wangjiajia1','m');"];
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
/* 2. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要后面的参数进行替代 */
[self.database executeUpdate:sql,@1,@"wangjiajia2",@"m"];
/* 3. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要数组参数里面的参数进行替代 */
[self.database executeUpdate:sql
withArgumentsInArray:@[@2,@"wangjiajia3",@"m"]];
/* 4. SQL语句字符串可以使用字符串格式化,这种我们应该比较熟悉 */
[self.database executeUpdateWithFormat:@"insert into mytable(num,name,sex) values(%d,%@,%@);",4,@"wangjiajia4",@"m"];
查询方法与更新方法类似,只不过查询方法存在返回值,可以通过返回值给出的相应方法获取需要的数据。
/* 执行查询SQL语句,返回FMResultSet查询结果 */
- (FMResultSet *)executeQuery:(NSString*)sql, ... ;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... ;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
执行查询语句实例如下
- (NSArray *)getResultFromDatabase{
//执行查询SQL语句,返回查询结果
FMResultSet *result = [self.database executeQuery:@"select * from mytable"];
NSMutableArray *array = [NSMutableArray array];
//获取查询结果的下一个记录
while ([result next]) {
//根据字段名,获取记录的值,存储到字典中
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
int num = [result intForColumn:@"num"];
NSString *name = [result stringForColumn:@"name"];
NSString *sex = [result stringForColumn:@"sex"];
dict[@"num"] = @(num);
dict[@"name"] = name;
dict[@"sex"] = sex;
//把字典添加进数组中
[array addObject:dict];
}
return array;
}
由于在多线程同时操作FMDatabase对象时,会造成数据混乱的问题,FMDB提供了一个可以确保线程安全的类(FMDatabaseQueue)。
FMDatabaseQueue的使用比较简单
//创建多线程安全队列对象
self.queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
//在block块内自行相关数据库操作即可
[self.queue inDatabase:^(FMDatabase * _Nonnull db) {
}];
事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。
比如要更新数据库的大量数据,我们需要确保所有的数据更新成功,才采取这种更新方案,如果在更新期间出现错误,就不能采取这种更新方案了,这就是事务的用处。只有事务提交了,开启事务期间的操作才会生效。
以下是事务的例子
//事务
-(void)transaction {
// 开启事务
[self.database beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i--) {
NSNumber *num = @(i+6);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [self.database executeUpdate:sql,num,name,sex];
if ( !result ) {
NSLog(@"插入失败!");
isRollBack = YES;
return;
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
@finally {
if (!isRollBack) {
NSLog(@"插入成功,事务提交");
//事务提交
[self.database commit];
}else{
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
}
}
//多线程安全事务实例
- (void)transactionByQueue {
//开启事务
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
BOOL isRollBack = NO;
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if ( !result ) {
isRollBack = YES;
return;
}
}
//当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
*rollback = isRollBack;
}];
}
以下是CoreData常用类的作用描述
PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。
ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。
PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。
ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。
CoreData主要工作原理如下
读取数据库的数据时,数据库数据先进入数据解析器,根据对应的模板,生成对应的关联对象。
向数据库插入数据时,对象管理器先根据实体描述创建一个空对象,对该对象进行初始化,然后经过数据解析器,根据对应的模板,转化为数据库的数据,插入数据库中。
更新数据库数据时,对象管理器需要先读取数据库的数据,拿到相互关联的对象,对该对象进行修改,修改的数据通过数据解析器,转化为数据库的更新数据,对数据库更新。
CoreData的使用步骤如下
1.添加框架。
2.数据模板和对象模型。
3.创建对象管理上下文。
4.数据的增删改查操作。
在其中第二步的时候要注意,Xcode 8.0 之后和之前的Xcode 版本是有一些区别的。在 8.0之后创建.xcdatamodeld文件之后,添加实体之后会自动生成对应的对应类文件。
#import
NS_ASSUME_NONNULL_BEGIN
@interface TCPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
当点击屏幕的时候,控制台输出:
2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}
在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:
- (void)setName:(NSString *)name{
_name = name;
}
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}
/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];
self.person2.name = @"ttttttttt";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
#import "NSKVONotifying_TCPerson.h"
@implementation NSKVONotifying_TCPerson
//NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
- (void)setName:(NSString *)name{
_NSSetIntVaueAndNotify();
}
//改变过程
void _NSSetIntVaueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
//通知观察者
- (void)didChangeValueForKey:(NSString *key){
[observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context];
}
@end
首先了解一下程序、进程和线程
程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。所以程序运行的时候,需要一个机制,让线程能随时处理事件但不退出。
以前写游戏的时候就写过这样的东西,通常是一个 do while 循环,让程序一直运转,直到接收到退出信息。
而 RunLoop 就是让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时默认是没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
他们的层级关系为,一个 RunLoop 对象包含若干个 Mode 对象,每个 Mode 又包含若干个 Source/Timer/Observer,RunLoop 在运作的时候,一次只能运作与一个 Mode 之下,如果需要切换 Mode,需要退出 Loop 才能重新指定一个 Mode。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
而一个 Source 对象是一个事件,Source 有两个版本:Source0 和 Source1,Source0 只包含一个函数指针,并不能主动触发,需要将 Source0 标记为待处理,在 RunLoop 运转的时候,才会处理这个事件(如果 RunLoop 处于休眠状态,则不会被唤醒去处理),而 Source1 包含了一个 mach_port 和一个函数指针,mach_port 是 iOS 系统提供的基于端口的输入源,可用于线程或进程间通讯。而 RunLoop 支持的输入源类型中就包括基于端口的输入源,可以做到对 mach_port 端口源事件的监听。所以监听到 source1 端口的消息时,RunLoop 就会自己醒来去执行 Source1 事件(也能称为被消息唤醒)。也就是 Source0 是直接添加给 RunLoop 处理的事件,而 Source1 是基于端口的,进程或线程之间传递消息触发的事件(为什么要 0 和 1 来命名,每次都记不住,GG)。
Timer 是基于时间的触发器,CFRunLoopTimerRef 和 NSTimer 可以通过 Toll-free bridging 技术混用,Toll-free bridging 是一种允许某些 ObjC 类与其对应的 CoreFoundation 类之间可以互换使用的机制,当将 Timer 加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行 Timer 回调。
Observer(观察者)都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
也就是可以在这几个时机去安排 RunLoop 执行一些其他的任务。
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
RunLoop 的 CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。
如上文说的 RunLoop 一次循环只能运行在一个 Mode 下,是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。但如果一个 Source/Timer/Observer 想在多个 Mode 下运作,则可以分别加入到多个 Mode,或者给两个 Mode 添加 "Common" 标记,再将 Source/Timer/Observer 加入到 RunLoop 的 "commonModeItems" 。
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,如果想让它回调则可以将这个 Timer 分别加入这两个 Mode。或将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 。
CFRunLoop 对外暴露的管理 Mode 接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
可以注意到 mode 并不像 Source/Timer/Observer 一样有 Remove 方法,所以 mode 只能增加,不能减少。
当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。
接下来看 RunLoop 的执行逻辑
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
其中第 2 步虽然通知 Observer 即将处理 Timer,但其实并没有真的即将处理 Timer 回调,这个通知每次 Loop 循环都会调用,但 Timer 只有在注册的时间到了才会在第 9 步去执行,第 4 步处理运行的 mode 里待处理的 Source0,其中第 5 步会判断 mode item 里是否有 Source1 处于 ready 状态(也就是 Source1 的端口已经收到消息),有的话跳到第 9 步,处理 Source1 事件,然后进入下一个循环,没有的话说明 mode item 里的事件都处理完毕,线程进入休眠状态,等待 Source1,Timer 或者外部手动将 RunLoop 唤醒(上文说 Source0 并不能唤醒 RunLoop,所以一般会通过手动唤醒 RunLoop,来让 RunLoop 处理新加入进去的 Source0)。
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,去执行加入到 RunLoop 里的 Source0,Timer 的回调,以及 Observer 回调,以及用于线程或进程间通讯的 Source1,当所有的都处理完之后,结束一次循环,进入休眠状态,休眠的时候等待 Timer 注册的时间点或者 Source1 唤醒 RunLoop(也可以手动唤醒)。
从上面我们可以了解,线程和进程之间的通讯是基于 mach port 传递消息实现的,这也是 RunLoop 的核心。有必要了解一下 mach port, OSX/iOS 的系统架构分为 4 层,从外到内为应用层,应用框架层,核心框架层,Darwin。应用层包括用户能接触到的图形应用,应用框架层即开发人员接触到的 Cocoa 等框架,核心框架层包括各种核心框架、OpenGL 等内容,Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容。
其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
在 Mach 中,进程、线程和虚拟内存都被称为"对象"。Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
Mach 中的对象通过一个 Mach 端口发送一个消息,消息中会携带目标端口,这个消息会从用户空间传递到内核空间,再由内核空间传递到目标端口,实现线程或进程之间的通讯。(也就是线程或进程之间的通讯不能绕过系统内核)。目标端口接收到消息,因为 RunLoop 会对 mach_port 端口源进行监听,如果 RunLoop 此时处于休眠状态,则被唤醒,便可以处理已经接收到消息的 source1 事件。
启动后,系统默认注册了5个Mode:
1:kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
2:UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3:UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4:GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5:kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()(因为需要设置不同的优先级,所以注册两个)。
第一个 Observer 监视的事件是 Entry(即将进入Loop),用来创建自动释放池,且设置的优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时去释放旧的池并创建新池;Exit(即将退出Loop) 时释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的事件传递。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。也就是 UI 是在界面 RunLoop 休眠之前更新的,所以如果想在 UI 更新之后做一些事情,可以注册一个 Observer 监听 kCFRunLoopAfterWaiting(刚从休眠中唤醒)。
定时器
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
网络请求
iOS 中,关于网络请求的接口自下至上有如下几层:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了多个需要手动触发的 Source0。
当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 基于 mach port 的 Source1 接收来自底层 CFSocket 的消息,当收到消息后,其会在合适的时机将 Source0 标记为待处理,同时唤醒 Delegate 线程的 RunLoop 来让其处理 Source0。完成一个由 CFSocket 线程到网络请求所在线程的数据处理。
AFNetworking
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 创建了一个线程,并在这个线程中启动了一个 RunLoop,RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。当需要这个后台线程执行网络请求任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了该线程的 RunLoop 中。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。
排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor 等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务(需要将 Node 的属性同步到主线程的 UIView/CALayer 去的任务),然后一一执行。
总结:
ibireme 的 深入理解RunLoop 前前后后我看了好几遍,每次都看的不深入,总是瞬时了解,睡一觉就记不清楚细节了,这次自己逐行去理解,并写下来,加上点自己的理解,也写了点代码去观察 RunLoop,算是对 RunLoop 的理解更加深入了。
Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。
也就是 Runtime 涉及三个点,
面向对象
消息分发
消息转发
。面向对象:
Objective-C 的对象是基于 Runtime 创建的结构体。先从代码层面分析一下。
Class *class = [[Class alloc] init];
alloc 方法会为对象分配一块内存空间,空间的大小为 isa_t(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化 isa_t ,而 isa_t 是一个 union 类型的结构体(或者称之为联合体),它的结构是在 Runtime 里被定义的。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
从 isa_t 的结构可以看出,isa_t 可以存储 struct,uintptr_t 或者 Class 类型
。
init 方法就直接返回了初始化好的对象,class 指针指向这个初始化好的对象。
也就是在 Runtime 的协助之下,一个对象完成了创建。
你可能想知道,这个对象只存放了一个 isa_t 结构体和成员变量,对象的方法在哪里?
在编译的时候,类在内存中的位置就已经确定,而在 main 方法之前,Runtime 将可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)加载到内存中,由 Runtime 管理,这里也包括了也是一个对象的类。
类对象里储存着一个 isa_t 的结构体,super_class 指针,cache_t 结构体,class_data_bits_t 指针。
struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
class_data_bits_t 指向类对象的数据区域,数据区域存放着这个类的实例方法链表。而类方法存在元类对象的数据区域。也就是有对象,类对象,元类对象三个概念,对象是在运行时动态创建的,可以有无数个,类对象和元类对象在 main 方法之前创建的,分别只会有一个。
消息分发:
在 Objective-C 中的“方法调用”其实应该叫做消息传递,[object message] 会被编译器翻译为 objc_msgSend(object, @selector(message)),这是一个 C 方法,首先看它的两个参数,第一个是 object ,既方法调用者,第二个参数称为选择子 SEL,Objective-C 为我们维护了一个巨大的选择子表,在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中,在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。
通过第一个参数 object,可以找到 object 对象的 isa_t 结构体,从上文中能看 isa_t 结构体的结构,在 isa_t 结构体中,shiftcls 存放的是一个 33 位的地址,用于指向 object 对象的类对象,而类对象里有一个 cache_t 结构体,来看一下 cache_t 的具体代码,
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
_mask:分配用来缓存 bucket 的总数。
_occupied:表明目前实际占用的缓存 bucket 的个数。
_buckets:一个散列表,用来方法缓存,bucket_t 类型,包含 key 以及方法实现 IMP。
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
objc_msgSend() 方法会先从缓存表里,查找是否有该 SEL 对应的 IMP,有的话算命中缓存,直接通过函数指针 IMP ,找到方法的具体实现函数,执行。
当然缓存表里可能并不会命中,则此时会根据类对象的 class_data_bits_t 指针找到数据区域,数据区域里用链表存放着类的实例方法,实例方法也是一个结构体,其结构为:
struct method_t {
SEL name;
const char *types;
IMP imp;
};
编译器将每个方法的返回值和参数类型编码为一个字符串,types 指向的就是这样一个字符串,objc_msgSend() 会在类对象的方法链表里按链表顺序去匹配 SEL,匹配成功则停止,并将此方法加入到类对象的 _buckets 里缓存起来。如果没找到则会通过类对象的 superclass 指针找到其父类,去父类的方法列表里寻找(也会从父类的方法缓存列表开始)。
如果继续没有找到会一直向父类寻找,直到遇见 NSObject,NSObject 的 superclass 指向 nil。也就意味着寻找结束,并没有找到实现方法。(如果这个过程找到了,也同样会在 object 的类对象的 _buckets 里缓存起来)。
选择子在当前类和父类中都没有找到实现,就进入了方法决议(method resolve),首先判断当前 object 的类对象是否实现了 resolveInstanceMethod: 方法,如果实现的话,会调用 resolveInstanceMethod:方法,这个时候我们可以在 resolveInstanceMethod:方法里动态的添加该 SEL 对应的方法(也可以去做点别的,比如写入日志)。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现 resolveInstanceMethod: 方法,Runtime 还有另一套机制,消息转发。
消息转发:
消息转发分为以下几步:
1.调用 forwardingTargetForSelector: 方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了 nil,继续下面的动作。
2.调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。
3.调用 forwardInvocation: 方法,将第 2 步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了。
以上三个方法都可以通过在 object 的类对象里实现, forwardingTargetForSelector: 可以通过对参数 SEL 的判断,返回一个可以响应该消息的对象。这样则会重新从该对象开始执行查找方法实现的流程,找到了也同样会在 object 的类对象的 _buckets 里缓存起来。而 2,3 方法则一般是配套使用,实现 methodSignatureForSelector: 方法根据参数 SEL ,做相应处理,返回 NSMethodSignature (方法签名) 对象,NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。
上面是讲的是实例方法,类方法没什么区别,类方法储存在元类对象的数据区域里,通过类对象的 isa_t 找到元类对象,执行查找方法实现的流程,元类对象的 superclass 最终也会指向 NSObject。没找到的话,也会有方法决议以及消息转发。
runtime 可以做什么:
实现多继承:从 forwardingTargetForSelector: 方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。
Method m1 = class_getInstanceMethod([M1 class], @selector(hello1));
Method m2 = class_getInstanceMethod([M2 class], @selector(hello2));
method_exchangeImplementations(m2, m1);
关联对象:
通过下面两个方法,可以给 category 实现添加成员变量的效果。
objc_setAssociatedObject
objc_getAssociatedObject
动态添加类和方法:
objc_allocateClassPair 函数与 objc_registerClassPair 函数可以完成一个新类的添加,class_addMethod 给类添加方法,class_addIvar 添加成员变量,objc_registerClassPair 来注册类,其中成员变量的添加必须在类注册之前,类注册后就可以创建该类的对象了,而再添加成员变量就会破坏创建的对象的内存结构。
用到了 Runtime 获取某一个类的全部属性的名字,以及 Runtime 获取属性的类型。
在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?
我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据
1.传递数据处理的流程:顶点着色器--->光栅化/图元装配--->片元着色器--->渲染完成
2.TextureData(纹理)、Uniforms:可以直接的传递到顶点着色器或者片元着色器中。
Attributes(属性):只能传递到顶点着色器中,进过处理后的数据可以传递到片元着色器中
3.着色器中的是我们可以控制的,但是光栅化/图元装配是由系统来完成的,不可控
三种传输方式详解
1.Attributes:只能传递到顶点着色器中,通过处理可以传递到片元着色器中
使用场景:当数据不停的进行变换时
经常传递的数据:颜色数据、顶点数据、纹理坐标、关照法线
2.Uniform:可以直接传递数据到顶点/片元着色器中
使用场景:比较统一的处理方式,不会发生太多变化时
顶点着色器处理的场景:图形的旋转操作,每个顶点乘以旋转矩阵来完成(旋转矩阵基本是不怎么改变的)
片元着色器处理场景:处理视频,视频解码之后是由一帧帧的图片来组成的,对视频的颜色空间进行渲染处理(在视频中常使用的颜色空间为YUV)将YUV颜色空间乘以矩阵转化为RGB颜色来处理视频
3.TextureData(纹理):颜色的填充,视频的处理。一般这里的数据不会传递到顶点着色器的,这里主要是用来处理图片的,一般不涉及到顶点数据的处理
收起阅读 »