注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS 离屏渲染的研究

iOS
GPU渲染机制: CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。 GPU屏幕渲染有以下两种方式: On-Screen Ren...
继续阅读 »

GPU渲染机制:


CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。




GPU屏幕渲染有以下两种方式:



  • On-Screen Rendering

    意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。


  • Off-Screen Rendering

    意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。




特殊的离屏渲染:

如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式: CPU渲染。

如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内 同步地

完成,渲染得到的bitmap最后再交由GPU用于显示。

备注:CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程,一个简单的异步绘制过程大致如下

 - (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}



离屏渲染的触发方式


设置了以下属性时,都会触发离屏绘制:



  • shouldRasterize(光栅化)

  • masks(遮罩)

  • shadows(阴影)

  • edge antialiasing(抗锯齿)

  • group opacity(不透明)

  • 复杂形状设置圆角等

  • 渐变



其中shouldRasterize(光栅化)是比较特别的一种:

光栅化概念:将图转化为一个个栅格组成的图象。

光栅化特点:每个元素对应帧缓冲区中的一像素。




shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。




相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。




当你使用光栅化时,你可以开启“Color Hits Green and Misses Red”来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。




如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁,红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。




注意:

对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费




例如我们日程经常打交道的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化。则会造成大量的离屏渲染,降低图形性能。




光栅化有什么好处?



shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。



举个栗子



如果在滚动tableView时,每次都执行圆角设置,肯定会阻塞UI,设置这个将会使滑动更加流畅。

当shouldRasterize设成true时,layer被渲染成一个bitmap,并缓存起来,等下次使用时不会再重新去渲染了。实现圆角本身就是在做颜色混合(blending),如果每次页面出来时都blending,消耗太大,这时shouldRasterize = yes,下次就只是简单的从渲染引擎的cache里读取那张bitmap,节约系统资源。



而光栅化会导致离屏渲染,影响图像性能,那么光栅化是否有助于优化性能,就取决于光栅化创建的位图缓存是否被有效复用,而减少渲染的频度。可以使用Instruments进行检测:



当你使用光栅化时,你可以开启“Color Hits Green and Misses Red”来检查该场景下光栅化操作是否是一个好的选择。

如果光栅化的图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。



注意:

对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费



为什么会使用离屏渲染


当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。


屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。


所以当使用离屏渲染的时候会很容易造成性能消耗,因为在OPENGL里离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。




Instruments监测离屏渲染


Instruments的Core Animation工具中有几个和离屏渲染相关的检查选项:



  • Color Offscreen-Rendered Yellow

    开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。


  • Color Hits Green and Misses Red

    如果shouldRasterize被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。





iOS版本上的优化


iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染


iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。


这可能是苹果也意识到离屏渲染会产生性能问题,所以能不产生离屏渲染的地方苹果也就不用离屏渲染了。






链接:https://www.jianshu.com/p/6d24a4c29e18

收起阅读 »

iOS - 多线程应用场景

iOS
实际项目开发中为了能够给用户更好的体验,有些延时操作我们都会放在子线程中进行。今天我们就来聊聊多线程在实际项目中的运用。我们先来看看多线程的基础知识:1.多线程的原理:        同一时间,CPU只能处理一条线程,也...
继续阅读 »

实际项目开发中为了能够给用户更好的体验,有些延时操作我们都会放在子线程中进行。

今天我们就来聊聊多线程在实际项目中的运用。

我们先来看看多线程的基础知识:

1.多线程的原理:

        同一时间,CPU只能处理一条线程,也就是只有一条线程在工作。所谓多线程并发(同时)执行,

其实是CPU快速的在多线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并

发执行的假象。

2.在实际项目开发中并不是线程越多越好,如果开了大量的线程,会消耗大量的CPU资源,CPU会

被累死,所以一般手机只开1~3个线程为宜,不超过5个。

3.多线程的优缺点:

优点:1.能适当提高程序的执行效率

       2.能适当提高资源的利用率,这个利用率表现在(CPU,内存的利用率)

缺点:1.开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,

子线程占用512KB,如果开启大量的线程,会占用大量的内存空间,降低程序

的性能)

     2.线程越多,CPU在调度线程上的开销就越大

     3.程序设计就越复杂:比如线程之间的通信,多线程的数据共享,这些

都需要程序的处理,增加了程序的复杂度。

4.在iOS开发中使用线程的注意事项:

    1.别将比较耗时的操作放在主线程中

    2.耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验

好了,多线程在iOS中的开发概念性的东西就讲这么多,下面我们来模拟一种开发中的场景:

我们在开发中经常会遇到,当你要缓存一组图片,但是这些图片必须要等到你缓冲好了后再来展现在UI上,

可是我们缓存图片的时候用的是SDWebImage框架,缓存的操作是异步进行的,我们如何来做到等缓存好了

再来执行以后的操作呢?下面讲个实现起来非常简单,方便的方法:

我先来放上代码,后面进行讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//1.添加一个组
        let group = dispatch_group_create()
         
        //缓存图片
        for url in picURLs! {
             
            //2.将当前的下载操作添加到组中
            dispatch_group_enter(group)
            SDWebImageManager.sharedManager().downloadImageWithURL(url, options: SDWebImageOptions.init(rawValue: 0), progress: nil, completed: { (_, _, _, _, _) in
                 
                //3.离开当前组
                dispatch_group_leave(group)
                print("正在缓存中...")
            })
        }
         
        //通过闭包将数据传递给调用者(通知图片缓存完毕)
        dispatch_group_notify(group, dispatch_get_main_queue()) {
            print("缓存完毕!")
            finished()
        }

 

从输出结果我们就可以看出来:我们做到了缓存完毕后再来执行以后的操作。

是如何做到的呢?

我在代码中已经用数字标出来了:

1.我们首先用

let group = dispatch_group_create()

函数来创建一个组,用来存放缓冲的操作

2.用这个函数做到把每一次的缓冲操作都添加到组中

1
dispatch_group_enter(group)

 3.缓存图片我用的是SDWebImage框架,我们可以看到,我在缓冲完毕后离开当前组,用到如下函数

dispatch_group_leave(group)

用了这三步就能做到我们想要的功能吗?显然不是,做了这三部系统内部就会为我们做些事了,

 

当我们离开当前组的时候,系统就会发出一个通知,我们来接收这个通知,当我们接收到这个通知的时候

我们就可以执行finished的操作了,接收通知的函数是:

dispatch_group_notify(group, dispatch_get_main_queue()) {
print(
"缓存完毕!")
finished()
}

以上就是一个非常方便的实现我们需要的功能的方法

 

https://blog.csdn.net/qq_24904667/article/details/52679473

收起阅读 »

swift 多线程下

iOS
Swift多线程编程方案: Thread Cocoa Operation (Operation 和 OperationQueue) Grand Central Dispath (GCD) 1. Thread在三种多线程技术中是最轻量级的, 但需要自己...
继续阅读 »

Swift多线程编程方案:



  • Thread


  • Cocoa Operation (OperationOperationQueue)


  • Grand Central Dispath (GCD)




1. Thread在三种多线程技术中是最轻量级的, 但需要自己管理线程的生命周期和线程同步. 线程同步对数据的加锁会有一定的系统开销.



  • detachNewThread(_ block: @escaping () -> Void)

  • detachNewThreadSelector(_ selector: Selector, to Target target: Any, with argument: Any?)



e.g.

for i in 0...10 {
Thread.detachNewThread {
print("\(i) \(Thread.current)")
}
}

输出结果:

8 <NSThread: 0x6000000f8e40>{number = 12, name = (null)}
10 <NSThread: 0x6000000f0240>{number = 17, name = (null)}
7 <NSThread: 0x6000000cc0c0>{number = 10, name = (null)}
1 <NSThread: 0x6000000c0180>{number = 14, name = (null)}
6 <NSThread: 0x6000000efe80>{number = 9, name = (null)}
4 <NSThread: 0x6000000efdc0>{number = 11, name = (null)}
5 <NSThread: 0x6000000c8580>{number = 15, name = (null)}
9 <NSThread: 0x6000000cc080>{number = 8, name = (null)}
0 <NSThread: 0x6000000fd300>{number = 7, name = (null)}
2 <NSThread: 0x6000000cc5c0>{number = 13, name = (null)}
3 <NSThread: 0x6000000f0780>{number = 16, name = (null)}


e.g.

class ObjectForThread {
func threadTest() -> Void {
let thread = Thread(target: self, selector: #selector(threadWorker), object: nil)
thread.start()
print("threadTest")
}
@objc func threadWorker() -> Void {
print("threadWorker Run")
}
}

let obj = ObjectForThread()
obj.threadTest()

输出结果:

threadTest
threadWorker Run




2. OperationOperationQueue


Operation



  • 面向对象 (OperationBlockOperation)


  • Operation+ OperationQueue

  • 取消、依赖、任务优先级、复杂逻辑、保存业务状态、子类化


Operation的四种状态 :



  • isReady

  • isExecuting

  • isFinished

  • isCancelled


OperationQueue




  • OperationQueue队列里可以加入很多个Operation, 可以把OperationQueue看做一个线程池, 可以往线程池中添加操作(Operation)到队列中

  • 底层使用GCD


  • maxConcurrentOperationCount可以设置最大并发数


  • defaultMaxConcurrentOperationCount根据当前系统条件动态确定的最大并发数

  • 可以取消所有Operation, 但是当前正在执行的不会取消

  • 所有Operation执行完毕后退出销毁



e.g. BlockOperation

class ObjectForThread {
func threadTest() -> Void {
let operation = BlockOperation { [weak self] in
self?.threadWorker()
}
let queue = OperationQueue()
queue.addOperation(operation)
print("threadTest")
}
}

let obj = ObjectForThread()
obj.threadTest()


e.g. 自定义的Operation

class ObjectForThread {
func threadTest() -> Void {
let operation = MyOperation()
operation.completionBlock = {() -> Void in
print("完成回调")
}
let queue = OperationQueue()
queue.addOperation(operation)
print("threadTest")
}
}
class MyOperation: Operation {
override func main() {
sleep(1)
print("MyOperation")
}
}

let obj = ObjectForThread()
obj.threadTest()



3. GCD


GCD特点



  • 任务+队列

  • 易用

  • 效率

  • 性能


GCD - 队列



  • 主队列: 任务在主线程执行

  • 并行队列: 任务会以先进先出的顺序入列和出列, 但是因为多个任务可以并行执行, 所以完成顺序是不一定的.

  • 串行队列: 任务会以先进先出的顺序入列和出列, 但是同一时刻只会执行一个任务


GCD - 队列API



  • Dispatch.main

  • Dispatch.global

  • DispatchQueue(label:,qos:,attributes:,autoreleaseFrequency:,target:)

  • queue.label

  • setTarget(queue:DispatchQueue)


  • 最终的目标队列都是主队列和全局队列

  • 如果把一个并行队列的目标队列都设置为同一个串行队列, 那么这多个队列连同目标队列里的任务都将串行执行

  • 如果设置目标队列成环了, 结果是不可预期的

  • 如果在一个队列正在执行任务的时候更换目标队列, 结果也是不可预期的



e.g.

let queue = DispatchQueue(label: "myQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
queue.async {
sleep(3)
print("queue")
}
print("end")


e.g.

let queue = DispatchQueue(label: "myQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
queue.asyncAfter(deadline: DispatchTime.now() + 10) {
print("in asyncAfter")
}
print("end")

DispatchTime 系统时钟

DispatchWallTime生活时间


GCD 高级特性- DispatchGroup



e.g. 阻塞当前线程

let group = DispatchGroup()
let queue = DispatchQueue(label: "test.queue")

group.enter()
queue.async {
sleep(3)
print("操作 1")
group.leave()
}

group.enter()
queue.async {
sleep(3)
print("操作 2")
group.leave()
}

print("操作1 操作2 安排完成")

group.wait()
print("操作1 操作2 全部执行完毕")


e.g. 非阻塞方式:

let group = DispatchGroup()
let queue = DispatchQueue(label: "test.queue")

group.enter()
queue.async {
sleep(3)
print("操作 1")
group.leave()
}

group.enter()
queue.async {
sleep(3)
print("操作 2")
group.leave()
}

print("操作1 操作2 安排完成")

group.notify(queue: queue) {
print("操作1 操作2 全部执行完毕")
}
print("非阻塞")

GCD 高级特性- DispatchSource



  • 简单来说, dispatch source是一个监听某些类型事件的对象. 当这些事件方法时, 它自动将一个task放入一个dispatch queue的执行历程中.



e.g. DispatchSource- Timer

var seconds = 10
let timer: DispatchSourceTimer = DispatchSource.makeTimerSource(flags: .strict, queue: .global())
timer.schedule(deadline: .now(), repeating: 1.0)
timer.setEventHandler {
seconds -= 1
if seconds < 0 {
timer.cancel()
}
else {
print(seconds)
}
}
timer.resume()


链接:https://www.jianshu.com/p/b52728779891

收起阅读 »

Swift 多线程上

iOS
提到多线程,无非就是关注二点,一是线程安全问题,二是在合适的地方合适的使用多线程(这个就有点广泛了,但是很重要不能为了去使用而使用)。 先看下OC中定义属性的关键字atomic/nonatomic,原子属性和非原子属性(此处先不谈内存相关的知识),有啥区别呢...
继续阅读 »

提到多线程,无非就是关注二点,一是线程安全问题,二是在合适的地方合适的使用多线程(这个就有点广泛了,但是很重要不能为了去使用而使用)。




先看下OC中定义属性的关键字atomic/nonatomic,原子属性和非原子属性(此处先不谈内存相关的知识),有啥区别呢?property申明的属性会默认实现setter和get方法,而atomic默认会给setter方法加锁。也就是atomic线程安全,防止数据在被不同线程读写时发生了错误,设置属性时默认就是nonatomic。万能的苹果开发大神为啥不都设置为线程安全呢,主要是因为atomic很耗内存资源,相比这点安全远不能弥补在内存资源和运行速度上的缺陷,所以需要我们在开发时,适当的时候加锁。


说到加锁(我自己了解的也不是很多),只能说说最基本的。先看我们在OC中创建单利时,用到的关键字synchronized('加锁对象'){'加锁代码'},互斥锁,简单粗暴,不需要释放啥的,在不考虑效率下还是很方便(创建个单利能耗啥效率了,如果在swift中使用那么就是objc_sync_enter('加锁对象')和objc_sync_exit('加锁对象'))。还有就是NSLock,这个或许偶尔会用到,对象锁,lock和unlock需要成对出现,不能频繁调用lock不然可能会出现线程锁死

let lock = NSLock
lock.lock()
defer {
lock.unlock()
}

只是简单说下锁(至于NSRecursiveLock,NSConditionLock等各种锁的我也不是很熟,应该说是没有用过),如果多线程出现问(愿你写的多线程永远不出现线程安全问题,但是需要谨记一句:线程复杂必死),至少知道有解决的方法,在使用多线程稍微注意下。关于线程锁和线程发生锁死,我也只是略懂,以后再总结。除了加锁还可以用其他方法避免线程问题,比如:串行队列(FMDB线程安全就是这种方式),合适的使用信号量DispatchSemaphore。


我自己也是在开发中遇到二次线程安全问题,一次在使用CoreData,还有一次就是在多线程上传图片的时候,都是泪。CoreData,最后我是使用了串行队列解决线程安全问题,CoreData是有安全队列方法的,但是呢那个是接手的项目,别人写好了,我就在原来的基础上修改的。多线程上传图片出现问题,完全是自己不够细心导致的,过了好久才意识到的问题,先说下,OC中的系统对象基本都是线程不安全的,都是操作指针,swift中的系统对象基本都是线程安全的,都是直接读取内存。即使对象是线程安全的,但是也不能在不同的线程操作同一个对象,应该先copy在再操作,就是直接用=(OC是不行的)。




说了一大堆线程相关的废话,那再看看怎样在Swift 中使用多线程,其实最常用的也就是GCD和Operation,如果说对线程安全,GCD,Operation,队列和线程关系不了解,那就去网上找资料补一补。


最常用的API,主线程延迟执行: DispatchQueue.main.asyncAfter(deadline: DispatchTime) {}

获取主线程(一个应用就一个主程):

DispatchQueue.main.async {}


再就是GCD创建队列了,分为串型队列和并发队列(串型队列只会开一个子线程,并发队列会创建多个子线程)


/// 并发队列
let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
/// 串行队列
let queue = DispatchQueue(label: "test.queue")

接着就是异步/同步(是否阻塞当前线程的区别,注意下线程相互锁死问题)执行任务的API了,任务放到队列里面去,由队列决定是否开辟新的线程执行任务,强调一点,任何UI相关的任务不能放到子线程中执行,子线程是不能刷新UI的,如果你看到你的子线程刷新了UI那是你的眼晴欺骗了你,原因是UIKit框架不是线程安全,可能会出现资源争夺,所以只能在主线程中绘制。只是简单的介绍下比较常用的API,相对于OC的写法已经简单很多了。具体的区别和参数可以参考官方的文档,貌似还很复杂。

接下来就可以愉快的异步线程执行耗时任务了,执行完再回到主线程中,该干嘛就干嘛了,简单粗暴。但是还是有问题需要我们去注意:



  • 任务全部执行完成的回调

  • 怎样控制执行的先后顺序(不考虑串行)

  • 怎样控制并发的数量

  • 怎样暂停/挂起任务


很明显,有些问题GCD可能不太容易解决。


Operation 和OperationQueue,基于GCD封装的对象,支持KVO,处理多线程的API更方便,如果你看过其他第三方框架,可以经常看到这些对象,GCD反而用的比较少。最开始出来找工作时,其实我很不喜欢别人在没有给应用场景时问我什么时候使用GCD,什么时候使用OperationQueue有种被套路的感觉(在合适的地方合理的去使用合理的多线程),就现在而言,如果简单不复杂首选GCD,当然想用OperationQueue去封装,也完全赞同,主要是看业务需求,而不是看技术需求。关于GCD和OperationQueue的区别以及性能,具体还是推荐看网上的博客,最好是先了解里面处理多线程的API,知道各自的优缺点,再去看会有不一样的收获。Operation是一个抽象类不能直接使用,可以自定义其子类,或者直接使用系统提供的子类BlockOperation(swift中系统提供的子类貌似就剩这么一个了)。Operation直接使用是没有开辟新的线程的,只有放到OperationQueue 中才是多线程(OperationQueue可以直接使用,复杂场景时会使用到Operation的子类)。OperationQueue使用的优点会在下面解决问题中提到部分。


再看GCD中的队列组DispatchGroup,也可以对队列做简单的管理。主要将某一个任务入组和出组,实现入组和出组主要用到二种方式,那么就可以监听入组的任务是否全部完成。

let groupQueue = DispatchGroup()
let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
defer {
groupQueue.notify(queue: DispatchQueue.main) {
debugPrint("执行完成\(Thread.current)")
}
}
/// 第一种直接创建的时候就加到组中,执行完成自动出组
queue.async(group: groupQueue) {
for i in 0 ... 100 {
debugPrint(Thread.current , i , "------1")
}
}
/// 第二种自己执行前加入,完成后手动出组
groupQueue.enter()
queue.async() {
for i in 0 ... 100 {
debugPrint(Thread.current , i , "------2")
}
groupQueue.leave()
}

二种方式只有选择的差距,第一种比较简单,但是适用的场景就比较简单,但是缺点也很明显,当执行的任务也存在异步,那么就不能判断是否真的完成任务了,很简单的例子,同时发送5个请求,要数据全部返回才能执行回调,那么很明显就只能使用第二种方式了,只有数据真正的返回了才出组。




先看第一个问题,比较简单,怎样知道队列中的任务全部执行完成?

使用GCD就很简单了,将任务放到队列组中去执行,完成后自动回调,当然也可以使用OperationQueue,那就是要使用KVO了监听OperationQueue 的maxConcurrentOperationCount属性,当为0的时候任务全部执行完成。


第二个问题,怎样控制执行的先后顺序?如果使用OperationQueue比较容易,方法比较多,举二个例子。看参数就知道,直到ops中的任务执行完成再接着执行其他的任务,如果每个任务都有严格的顺序,那么直接设置OperationQueue的maxConcurrentOperationCount为1就行了,为1时候其实也就是串行队列了。

func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool)

第二种方式就是添加依耐关系

let operation1 = BlockOperation {
debugPrint("开始1", Thread.current)
sleep(2)
debugPrint("完成 ")
}
let operation2 = BlockOperation {
debugPrint("开始2")
sleep(2)
debugPrint("完成")
}
operation2.addDependency(operation1)
operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)

如果使用GCD的话,那么就需要使用栅栏函数了,这个可以具体看看官方的文档或者网上优秀博客,这就举一个简单的列子

/// 栅栏函数要求队列参数为. concurrent 
let queue = DispatchQueue(label: "text.queue", attributes: .concurrent)
queue.async {
for i in 0 ... 10 {
debugPrint(i , Thread.current , "------1")
}
}
queue.async {
for i in 0 ... 10 {
debugPrint(i , Thread.current , "-----2")
}
}
/// 栅栏函数,前面的执行完,再执行栅栏函数里面的代码,执行完,再执行后面的任务
queue.async(flags: .barrier) {
for i in 0 ... 10 {
debugPrint(i , Thread.current , "------3")
}
}
queue.async {
for i in 0 ... 10 {
debugPrint(i , Thread.current , "------4")
}
}
queue.async {
for i in 0 ... 10 {
debugPrint(i , Thread.current , "------5")
}
}

举的例子比较简单,遇到实际问题其实是很复杂的(强烈建议多看第三方框架,看看别人怎么封装的,多学习),考虑的问题比较多。


第三个问题,怎样控制并发数量?问题二中已经回答了设置OperationQueue的maxConcurrentOperationCount就可以控制并发量了。GCD中的话就可以使用信号量(DispatchSemaphore)去控制并发量了,用法也很简单。信号量还有很多其他的用法,比如将异步变成同步(这种骚操作不建议去使用信号量),资源的保护等等

let queue = DispatchQueue.global(qos:    DispatchQoS.QoSClass.background)
/// 初始化信号量为2,最大并发为2,为0时会等待
let semap = DispatchSemaphore.init(value: 2)
semap.wait() // 信号量减1
queue.async {
debugPrint("开始执行", Thread.current)
sleep(2)
debugPrint("执行完成" , Thread.current)
semap.signal() // 信号量加1
}
semap.wait()
queue.async {
debugPrint("开始执行", Thread.current)
sleep(2)
debugPrint("执行完成" , Thread.current)
semap.signal()
}
semap.wait()
queue.async {
debugPrint("开始执行", Thread.current)
sleep(2)
debugPrint("执行完成" , Thread.current)
semap.signal()
}

第四个问题,怎样暂停/挂起任务?GCD可以但是相当麻烦,OperationQueue就很简单了,可以挂起,暂停所有的任务,也可以取消所有任务,在控制任务执行上,还是很方便的。这个可以参考下Alamofire中的设计,可以避免双重回调嵌套,在此不仔细详解,大概提下Alamofire设计的思路,有个方法是json序列化的,在我们最后获取数据的回调block中,但是,调用这个方法时,是将任务放到了OperationQueue中并且设置了maxConcurrentOperationCount为1,并且设置isSuspended为true,什么时候执行呢,当后台数据返回成功时isSuspended置为false,开始执行序列化的代码,执行完后我们在回调中获取后台返回的数据,没有嵌套block。


整个总结比较简单,只是大概说了下可能会遇到的问题,提供下简单的解决思路,解决复杂的问题,还需要再去多尝试,多去了解相关的文档,了解每一个API甚至参数使用的场景,或许用不到,但是万一在解决bug时不小心找到灵感了呢,书到用时方恨少。但是还是想强调下:线程复杂必死。



链接:https://www.jianshu.com/p/5fb6e565ee23

收起阅读 »

iOS -性能优化 _RunLoop原理去监控

iOS
ios 利用RunLoop的原理去监控卡顿一、卡顿问题的几种原因复杂 UI 、图文混排的绘制量过大;在主线程上做网络同步请求;在主线程做大量的 IO 操作;运算量过大,CPU 持续高占用;死锁和主子线程抢锁。二、监测卡顿的思路监测FPS:FPS 是一秒显示的帧...
继续阅读 »

ios 利用RunLoop的原理去监控卡顿

一、卡顿问题的几种原因

  • 复杂 UI 、图文混排的绘制量过大;
  • 在主线程上做网络同步请求;
  • 在主线程做大量的 IO 操作;
  • 运算量过大,CPU 持续高占用;
  • 死锁和主子线程抢锁。

二、监测卡顿的思路

  1. 监测FPS:FPS 是一秒显示的帧数,也就是一秒内画面变化数量。如果按照动画片来说,动画片的 FPS 就是 24,是达不到 60 满帧的。也就是说,对于动画片来说,24 帧时虽然没有 60 帧时流畅,但也已经是连贯的了,所以并不能说 24 帧时就算是卡住了。 由此可见,简单地通过监视 FPS 是很难确定是否会出现卡顿问题了,所以我就果断弃了通过监视 FPS 来监控卡顿的方案。
  2. RunLoop:通过监控 RunLoop 的状态来判断是否会出现卡顿。RunLoop原理这里就不再多说,主要说方法,首先明确loop的状态有六个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

kCFRunLoopEntry ,
// 进入 loop

kCFRunLoopBeforeTimers ,
// 触发 Timer 回调

kCFRunLoopBeforeSources ,
// 触发 Source0 回调

kCFRunLoopBeforeWaiting ,
// 等待 mach_port 消息

kCFRunLoopAfterWaiting ),
// 接收 mach_port 消息

kCFRunLoopExit ,
// 退出 loop

kCFRunLoopAllActivities
// loop 所有状态改变

}

我们需要监测的状态有两个:RunLoop 在进入睡眠之前和唤醒后的两个 loop 状态定义的值,分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,也就是要触发 Source0 回调和接收 mach_port 消息两个状态。

 

三、如何检查卡顿

先粗略说下步骤:

  • 创建一个 CFRunLoopObserverContext 观察者;
  • 将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察;
  • 创建一个持续的子线程专门用来监控主线程的 RunLoop 状态;
  • 一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿;
  • dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长;

上代码:


//
// SMLagMonitor.h
//
// Created by DaiMing on 16/3/28.
//

#import <Foundation/Foundation.h>

@interface SMLagMonitor : NSObject

+ (instancetype)shareInstance;

- (void)beginMonitor; //开始监视卡顿
- (void)endMonitor; //停止监视卡顿

@end


//
// SMLagMonitor.m
//
// Created by DaiMing on 16/3/28.
//

#import "SMLagMonitor.h"
#import "SMCallStack.h"
#import "SMCPUMonitor.h"

@interface SMLagMonitor() {
int timeoutCount;
CFRunLoopObserverRef runLoopObserver;
@public
dispatch_semaphore_t dispatchSemaphore;
CFRunLoopActivity runLoopActivity;
}
@property (nonatomic, strong) NSTimer
*cpuMonitorTimer;
@end

@implementation SMLagMonitor

#pragma mark - Interface
+ (instancetype)shareInstance {
static id instance = nil;
static dispatch_once_t dispatchOnce;
dispatch_once(
&dispatchOnce, ^{
instance
= [[self alloc] init];
});
return instance;
}

- (void)beginMonitor {
//监测 CPU 消耗
self.cpuMonitorTimer = [NSTimer scheduledTimerWithTimeInterval:3
target:self
selector:@selector(updateCPUInfo)
userInfo:nil
repeats:YES];
//监测卡顿
if (runLoopObserver) {
return;
}
dispatchSemaphore
= dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
//创建一个观察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver
= CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程开启一个持续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount
= 0;
dispatchSemaphore
= 0;
runLoopActivity
= 0;
return;
}
//两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
// 将堆栈信息上报服务器的代码放到这里
//出现三次出结果
// if (++timeoutCount < 3) {
// continue;
// }
NSLog(@"monitor trigger");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,
0), ^{
// [SMCallStack callStackWithType:SMCallStackTypeAll];
});
}
//end activity
}// end semaphore wait
timeoutCount = 0;
}
// end while
});

}

- (void)endMonitor {
[self.cpuMonitorTimer invalidate];
if (!runLoopObserver) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
CFRelease(runLoopObserver);
runLoopObserver
= NULL;
}

#pragma mark - Private

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
SMLagMonitor
*lagMonitor = (__bridge SMLagMonitor*)info;
lagMonitor
->runLoopActivity = activity;

dispatch_semaphore_t semaphore
= lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}


- (void)updateCPUInfo {
thread_act_array_t threads;
mach_msg_type_number_t threadCount
= 0;
const task_t thisTask = mach_task_self();
kern_return_t kr
= task_threads(thisTask, &threads, &threadCount);
if (kr != KERN_SUCCESS) {
return;
}
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount
= THREAD_INFO_MAX;
if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
threadBaseInfo
= (thread_basic_info_t)threadInfo;
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
integer_t cpuUsage
= threadBaseInfo->cpu_usage / 10;
if (cpuUsage > 70) {
//cup 消耗大于 70 时打印和记录堆栈
NSString *reStr = smStackOfThread(threads[i]);
//记录数据库中
// [[[SMLagDB shareInstance] increaseWithStackString:reStr] subscribeNext:^(id x) {}];
NSLog(@"CPU useage overload thread stack:\n%@",reStr);
}
}
}
}
}

@end

使用,直接在APP didFinishLaunchingWithOptions 方法里面这样写:

[[SMLagMonitor shareInstance] beginMonitor];

 

搞定!

链接:https://www.cnblogs.com/qiyiyifan/p/11089735.html

收起阅读 »

iOS 实例化讲解RunLoop

iOS
实例化讲解RunLoop之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原...
继续阅读 »

实例化讲解RunLoop

之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原因,本文中将用很少的篇幅来对基础内容做以介绍,然后主要利用实例来加深大家对RunLoop的理解,本文中的代码已经上传GitHub,大家可以下载查看,有问题欢迎Issue我。本文主要分为如下几个部分:

  • RunLoop的基础知识
  • 初识RunLoop,如何让RunLoop进驻线程
  • 深入理解Perform Selector
  • 一直"活着"的后台线程
  • 深入理解NSTimer
  • 让两个后台线程有依赖性的一种方式
  • NSURLConnetction的内部实现
  • AFNetWorking中是如何使用RunLoop的?
  • 其它:利用GCD实现定时器功能
  • 延伸阅读

一、RunLoop的基本概念:

什么是RunLoop?提到RunLoop,我们一般都会提到线程,这是为什么呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有'主人'的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,'撞坏'被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来)其实RunLoop是管理线程的一种机制,这种机制不仅在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observerRunLoop Mode包含以下几种: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode,至于这些mode各自的含义,读者可自己查询,网上不乏这类资源;

二、初识RunLoop,如何让RunLoop进驻线程

我们在主线程中添加如下代码:

  1. while (1) {
  2. NSLog(@"while begin");
  3. // the thread be blocked here
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. // this will not be executed
  7. NSLog(@"while end");
  8. }

这个时候我们可以看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];之后被阻塞而没有执行下面的NSLog(@"while end");同时,我们利用GCD,将这段代码放到一个后台线程中:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSLog(@"while begin");
  4. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  5. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. NSLog(@"while end");
  7. }
  8. });

这个时候我们发现这个while循环会一直在执行;这是为什么呢?我们先将这两个RunLoop分别打印出来:

主线程的RunLoop


由于这个日志比较长,我就只截取了上面的一部分。
我们再看我们新建的子线程中的RunLoop,打印出来之后:

backGroundThreadRunLoop.png


从中可以看出来:我们新建的线程中:

 

  1. sources0 = (null),
  2. sources1 = (null),
  3. observers = (null),
  4. timers = (null),

我们看到虽然有Mode,但是我们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Modeitem,如果一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSPort *macPort = [NSPort port];
  4. NSLog(@"while begin");
  5. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  6. [subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
  7. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  8. NSLog(@"while end");
  9. NSLog(@"%@",subRunLoop);
  10. }
  11. });

这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明天让RunLoop在distantFuture之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。
小结:我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item

三、深入理解Perform Selector

我们先在主线程中使用下performselector:

  1. - (void)tryPerformSelectorOnMianThread{
  2. [self performSelector:@selector(mainThreadMethod) withObject:nil]; }
  3. - (void)mainThreadMethod{
  4. NSLog(@"execute %s",__func__);
  5. // print: execute -[ViewController mainThreadMethod]
  6. }

这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. });
  5. }
  6. - (void)backGroundThread{
  7. NSLog(@"%u",[NSThread isMainThread]);
  8. NSLog(@"execute %s",__FUNCTION__);
  9. }

同样的,我们调用tryPerformSelectorOnBackGroundThread这个方法,我们会发现,下面的backGroundThread不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop,如果我们加上RunLoop:

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop run];
  6. });
  7. }

这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。从Apple的文档中我们也可以得到验证:

Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每个执行perform selector的请求都以队列的形式被放到目标线程的run loop中。然后目标线程会根据进入run loop的顺序来一一执行。

小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop

四、一直"活着"的后台线程

现在有这样一个需求,每点击一下屏幕,让子线程做一个任务,然后大家一般会想到这样的方式:

  1. @interface ViewController ()
  2. @property(nonatomic,strong) NSThread *myThread;
  3. @end
  4. @implementation ViewController
  5. - (void)alwaysLiveBackGoundThread{
  6. NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
  7. self.myThread = thread;
  8. [self.myThread start];
  9. }
  10. - (void)myThreadRun{
  11. NSLog(@"my thread run");
  12. }
  13. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  14. NSLog(@"%@",self.myThread);
  15. [self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
  16. }
  17. - (void)doBackGroundThreadWork{
  18. NSLog(@"do some work %s",__FUNCTION__);
  19. }
  20. @end

这个方法中,我们利用一个强引用来获取了后台线程中的thread,然后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时我们可以看到,在touchesBegin方法中,self.myThread是存在的,但是这是为是什么呢?这就要从线程的五大状态来说明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,但是这个线程在执行完任务之后已经死亡了,经过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工作的强烈愿望而不死亡):

  1. - (void)myThreadRun{
  2. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  3. [[NSRunLoop currentRunLoop] run]
  4. NSLog(@"my thread run");
  5. }

这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。
小结:正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在

五、深入理解NSTimer

我们平时使用NSTimer,一般是在主线程中的,代码大多如下:

  1. - (void)tryTimerOnMainThread{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
  3. selector:@selector(timerAction) userInfo:nil repeats:YES];
  4. [myTimer fire];
  5. }
  6. - (void)timerAction{
  7. NSLog(@"timer action");
  8. }

这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
  3. [myTimer fire];
  4. });

这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:

  1. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  2. [runLoop run];

这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。

六、让两个后台线程有依赖性的一种方式

给两个后台线程添加依赖可能有很多的方式,这里说明一种利用RunLoop实现的方式。原理很简单,我们先让一个线程工作,当工作完成之后唤醒另外的一线程,通过上面对RunLoop的说明,相信大家很容易能够理解这些代码:

  1. - (void)runLoopAddDependance{
  2. self.runLoopThreadDidFinishFlag = NO;
  3. NSLog(@"Start a New Run Loop Thread");
  4. NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
  5. [runLoopThread start];
  6. NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
  7. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  8. while (!_runLoopThreadDidFinishFlag) {
  9. self.myThread = [NSThread currentThread];
  10. NSLog(@"Begin RunLoop");
  11. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  12. NSPort *myPort = [NSPort port];
  13. [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
  14. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  15. NSLog(@"End RunLoop");
  16. [self.myThread cancel];
  17. self.myThread = nil;
  18. }
  19. });
  20. }
  21. - (void)handleRunLoopThreadTask
  22. {
  23. NSLog(@"Enter Run Loop Thread");
  24. for (NSInteger i = 0; i < 5; i ++) {
  25. NSLog(@"In Run Loop Thread, count = %ld", i);
  26. sleep(1);
  27. }
  28. #if 0
  29. // 错误示范
  30. _runLoopThreadDidFinishFlag = YES;
  31. // 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。
  32. // 正确的做法是使用 "selector"方法唤醒Run Loop。 即如下:
  33. #endif
  34. NSLog(@"Exit Normal Thread");
  35. [self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
  36. // NSLog(@"Exit Run Loop Thread");
  37. }

七、NSURLConnection的执行过程

在使用NSURLConnection时,我们会传入一个Delegate,当我们调用了[connection start]之后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,然后在其中的DefaultMode中添加4个source。如下图所示,CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。如下图所示:

NSURLConnection的执行过程


从中可以看出,当开始网络传输是,我们可以看到NSURLConnection创建了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket链接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并通过之前添加的source,来通知(唤醒)上层的Delegate。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:

 

  1. while (!_isEndRequest)
  2. {
  3. NSLog(@"entered run loop");
  4. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  5. }
  6. NSLog(@"main finished,task be removed");
  7. - (void)connectionDidFinishLoading:(NSURLConnection *)connection
  8. {
  9. _isEndRequest = YES;
  10. }

这里我们就可以解决下面这些疑问了:

  1. 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
  2. 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。

八、AFNetWorking中是如何使用RunLoop的?

在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。
为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item

  1. + (void)networkRequestThreadEntryPoint:(id)__unused object {
  2. @autoreleasepool {
  3. [[NSThread currentThread] setName:@"AFNetworking"];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  6. [runLoop run];
  7. }
  8. }
  9. + (NSThread *)networkRequestThread {
  10. static NSThread *_networkRequestThread = nil;
  11. static dispatch_once_t oncePredicate;
  12. dispatch_once(&oncePredicate, ^{
  13. _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
  14. [_networkRequestThread start];
  15. });
  16. return _networkRequestThread;
  17. }

这里这个NSMachPort的作用和上文中的一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。

  1. - (void)start {
  2. [self.lock lock];
  3. if ([self isCancelled]) {
  4. [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  5. } else if ([self isReady]) {
  6. self.state = AFOperationExecutingState;
  7. [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  8. }
  9. [self.lock unlock];
  10. }

GCD定时器的实现

  1. - (void)gcdTimer{
  2. // get the queue
  3. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  4. // creat timer
  5. self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
  6. // config the timer (starting time,interval)
  7. // set begining time
  8. dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
  9. // set the interval
  10. uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
  11. dispatch_source_set_timer(self.timer, start, interver, 0.0);
  12. dispatch_source_set_event_handler(self.timer, ^{
  13. // the tarsk needed to be processed async
  14. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  15. for (int i = 0; i < 100000; i++) {
  16. NSLog(@"gcdTimer");
  17. }
  18. });
  19. });
  20. dispatch_resume(self.timer);

链接:https://www.jianshu.com/p/536184bfd163
收起阅读 »

iOS 组件化方案

iOS
为什么要组件化?易移植、易维护、易重构、易根据业务做加减法、易开发理想中的组件化组件化最终应该达到每个组件可以单独开发,单独维护,不会对其他组件进行强依赖。理想的架构应该在横向上能够拆分出容器层,开源三方库,基础组件,业务形态SDK组件,普通业务组件;在纵向上...
继续阅读 »

为什么要组件化?
易移植、易维护、易重构、易根据业务做加减法、易开发

理想中的组件化
组件化最终应该达到每个组件可以单独开发,单独维护,不会对其他组件进行强依赖。
理想的架构应该在横向上能够拆分出容器层,开源三方库,基础组件,业务形态SDK组件,普通业务组件;在纵向上能够进行组件解耦,组件之间可以单独开发、维护、复用以及组件之间合理的通信机制。随着业务的复杂度增加,理想中的架构也应该不断的变化,


如何进行组件化
先进行组件的拆分,然后进行组件的之间的服务调度,然后进行事件分发包含系统事件以及组件本身自定义的事件实现比较完善的解耦

组件的拆分
将不同的业务代码按照业务的划分整合成一个独立的组件成为独立repo。

进行组件之间服务的调度实现接口依赖
1、将组件之间的依赖实体转为依赖抽象、依赖接口
2、不进行依赖直接依靠OC自身的动态性进行方法的调用

完善组件之间的通信以及事件分发
将系统事件、应用事件以及业务事件进行分发。基本可以做到每个组件之间,组件之间实现无耦合的通信,以及对系统、应用事件的感知。实现每一个组件都可以成为一个独立的APP运行

通用方案调研
目前业界有三大通用方案:面向接口进行解耦、使用URL路由的方式以及使用runtime进行解耦。各自的代表为:serviceCenter注册的BeeHive、URL注册的Router、使用runtime+Category的CTMediator
BeeHive的架构与解析


Beehive在服务调度上采用了protocol的方式,依赖指定的接口来实现组件之间的耦合。

服务注册
注册一个服务主要有两个问题:如何注册以及注册时间,BeeHive采用了三种服务注册的方式

Annotation方式注册
将所需要注册的服务通过注解的方式保存在可执行文件中,在可执行文件加载是的过程中实现protocol与class的注册。这种方式注册服务简单方便,每个组件可以在自己自行注册,不需要集中注册。整体流程对开发者比较友好

保存服务名
@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
//
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

这里是将服务名字存在__Data(这是数据段用于存放初始化好的变量)段中的指定区域,到时候和其他资源一起打入ipa中
__attribute((used))是为了防止release模式下被优化(在单测上尤其必要。有些接口只有单测代码引用了这个时候如果不加used进行修饰就无法进行单测)
为什么#要带两个""
进行注册
通过constructor进行修饰,在可执行文件加载之前设置mach-o和动态共享库的加载后的回调
在回调里面通过传过来的Mach-o的文件头地址以及之前设置好的__data段的section名字读取出存在该section的服务protocol以及class然后利用serviceCenter执行服务注册的逻辑,等待使用的时候进行初始化,避免内存常驻

__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback); //添加image的load回调
}

NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
// 读取存贮的服务的section地址
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, BeehiveServiceSectName, &size);
#else
const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
// 读取存贮的服务的section地址
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, BeehiveServiceSectName, &size);
#endif
// 遍历改section获取服务的protocol以及class
unsigned long counter = size/sizeof(void*);
for(int idx = 0; idx < counter; ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;

BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}
//register services
for (NSString *map in configs) {
NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error) {
if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {

NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];

if (protocol && clsName) {
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}

}
}
}

使用文件的方式进行注册
将所需要注册的服务通过plist文件打包到app中,在APP启动的lauch回调中实现实现protocol与class的注册

保存服务名
在文件中以key-value的形式保存服务名到plist文件中

进行注册
在app的lauch中找到对应保存服务的plist文件进行读取,然后使用serviceCenter将服务保存字典中allServicesDict,等待使用的时候进行初始化,避免内存常驻

NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
if (!plistPath) {
return;
}
NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];

[self.lock lock];
for (NSDictionary *dict in serviceList) {
NSString *protocolKey = [dict objectForKey:@"service"];
NSString *protocolImplClass = [dict objectForKey:@"impl"];
if (protocolKey.length > 0 && protocolImplClass.length > 0) {
[self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
}
}
// 使用的时候主动创建服务的实现方
id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];

使用+load的方式进行注册
无需保存文件名直接在+load方法中利用serviceCenter进行protocol与class的注册,等待使用的时候进行初始化,避免内存常驻

总结
三种注册方式,使用注解的注册方式整体来说比较优雅,对开发者也是比较友好的,注册的时机也是最靠前的。其次是+load也是执行main函数之前使用起来也比较简单。之后是文件注册,注册时间再main函数之后,APP启动中进行注册,一个是时间需要人为的去保证,其实使用起来需要统一改plist文件不是很好的处理方式。

服务的调度
服务的创建
在使用服务之前进行进行服务的获取,使用懒加载避免了服务在内存中常驻

id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];
1
服务的实现类可以是普通实例也可以是单例,业务方可以自行实现singleton方法进行选择。内部还有一个shouldCache参数决定是否使用cache(看起来作用不是很大,如果是单例则无论是否使用cache,获取的都会是唯一的实例,如果不是单例则无论是否使用cache获取的都是新的实例)。
shouldCache这里可以修改一下使用,由于业务方是不知道每次获取的服务是单例还是唯一的实例,所以确实可以增加shouldCache给业务方使用,当业务方选择使用缓存的时候,这时候如果没有缓存返回空给业务方一个感知,有缓存的时候正常返回。这样shouldCache才有意义

if (shouldCache) {
id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
if (protocolImpl) {
return protocolImpl;
}
//增加else
else {
return nil
}
}
1
2
3
4
5
6
7
8
9
10
服务获取以及移除
在已经创建的服务列表中移除所创建的服务或者获取服务。

- (id)getServiceInstanceFromServiceName:(NSString *)serviceName
{
return [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceName];
}
- (void)removeServiceWithServiceName:(NSString *)serviceName
{
[[BHContext shareInstance] removeServiceWithServiceName:serviceName];
}
1
2
3
4
5
6
7
8
服务的调用
通过协议定义好的接口进行指定服务的调用

总结
能够进行较好的服务调度基本能满足需求,使用延迟创建的方式可以避免内存常驻。区分了单例获取与普通实例获取。在cache设计上可以稍作完善,使得cache变的更有意义

进行组件之间的通信以及消息分发
BeeHive在实现组件调度的同时,实现了组件的可插拨,同时实现了一套消息分发的机制,利用BHContext容器进行组件与组件,组件之前的通信以及消息分发,完善通信机制,一定程度上做到了每一个组件都是一个五脏俱全的小APP,以及彼此之间的一些相互影响

总结
BeeHive架构下组件实现了可插拔,可以独立存在进行开发。组件之间解耦相对彻底,可以进行通信。每个组件有自己的生命周期,独立管理。组件之间通过protocol进行服务之间的调度,对开发者比较友好。同时protocol对方法调用的参数等也进行了约束,

MGJRouter架构解析


MGJRouter使用URL路由的方式进行组件之间的服务调度

服务的注册
app启动期间注册指定的scheme与block

服务的调度
通过解析scheme,进行对应block的获取以及调用

总结
总体来说比较轻量,使用起来简单,但是可用范围也有限。
1、只能注册block,可以增加protocol的注册或者实现解析指定参数利用NSInvocation进行方法的调用
2、注册过程依赖字符串,容易引发问题

CTMediator Target-Action方案
该方案没有注册的流程,使用的runtime利用字符串来实现服务的调度

服务的调度
远程调度
约定好URL规则之后,将远程调度在内部转为本地调度

本地调度
利用CTMediator使用performTarget:action:param进行方法的调度,在CTMediator中利用invocation的方式行方法的调用

分类的使用
为了避免方法签名的不确定性,CTMediator使用分类的方式明确了对外的接口同时在分类内部将参数转成字典传参,使用runtime主动发现服务方后在利用invocation进行方法的调用

总结
整体上利用runtime主动发现服务比注册服务来说解耦是比较完善的。使用category也一定程度上解决了参数的校验问题
但是在mediator内部使用仍然是perfromSelector来调用,所以整体上始终是无法做到完美的参数校验。此外增加了Category层,在里面进行参数的中转以及一些适配逻辑相比较protocol而言并没有减轻增加服务的复杂度

三种方案对比
方案 S W 适用场景
URLRouter 使用简单;支持远程调用 字符串的方式硬编码较多,维护成本大;内存常驻;组件依赖中间件;参数缺乏强约束(字典) 远程调用
Service 面向接口更符合iOS开发者习惯;编编译阶段进行检查,使用上比rumtime方式更安全;支持复杂参数 组件依赖中间件,新增服务稍微麻烦 内部调用
Runtime+category 新增组件更加快速;依赖最小;支持复杂参数 使用runtime缺少参数的校验;对开发者要求更高,使用不当容易产生问题,;不利于重构 内部调用
公司内部解决方案
抖音与头条使用的方案与Beehive类似主要使用的protocol-class的方案进行解耦

使用 TTRoute进行页面之间的跳转.
利用protocol-class进行方法的调用

————————————————

原文链接:https://blog.csdn.net/songzhuo1991/article/details/115977726

收起阅读 »

iOS - http & https & 网络请求过程

iOS
给大家总结网络请求过程:三次握手图集: 看了此图, 于是乎,问题来了, 不是TCP链接的时候需要三次握手么( http://blog.csdn.net/whuslei/article/details/6667471 ),问题确实来...
继续阅读 »

给大家总结网络请求过程:

三次握手图集:

 

看了此图, 于是乎,问题来了, 不是TCP链接的时候需要三次握手么( http://blog.csdn.net/whuslei/article/details/6667471 ),问题确实来了, 三次握手每次都需要应用层的数据报文么, 于是乎搜得答案

 

具体链接:http://blog.csdn.net/luozenghui529480823/article/details/12978957  了解了网络链接, 有必要了解HTTP 和Https ,  那么首先看一下Http :http://www.jianshu.com/p/81632fea327c 这都是深度好文啊,就连cookie你都知道原理了吧,那么看看https吧,

 

  一看我们的工程既有https又有http你会发现有这个东西,

 

既然 https如此安全, 那机制是什么呢,我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取。所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。

下面我们介绍下https:

HTTPS其实是有两部分组成:HTTP +SSL/ TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。具体是如何进行加密,解密,验证的,且看下图。

1. 客户端发起HTTPS请求

这个没什么好说的,就是用户在浏览器里输入一个https网址,然后连接到server的443端口。

2. 服务端的配置

采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl就是个不错的选择,有1年的免费服务)。这套证书其实就是一对公钥和私钥。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。

3. 传送证书

这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。

4. 客户端解析证书

这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值。然后用证书对该随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。

5. 传送加密信息

这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

6. 服务段解密信息

服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

7. 传输加密后的信息

这部分信息是服务段用私钥加密后的信息,可以在客户端被还原

8. 客户端解密信息

客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。

 

SSL的位置

SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层,而是传递给SSL层,SSL层对从应用层收到的数据进行加密,并增加自己的SSL头。

RSA性能是非常低的,原因在于寻找大素数、大数计算、数据分割需要耗费很多的CPU周期,所以一般的HTTPS连接只在第一次握手时使用非对称加密,通过握手交换对称加密密钥,在之后的通信走对称加密。

http://www.cnblogs.com/ttltry-air/archive/2012/08/20/2647898.html

HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL协议不仅仅是一套加密传输的协议,更是一件经过艺术家精心设计的艺术品,TLS/SSL中使用了非对称加密,对称加密以及HASH算法。握手过程的具体描述如下:

1.浏览器将自己支持的一套加密规则发送给网站。

2.网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。

3.浏览器获得网站证书之后浏览器要做以下工作

a)验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。

b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。

c)使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。

4.网站接收浏览器发来的数据之后要做以下的操作:

a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。

b) 使用密码加密一段握手消息,发送给浏览器。

5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密

这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。另外,HTTPS一般使用的加密与HASH算法如下:

非对称加密算法:RSA,DSA/DSS

对称加密算法:AES,RC4,3DES

HASH算法:MD5,SHA1,SHA256

总结:

服务器 用RSA生成公钥和私钥

把公钥放在证书里发送给客户端,私钥自己保存

客户端首先向一个权威的服务器检查证书的合法性,如果证书合法,客户端产生一段随机数,这个随机数就作为通信的密钥,我们称之为对称密钥,用公钥加密这段随机数,然后发送到服务器

服务器用密钥解密获取对称密钥,然后,双方就已对称密钥进行加密解密通信了

链接:https://blog.csdn.net/songzhuo1991/article/details/104349714?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-1.control&spm=1001.2101.3001.4242

收起阅读 »

iOS 对接系统“fileAPP” - 文件操作

1、前言iOS文件存储机制每个iOS程序都有一个独立的文件系统(存储空间),而且只能在对应的文件系统中进行操作,此区域被称为沙盒。应用必须待在自己的沙盒里,其他应用不能访问该沙盒。所有的非代码文件都要保存在此,例如属性文件plist、文本文件、图像、图标、媒体...
继续阅读 »

1、前言

iOS文件存储机制

每个iOS程序都有一个独立的文件系统(存储空间),而且只能在对应的文件系统中进行操作,此区域被称为沙盒。应用必须待在自己的沙盒里,其他应用不能访问该沙盒。所有的非代码文件都要保存在此,例如属性文件plist、文本文件、图像、图标、媒体资源等

沙盒中相关路径

AppName.app 应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以不能在运行时对这个目录中的内容进行修改,否则会导致应用程序无法启动。

Documents/ 保存应用程序的重要数据文件和用户数据文件等。用户数据基本上都放在这个位置(例如从网上下载的图片或音乐文件),该文件夹在应用程序更新时会自动备份,在连接iTunes时也可以自动同步备份其中的数据。

Library:这个目录下有两个子目录,可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份.

Library/Caches: 保存应用程序使用时产生的支持文件和缓存文件(保存应用程序再次启动过程中需要的信息),还有日志文件最好也放在这个目录。iTunes 同步时不会备份该目录并且可能被其他工具清理掉其中的数据。
Library/Preferences: 保存应用程序的偏好设置文件。NSUserDefaults类创建的数据和plist文件都放在这里。会被iTunes备份。

相关问题

因为每个app下的沙盒路径都是封闭的,其他应用访问不到,也就导致了文件不能共享。只能自己在代码中维护本app内的文件。这给用户带来很多的不便。因此,苹果除了一个系统的“文件”的app,用来管理app内的文件。但是系统的“文件”这个app不是可以管理全部的app下的文件,他只能管理对“文件”这个app开放权限的APP。

计划

最近在做IM开发,涉及到文件传输,我们不想做的特别封闭,所以就想可以发送app以外的文件(比如微信中保存下来的)具体实现就是:从微信里下载的文件保存到“ fileAPP”内,然后我们在发送文件时,可以发送本地文件,也可以发送“ fileAPP”内保存的文件

2、让自己的app对“ fileAPP”开放管理权限

2.1、在Identifiers下选择你要添加icloud的boundid把icloud配置勾选上


2.2、工程配置



点击Caoablity左侧的加号,搜索iCloud,然后添加即可

2.3、 设置info.plist

第一个是 UIFileSharingEnabled,这个可以使 iTunes 分享你文件夹内的内容;第二个是 LSSupportsOpeningDocumentsInPlace ,它保证了你文件夹内本地文件的获取权限,你需要将这两个键值对的值设置为 YES

以上设置完,重新运行app之后,在系统的“ fileAPP”中会出现一个以你的APP命名的文件夹,里面包含了APP内沙盒的目录和文件






但是这里面的文件很乱,而且会将沙盒内 Documents 文件夹内的所有文件都显示出来

2.4、如何在fileAPP里隐藏重要的文件?

如果它不是那么重要的,我们可以将它们存放在 cachesDirectory 或者是 temporaryDirectory 文件夹下面;如果它是重要的文件,大多数情况下,我们是需要将它们备份在 iCloud 上的,这样的文件我们建议将它存放在 applicationSupportDirectory 目录下




以上,是将自己的文件共享给 fileAPP

3、app如何获取到“文件”app下管理的文件到本app内

3.1、调起fileAPP 文件目录

//打开文件APP
- (void)presentDocumentCloud {
NSArray *documentTypes = @[@"public.content", @"public.text", @"public.source-code ", @"public.image", @"public.audiovisual-content", @"com.adobe.pdf", @"com.apple.keynote.key", @"com.microsoft.word.doc", @"com.microsoft.excel.xls", @"com.microsoft.powerpoint.ppt"];

UIDocumentPickerViewController *documentPickerViewController = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:documentTypes inMode:UIDocumentPickerModeOpen];
documentPickerViewController.delegate = self;
[self presentViewController:documentPickerViewController animated:YES completion:nil];
}

这里的documentTypes数组内设置要拿的文件格式

UIDocumentPickerMode有四种:

typedef NS_ENUM(NSUInteger, UIDocumentPickerMode) {
UIDocumentPickerModeImport,
UIDocumentPickerModeOpen,
UIDocumentPickerModeExportToService,
UIDocumentPickerModeMoveToService
} API_DEPRECATED("Use appropriate initializers instead",ios(8.0,14.0)) API_UNAVAILABLE(tvOS);


UIDocumentPickerModeImport : 将文件拿出来UIDocumentPickerModeOpen:打开文件
后面是将文件传到fileAPP内的操作,类似于微信的保存文件到fileAPP内的操作。这里官方文档之给了前两个,后面的我们也没涉及到,以后研究吧。

3.2、设置代理
<UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate>
3.3、实现代理
#pragma mark - UIDocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {

BOOL fileUrlAuthozied = [url startAccessingSecurityScopedResource];
if (fileUrlAuthozied && [self iCloudEnable]) {
//通过文件协调工具来得到新的文件地址,以此得到文件保护功能
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init];
NSError *error;

[fileCoordinator coordinateReadingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {
//读取文件
NSString *fileName = [newURL lastPathComponent];
fileName = [fileName stringByRemovingPercentEncoding];
NSData * data = [NSData dataWithContentsOfURL:newURL];

self.completionBlock(data,fileName);
self.completionBlock = nil;
}];
[url stopAccessingSecurityScopedResource];
}
}

这里需要将拿到的url做处理,应为这里拿到的url是fileAPP文件的本地文件路径,在自己的app内通过[NSData dataWithContentsOfURL:url]方法是无法拿到数据的,应该是只能拿自己沙盒文件路径的设置的原因。

 [fileCoordinator coordinateReadingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {

}];

这么处理后,拿到的newUrl就是本地的路径了,通过[NSData dataWithContentsOfURL:url]即可拿到文件data。

之后即可对文件进行发送上传等操作。


作者:冰棍儿好烫嘴
链接:https://www.jianshu.com/p/19b1c0f124e2



收起阅读 »

swift基础语法(内部函数,外部函数)

内部函数: 默认情况下的参数都是内部参数外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,         只能通过查看头文件的形式理解参数的含义    ...
继续阅读 »
内部函数: 默认情况下的参数都是内部参数
外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,
         只能通过查看头文件的形式理解参数的含义
        那么能不能和OC一样让调用者直观的知道参数的含义呢? 使用外部参数
         外部参数只能外部用, 函数内部不能使用, 函数内部只能使用内部参数
func divisionOpertaion1(a: Double, b:Double) -> Double{
    return a / b
}
func divisionOpertaion2(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
func divisionOpertaion3(dividend a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion3(dividend: 10, divisor: 3.5))
func divisionOpertaion4(a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion4(10, divisor: 3.5))
输出结果:
2.85714285714286
2.85714285714286
 
 
func divisionOpertaion(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
print(divisionOpertaion(10, divisor: 3.5))
输出结果:2.85714285714286
 
默认参数:
可以在定义函数的时候给某个参数赋值, 当外部调用没有传递该参数时会自动使用默认值
func joinString(s1:String ,toString s2:String, jioner s3:String) ->String
{
    return s1 + s3 + s2;
}
func joinString2(s1:String ,toString
                 s2:String, jioner
                 s3:String = "❤️") ->String
{
    return s1 + s3 + s2;
}
print(joinString2("hi", toString:"beauty"))
输出结果:hi❤️beauty
 
如果指定了默认参数, 但是确没有声明外部参数时
系统会自动把内部参数名称既作为内部参数也作为外部参数名称
并且在调用时如果需要修改默认参数的值必须写上外部参数名称
func joinString3(s1:String ,toString
                 s2:String,
             jioner:String = "❤️") ->String
{
    return s1 + jioner + s2;
}
print(joinString3("hi", toString:"beauty", jioner:"🐔"))
输出结果: hi🐔beauty
 
在其它语言中默认参数智能出现在参数列表的最后面, 但是在Swift中可以出现在任何位置
func joinString4(s1:String ,
             jioner:String = "❤️",
        toString s2:String) ->String
{
    return s1 + jioner + s2;
}
print(joinString4("hi", jioner:"🐔", toString:"beauty"))
输出结果: hi🐔beauty
 
常量参数和遍历参数:
默认情况下Swift中所有函数的参数都是常量参数
如果想在函数中修改参数, 必须在参数前加上var
func swap(var a:Int, var b:Int)
{
    print("交换前 a = \(a) b = \(b)")
    let temp = a;
    a = b;
    b = temp;
    print("交换后 a = \(a) b = \(b)")
}
swap(10, b: 20)
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
inout参数
如果想在函数中修改外界传入的参数
可以将参数的var换成inout, 这回会传递参数本身而不是参数的值
func swap(inout a:Int, inout b:Int)
{
    let temp = a;
    a = b;
    b = temp;
}
var x1 = 10;
var y1 = 20;
print("交换前 a = \(x1) b = \(y1)")
swap(&x1, b: &y1)
print("交换后 a = \(x1) b = \(y1)")
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
 
变参函数
如果没有变参函数 , 并且函数的参数个数又不确定那么只能写多个方法或者用将函数参数改为集合
变参只能放到参数列表的最后一位, 变参必须指定数据类型, 变参只能是同种类型的数据
 
func add(num1:Int, num2:Int, num3:Int) -> Int
{
    let sum = num1 + num2 + num3
    return sum
}
print(add(1, num2: 2, num3: 3))
输出结果:6
 
func add(nums:[Int]) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add([1, 2, 3]))
输出结果:6
 
func add(nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(1, 2, 3))
输出结果:6
 
func add(other:Int, nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(99, nums: 1, 2, 3))
输出结果:6
收起阅读 »

swift基础语法(内部函数,外部函数)

内部函数: 默认情况下的参数都是内部参数外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,         只能通过查看头文件的形式理解参数的含义    ...
继续阅读 »
内部函数: 默认情况下的参数都是内部参数
外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,
         只能通过查看头文件的形式理解参数的含义
        那么能不能和OC一样让调用者直观的知道参数的含义呢? 使用外部参数
         外部参数只能外部用, 函数内部不能使用, 函数内部只能使用内部参数
func divisionOpertaion1(a: Double, b:Double) -> Double{
    return a / b
}
func divisionOpertaion2(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
func divisionOpertaion3(dividend a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion3(dividend: 10, divisor: 3.5))
func divisionOpertaion4(a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion4(10, divisor: 3.5))
输出结果:
2.85714285714286
2.85714285714286
 
 
func divisionOpertaion(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
print(divisionOpertaion(10, divisor: 3.5))
输出结果:2.85714285714286
 
默认参数:
可以在定义函数的时候给某个参数赋值, 当外部调用没有传递该参数时会自动使用默认值
func joinString(s1:String ,toString s2:String, jioner s3:String) ->String
{
    return s1 + s3 + s2;
}
func joinString2(s1:String ,toString
                 s2:String, jioner
                 s3:String = "❤️") ->String
{
    return s1 + s3 + s2;
}
print(joinString2("hi", toString:"beauty"))
输出结果:hi❤️beauty
 
如果指定了默认参数, 但是确没有声明外部参数时
系统会自动把内部参数名称既作为内部参数也作为外部参数名称
并且在调用时如果需要修改默认参数的值必须写上外部参数名称
func joinString3(s1:String ,toString
                 s2:String,
             jioner:String = "❤️") ->String
{
    return s1 + jioner + s2;
}
print(joinString3("hi", toString:"beauty", jioner:"🐔"))
输出结果: hi🐔beauty
 
在其它语言中默认参数智能出现在参数列表的最后面, 但是在Swift中可以出现在任何位置
func joinString4(s1:String ,
             jioner:String = "❤️",
        toString s2:String) ->String
{
    return s1 + jioner + s2;
}
print(joinString4("hi", jioner:"🐔", toString:"beauty"))
输出结果: hi🐔beauty
 
常量参数和遍历参数:
默认情况下Swift中所有函数的参数都是常量参数
如果想在函数中修改参数, 必须在参数前加上var
func swap(var a:Int, var b:Int)
{
    print("交换前 a = \(a) b = \(b)")
    let temp = a;
    a = b;
    b = temp;
    print("交换后 a = \(a) b = \(b)")
}
swap(10, b: 20)
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
inout参数
如果想在函数中修改外界传入的参数
可以将参数的var换成inout, 这回会传递参数本身而不是参数的值
func swap(inout a:Int, inout b:Int)
{
    let temp = a;
    a = b;
    b = temp;
}
var x1 = 10;
var y1 = 20;
print("交换前 a = \(x1) b = \(y1)")
swap(&x1, b: &y1)
print("交换后 a = \(x1) b = \(y1)")
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
 
变参函数
如果没有变参函数 , 并且函数的参数个数又不确定那么只能写多个方法或者用将函数参数改为集合
变参只能放到参数列表的最后一位, 变参必须指定数据类型, 变参只能是同种类型的数据
 
func add(num1:Int, num2:Int, num3:Int) -> Int
{
    let sum = num1 + num2 + num3
    return sum
}
print(add(1, num2: 2, num3: 3))
输出结果:6
 
func add(nums:[Int]) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add([1, 2, 3]))
输出结果:6
 
func add(nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(1, 2, 3))
输出结果:6
 
func add(other:Int, nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(99, nums: 1, 2, 3))
输出结果:6
收起阅读 »

swift 基础语法(19-闭包,闭包函数回调,尾随闭包,闭包捕获值)

闭包:函数是闭包的一种类似于OC语言的block闭包表达式(匿名函数) -- 能够捕获上下文中的值语法: in关键字的目的是便于区分返回值和执行语句闭包表达式的类型和函数的类型一样, 是参数加上返回值, 也就是in之前的部分{   ...
继续阅读 »
闭包:
函数是闭包的一种
类似于OC语言的block
闭包表达式(匿名函数) -- 能够捕获上下文中的值

语法: in关键字的目的是便于区分返回值和执行语句
闭包表达式的类型和函数的类型一样, 是参数加上返回值, 也就是in之前的部分
{
    (参数) -> 返回值类型 in
    执行语句
}
完整写法
let say:(String) -> Void = {
    (name: String) -> Void in
    print("hi \(name)")
}
say("qbs")
输出结果:  hi qbs
 
没有返回值写法
let say:(String) ->Void = {
    (name: String) in
    print("hi \(name)")
}
say("qbs")
输出结果:  hi qbs
 
没有参数没有返回值写法
let say:() ->Void = {
    print("hi qbs")
}
say()
输出结果:  hi qbs
 
 
闭包表达式作为回调函数
传统数组排序写法:
缺点: 不一定是小到大, 不一定是全部比较, 有可能只比较个位数
           所以, 如何比较可以交给调用者决定
func bubbleSort(inout array:[Int])
{
    let count = array.count;
    for var i = 1; i < count; i++
    {
        for var j = 0; j < (count - i); j++
        {
            if array[j] > array[j + 1]
            {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}
 
闭包写法:
func bubbleSort(inout array:[Int], cmp: (Int, Int) -> Int)
{
    let count = array.count;
    for var i = 1; i < count; i++
    {
        for var j = 0; j < (count - i); j++
        {
            if cmp(array[j], array[j + 1]) == -1
            {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}
 
let cmp = {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
}
var arr:Array<Int> = [31, 13, 52, 84, 5]
bubbleSort(&arr, cmp: cmp)
print(arr)
 
输出结果:
[84, 52, 31, 13, 5]
 
 
闭包作为参数传递
var arr:Array<Int> = [31, 13, 52, 84, 5]
bubbleSort(&arr, cmp: {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
})
print(arr)
输出结果:
[84, 52, 31, 13, 5]
 
尾随闭包:
如果闭包是最后一个参数, 可以直接将闭包写到参数列表后面
这样可以提高阅读性. 称之为尾随闭包
bubbleSort(&arr) {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
}
 
闭包表达式优化
 1.类型优化, 由于函数中已经声明了闭包参数的类型
   所以传入的实参可以不用写类型
 2.返回值优化, 同理由于函数中已经声明了闭包的返回值类型
   所以传入的实参可以不用写类型
 3.参数优化, swift可以使用$索引的方式来访问闭包的参数, 默认从0开始
bubbleSort(&arr){
   (a , b) -> Int in
   (a , b) in
    if $0 > $1{
        return 1;
    }else if $0 < $1
    {
        return -1;
    }else
    {
        return 0;
    }
}
 
 
如果只有一条语句可以省略return
let hehe = {
    "我是qbs"
}
闭包捕获值
func getIncFunc() -> (Int) -> Int
{
    var max = 10
    func incFunc(x :Int) ->Int{
        print("incFunc函数结束")
        max++
        return max + x
    }
    当执行到这一句时inc参数就应该被释放了
    但是由于在内部函数中使用到了它, 所以它被捕获了
    同理, 当执行完这一句时max变量就被释放了
    但是由于在内部函数中使用到了它, 所以它被捕获了
    print("getIncFunc函数结束")
    return incFunc
}
 
被捕获的值会和与之对应的方法绑定在一起
同一个方法中的变量会被绑定到不同的方法中
let incFunc = getIncFunc()
print("---------")
print(incFunc(5))
print("---------")
print(incFunc(5))
输出结果:
getIncFunc函数结束
---------
incFunc
函数结束
16
---------
incFunc
函数结束
17
 
 
let incFunc2 = getIncFunc(5)
print(incFunc2(5))
输出结果:
getIncFunc函数结束
incFunc函数结束
16
收起阅读 »

IOS-实例化讲解RunLoop(应用于子线程)

iOS
实例化讲解RunLoop之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原...
继续阅读 »

实例化讲解RunLoop

之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原因,本文中将用很少的篇幅来对基础内容做以介绍,然后主要利用实例来加深大家对RunLoop的理解,本文中的代码已经上传GitHub,大家可以下载查看,有问题欢迎Issue我。本文主要分为如下几个部分:

  • RunLoop的基础知识
  • 初识RunLoop,如何让RunLoop进驻线程
  • 深入理解Perform Selector
  • 一直"活着"的后台线程
  • 深入理解NSTimer
  • 让两个后台线程有依赖性的一种方式
  • NSURLConnetction的内部实现
  • AFNetWorking中是如何使用RunLoop的?
  • 其它:利用GCD实现定时器功能
  • 延伸阅读

一、RunLoop的基本概念:

什么是RunLoop?提到RunLoop,我们一般都会提到线程,这是为什么呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有'主人'的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,'撞坏'被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来)其实RunLoop是管理线程的一种机制,这种机制不仅在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observerRunLoop Mode包含以下几种: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode,至于这些mode各自的含义,读者可自己查询,网上不乏这类资源;

二、初识RunLoop,如何让RunLoop进驻线程

我们在主线程中添加如下代码:

  1. while (1) {
  2. NSLog(@"while begin");
  3. // the thread be blocked here
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. // this will not be executed
  7. NSLog(@"while end");
  8. }

这个时候我们可以看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];之后被阻塞而没有执行下面的NSLog(@"while end");同时,我们利用GCD,将这段代码放到一个后台线程中:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSLog(@"while begin");
  4. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  5. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. NSLog(@"while end");
  7. }
  8. });

这个时候我们发现这个while循环会一直在执行;这是为什么呢?我们先将这两个RunLoop分别打印出来:

主线程的RunLoop


由于这个日志比较长,我就只截取了上面的一部分。
我们再看我们新建的子线程中的RunLoop,打印出来之后:

backGroundThreadRunLoop.png


从中可以看出来:我们新建的线程中:

 

  1. sources0 = (null),
  2. sources1 = (null),
  3. observers = (null),
  4. timers = (null),

我们看到虽然有Mode,但是我们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Modeitem,如果一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSPort *macPort = [NSPort port];
  4. NSLog(@"while begin");
  5. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  6. [subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
  7. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  8. NSLog(@"while end");
  9. NSLog(@"%@",subRunLoop);
  10. }
  11. });

这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明天让RunLoop在distantFuture之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。
小结:我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item

三、深入理解Perform Selector

我们先在主线程中使用下performselector:

  1. - (void)tryPerformSelectorOnMianThread{
  2. [self performSelector:@selector(mainThreadMethod) withObject:nil]; }
  3. - (void)mainThreadMethod{
  4. NSLog(@"execute %s",__func__);
  5. // print: execute -[ViewController mainThreadMethod]
  6. }

这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. });
  5. }
  6. - (void)backGroundThread{
  7. NSLog(@"%u",[NSThread isMainThread]);
  8. NSLog(@"execute %s",__FUNCTION__);
  9. }

同样的,我们调用tryPerformSelectorOnBackGroundThread这个方法,我们会发现,下面的backGroundThread不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop,如果我们加上RunLoop:

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop run];
  6. });
  7. }

这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。从Apple的文档中我们也可以得到验证:

Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每个执行perform selector的请求都以队列的形式被放到目标线程的run loop中。然后目标线程会根据进入run loop的顺序来一一执行。

小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop

四、一直"活着"的后台线程

现在有这样一个需求,每点击一下屏幕,让子线程做一个任务,然后大家一般会想到这样的方式:

  1. @interface ViewController ()
  2. @property(nonatomic,strong) NSThread *myThread;
  3. @end
  4. @implementation ViewController
  5. - (void)alwaysLiveBackGoundThread{
  6. NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
  7. self.myThread = thread;
  8. [self.myThread start];
  9. }
  10. - (void)myThreadRun{
  11. NSLog(@"my thread run");
  12. }
  13. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  14. NSLog(@"%@",self.myThread);
  15. [self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
  16. }
  17. - (void)doBackGroundThreadWork{
  18. NSLog(@"do some work %s",__FUNCTION__);
  19. }
  20. @end

这个方法中,我们利用一个强引用来获取了后台线程中的thread,然后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时我们可以看到,在touchesBegin方法中,self.myThread是存在的,但是这是为是什么呢?这就要从线程的五大状态来说明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,但是这个线程在执行完任务之后已经死亡了,经过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工作的强烈愿望而不死亡):

  1. - (void)myThreadRun{
  2. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  3. [[NSRunLoop currentRunLoop] run]
  4. NSLog(@"my thread run");
  5. }

这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。
小结:正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在

五、深入理解NSTimer

我们平时使用NSTimer,一般是在主线程中的,代码大多如下:

  1. - (void)tryTimerOnMainThread{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
  3. selector:@selector(timerAction) userInfo:nil repeats:YES];
  4. [myTimer fire];
  5. }
  6. - (void)timerAction{
  7. NSLog(@"timer action");
  8. }

这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
  3. [myTimer fire];
  4. });

这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:

  1. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  2. [runLoop run];

这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。

六、让两个后台线程有依赖性的一种方式

给两个后台线程添加依赖可能有很多的方式,这里说明一种利用RunLoop实现的方式。原理很简单,我们先让一个线程工作,当工作完成之后唤醒另外的一线程,通过上面对RunLoop的说明,相信大家很容易能够理解这些代码:

  1. - (void)runLoopAddDependance{
  2. self.runLoopThreadDidFinishFlag = NO;
  3. NSLog(@"Start a New Run Loop Thread");
  4. NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
  5. [runLoopThread start];
  6. NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
  7. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  8. while (!_runLoopThreadDidFinishFlag) {
  9. self.myThread = [NSThread currentThread];
  10. NSLog(@"Begin RunLoop");
  11. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  12. NSPort *myPort = [NSPort port];
  13. [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
  14. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  15. NSLog(@"End RunLoop");
  16. [self.myThread cancel];
  17. self.myThread = nil;
  18. }
  19. });
  20. }
  21. - (void)handleRunLoopThreadTask
  22. {
  23. NSLog(@"Enter Run Loop Thread");
  24. for (NSInteger i = 0; i < 5; i ++) {
  25. NSLog(@"In Run Loop Thread, count = %ld", i);
  26. sleep(1);
  27. }
  28. #if 0
  29. // 错误示范
  30. _runLoopThreadDidFinishFlag = YES;
  31. // 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。
  32. // 正确的做法是使用 "selector"方法唤醒Run Loop。 即如下:
  33. #endif
  34. NSLog(@"Exit Normal Thread");
  35. [self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
  36. // NSLog(@"Exit Run Loop Thread");
  37. }

七、NSURLConnection的执行过程

在使用NSURLConnection时,我们会传入一个Delegate,当我们调用了[connection start]之后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,然后在其中的DefaultMode中添加4个source。如下图所示,CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。如下图所示:

NSURLConnection的执行过程


从中可以看出,当开始网络传输是,我们可以看到NSURLConnection创建了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket链接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并通过之前添加的source,来通知(唤醒)上层的Delegate。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:

 

  1. while (!_isEndRequest)
  2. {
  3. NSLog(@"entered run loop");
  4. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  5. }
  6. NSLog(@"main finished,task be removed");
  7. - (void)connectionDidFinishLoading:(NSURLConnection *)connection
  8. {
  9. _isEndRequest = YES;
  10. }

这里我们就可以解决下面这些疑问了:

  1. 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
  2. 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。

八、AFNetWorking中是如何使用RunLoop的?

在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。
为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item

  1. + (void)networkRequestThreadEntryPoint:(id)__unused object {
  2. @autoreleasepool {
  3. [[NSThread currentThread] setName:@"AFNetworking"];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  6. [runLoop run];
  7. }
  8. }
  9. + (NSThread *)networkRequestThread {
  10. static NSThread *_networkRequestThread = nil;
  11. static dispatch_once_t oncePredicate;
  12. dispatch_once(&oncePredicate, ^{
  13. _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
  14. [_networkRequestThread start];
  15. });
  16. return _networkRequestThread;
  17. }

这里这个NSMachPort的作用和上文中的一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。

  1. - (void)start {
  2. [self.lock lock];
  3. if ([self isCancelled]) {
  4. [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  5. } else if ([self isReady]) {
  6. self.state = AFOperationExecutingState;
  7. [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  8. }
  9. [self.lock unlock];
  10. }

GCD定时器的实现

  1. - (void)gcdTimer{
  2. // get the queue
  3. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  4. // creat timer
  5. self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
  6. // config the timer (starting time,interval)
  7. // set begining time
  8. dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
  9. // set the interval
  10. uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
  11. dispatch_source_set_timer(self.timer, start, interver, 0.0);
  12. dispatch_source_set_event_handler(self.timer, ^{
  13. // the tarsk needed to be processed async
  14. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  15. for (int i = 0; i < 100000; i++) {
  16. NSLog(@"gcdTimer");
  17. }
  18. });
  19. });
  20. dispatch_resume(self.timer);
  21. }

链接: https://blog.csdn.net/qq_22389025/article/details/85264178

收起阅读 »

iOS Swift 高阶函数

iOS
在Swift的集合类型中,有许多十分便捷的函数。相比于Objective-C,这些高阶函数会引起你的极度舒适。因为在Swift的许多函数中引入了闭包元素,这就直接造就了它的灵活性,简直就是极致的便捷。下面就来对Swift集合类中的这些高阶函数进行总结。// 全...
继续阅读 »

在Swift的集合类型中,有许多十分便捷的函数。相比于Objective-C,这些高阶函数会引起你的极度舒适。因为在Swift的许多函数中引入了闭包元素,这就直接造就了它的灵活性,简直就是极致的便捷。

下面就来对Swift集合类中的这些高阶函数进行总结。

// 全文的基础数据
let numbers = [7, 6, 10, 9, 8, 1, 2, 3, 4, 5]
1
2
1.sort函数
对原集合进行给定条件排序。
无返回值,直接修改原集合,所以这个集合应该是可变类型的。

var sortArr = numbers
numbers.sort { a, b in
return a < b
}
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1
2
3
4
5
另外,系统还定义了一个sort()函数,即对集合进行升序排序的函数。但这个函数并不是上面函数不传入缺省值的情况,而是另外一个函数。

var sortArr2 = numbers
numbers.sort()
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1
2
3
2.sorted函数
sorted函数与sort函数对应。
将集合进行给定条件排序,返回一个新的集合,不修改原集合。

let sortedArr = numbers.sorted { a, b in
return a > b
}
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// sorted()函数
let sortedArr2 = numbers.sorted()
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// 闭包简写
let sortedArr3 = sortedArr2.sorted(by: >)
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
1
2
3
4
5
6
7
8
9
10
11
12
闭包的省略写法
因为在高阶函数中大部分都使用了闭包,所以我认为有必要做一个铺垫,以更好地理解本文。清楚闭包简写的请跳过本段,直奔第3条。

由于sort函数使用了闭包,所以自主定义的闭包可以简写为如下格式:

numbers.sort(by: >)
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
1
2
以上述方法为例,一个完整的闭包应该是这样的:

numbers.sorted { (a: Int, b: Int) -> Bool in
return a > b
}
1
2
3
然后,可以省略闭包中的返回值。

numbers.sorted { (a: Int, b: Int) in
return a > b
}
1
2
3
然后,再可以省略形参的类型,让编译器去自主推断。

numbers.sorted { a, b in
return a > b
}
1
2
3
再然后,还可以让$0,$1…来代替第一个,第二个形参,以此类推。

numbers.sorted { return $0 > $1 }
1
再然后,省略return。一般的,到这里也就足够简化了。毕竟在实际开发中我们需要使用闭包中的参数进行一些复杂的判断。

numbers.sorted { $0 > $1 }
1
如果你不需要复杂的判断,那么还可以写成下面这样,代表降序排序。

numbers.sorted(by: >)
1
3.map函数
按照闭包中的返回结果,将集合中对应元素进行替代,也就是映射函数。

// 数组数值转换为其各自平方
let mapArr = numbers.map { $0 * $0 }
// [49, 36, 100, 81, 64, 1, 4, 9, 16, 25]
1
2
3
可选类型的 map, flatMap函数
另外,不仅CollectionType有map和flatMap函数,在Optional类型中,也存在这两个函数。
它们的作用是对可选类型就行解包操作,若有值则进入闭包,并返回一个 Optional类型;若为nil,则直接返回当前可选类型的nil。

let num1: Int? = 3
let num2: Int? = nil

let numMap1 = num1.map {
$0 * 2
}
numMap1 // 6
type(of: numMap1) // Optional<Int>.Type

let numMap2 = num2.map {
$0 == 0
}
numMap2 // nil
type(of: numMap2) // Optional<Bool>.Type


let numFlatMap1 = num1.flatMap {
$0 * $0
}
numFlatMap1 // 9
type(of: numFlatMap1) // Optional<Int>.Type

let numFlatMap2 = num2.flatMap {
$0 == 0
}
numFlatMap2 // nil
type(of: numFlatMap2) // Optional<Bool>.Type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
还有一种应用场景,就是解析可选类型的时候,map和flatMap函数会让你的代码更加优雅。

举个例子,当解析并判断可选类型的时候,你可能会经过一堆if或者guard判断,如下所示:

func loadURL(url: URL) {
print(url.absoluteString)
}

let urlStr: String? = "https://github.com/wangyanchang21"
guard let siteStr = urlStr else {
assert(false)
}
guard let url = URL(string: siteStr) else {
assert(false)
}
loadURL(url: url)
1
2
3
4
5
6
7
8
9
10
11
12
如果使用map和flatMap函数的话,就会有十分优雅的感觉。

// 这行优雅的代码代替上面的代码
urlStr.flatMap(URL.init).map(loadURL)
1
2
但有一点需要注意,这里 map替换 flatMap会报错, 原因在于 flatMap闭包可以返回 nil, 而 map闭包不可以。就如下面的代码编译不会通过:

// compile error
// urlStr.map(URL.init).map(loadURL)
1
2
再举一个例子:

let date: Date? = Date()
let format = date.map(DateFormatter().string)
1
2
我在函数的闭包形式中也写过这种优雅的写法具体是怎么回事。有兴趣可以了解一下。

4.flatMap函数
也是一种映射函数,这个函数具有多重功能,所以也就造成了这个函数有一个历史问题,稍后会解释。

第一种情况,解析首层元素,若有nil则过滤,就不会降维

let optLatticeNumbers = [[1, Optional(2), 3], [3, nil, 5], nil]
// 解析首层元素, 若有nil则过滤, 就不会降维
let flatMapArr2 = optLatticeNumbers.flatMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
1
2
3
4
第二种情况,解析首层元素,若没有nil,则会降维

let latticeNumbers = [[1, Optional(2), 3], [3, nil, 5]]
// 解析首层元素, 若没有nil, 则会降维
let flatMapArr = latticeNumbers.flatMap { $0 }
// [1, 2, 3, 3, nil, 5]
1
2
3
4
所以flatMap的功能就有两个了,一个功能是解析并过滤首层元素为nil的元素,一个功能是对多维集合进行降维。原因是,其实这是两个功能是两个函数,只是在调用时代码上没有区别。

flatMap和compactMap的关系
但从表面上看,flatMap函数违背了单一功能原则,将过滤nil和降维两个功能于隐藏条件中进行判定。这也就是那个历史问题。

因此,为了将过滤nil和降维两个功能于区分开,swift4.1开始,就只保留了降维的flatMap函数,并弃用了过滤nil的flatMap函数,又用开放的新函数compactMap来替代弃用的函数。

所以,当需要过滤nil的时候,请使用compactMap函数;当需要进行降维时,请使用flatMap函数。这也就是flatMap和compactMap之间的区别。

5.compactMap函数
Swift4.1开始开放的一种映射函数,会解析并过滤首层元素为nil的元素。

let compactMapArr = optLatticeNumbers.compactMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
let compactMapArr2 = latticeNumbers.compactMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
1
2
3
4
compactMap函数作为过滤nil的flatMap函数的替代函数。当集合中的元素为一个一维集合,他们之间的功能是没有差别的。

let flatNumbers = [1, Optional(2), 3, nil, Optional(5), nil]

let flatMapArr = latticeNumbers.flatMap { $0 }
// [1, 2, 3, 5]
let compactMapArr = optLatticeNumbers.compactMap { $0 }
// [1, 2, 3, 5]
1
2
3
4
5
6
6.filter函数
按照条件进行元素过滤。

let filterArr = numbers.filter { num in
return num < 3 || num > 8
}
// [10, 9, 1, 2]
1
2
3
4
7.reduce函数
以指定参数为基础,按照条件进行拼接

let reduceNumber = numbers.reduce(100) { result, num in
return result + num
}
// 155

let reduceString = ["C", "O", "D", "E"].reduce("word: ") { result, num in
return result + num
}
// "word: CODE"
1
2
3
4
5
6
7
8
9
8.prefix函数
正向取满足条件的元素,进行新集合创建。一旦出现不满足条件的元素,则跳出循环,不再执行。

let prefixArr = numbers.prefix { $0 < 10 }
// [7, 6]
1
2
prefix相关函数:
upTo: 正向取元素创建数组, 包含小于指定index的元素

let prefixUpToArr = numbers.prefix(upTo: 5)
// [7, 6, 10, 9, 8]
1
2
through: 正向取元素创建数组, 包含小于等于指定index的元素

let prefixThroughArr = numbers.prefix(through: 2)
// [7, 6, 10]
1
2
maxLength: 正向取元素创建数组, 包含指定的元素个数

let prefixMaxLengthArr = numbers.prefix(6)
// [7, 6, 10, 9, 8, 1]
1
2
9.drop函数
与prefix函数对应。正向跳过满足条件的元素,进行新集合创建。一旦出现不满足条件的元素,则跳出循环,不再执行。

let dropArr = numbers.drop { $0 < 10 }
// [10, 9, 8, 1, 2, 3, 4, 5]
1
2
drop相关函数:
dropFirst: 正向跳过元素创建数组, 跳过指定元素个数, 缺省值为1

let dropFirstArr = numbers.dropFirst(3)
// [7, 6, 10, 9, 8]
1
2
dropLast: 返向跳过元素创建数组, 跳过指定元素个数, 缺省值为1

let dropLastArr = numbers.dropLast(5)
// [7, 6, 10, 9, 8]
1
2
10.first函数
正向找出第一个满足条件的元素。

let first = numbers.first { $0 < 7 }
// 6
1
2
11.last函数
与first函数对应。反向找出第一个满足条件的元素。

let last = numbers.last { $0 > 5 }
// 8
1
2
12.firstIndex函数
正向找出第一个满足条件的元素下标。

let firstIndex = numbers.firstIndex { $0 < 7 }
// 1
1
2
13.lastIndex函数
反向找出第一个满足条件的元素下标。

let lastIndex = numbers.lastIndex { $0 > 5 }
// 4
1
2
14.partition函数
按照条件进行重新排序,不满足条件的元素在集合前半部分,满足条件的元素后半部分,但不是完整的升序或者降序排列。
返回值为排序完成后集合中第一个满足条件的元素下标。

var partitionNumbers = [20, 50, 30, 10, 40, 20, 60]
let pIndex = partitionNumbers.partition { $0 > 30 }
// partitionNumbers = [20, 20, 30, 10, 40, 50, 60]
// pIndex = 4
1
2
3
4
15.min函数
按条件排序后取最小元素。

let min = numbers.min { $0 % 5 < $1 % 5 }
// 10
1
2
min()函数,自然升序取最小。

let minDefault = numbers.min()
// 1
1
2
16.max函数
按条件排序后取最大元素。

let maxDictionary = ["aKey": 33, "bKey": 66, "cKey": 99]
let max = maxDictionary.max { $0.value < $1.value }
// (key "cKey", value 99)
1
2
3
max()函数,自然升序取最大。

let maxDefault = numbers.max()
// 10
1
2
17.removeAll函数
移除原集合中所有满足条件的元素。
无返回值,直接修改原集合,所以这个集合应该是可变类型的。

var removeArr = numbers
removeArr.removeAll { $0 > 6 }
// [6, 1, 2, 3, 4, 5]
1
2
3
18.集合遍历
forEach函数:

numbers.forEach { num in
print(num)
}
1
2
3
for-in函数:

for num in numbers where num < 5 {
print(num)
}
1
2
3
与enumerated()函数配合使用:

for (index, num) in numbers.enumerated() {
print("\(index)-\(num)")
}
1
2
3
关于集合遍历的性能问题,可以看这里enumerated() 和 enumerateObjectsUsingBlock。

19.shuffled函数
shuffled函数,打乱集合中元素的的顺序。

let ascendingNumbers = 0...9
let shuffledArr = ascendingNumbers.shuffled()
// [3, 9, 2, 6, 4, 5, 0, 1, 7, 8]
1
2
3
20.contains函数
contains函数,判断集合中是否包含某元素。

let containsBool = numbers.contains(8)
let containsBool1 = numbers.contains(11)
// true
// false
1
2
3
4
21.split和joined函数
split函数,字符串的函数,按条件分割字符串,为子字符串创建集合。与Objective-C中的componentsSeparatedByString:方法类似。

let line = "123Hi!123I'm123a123coder.123"
let splitArr = line.split { $0.isNumber }
// ["Hi!", "I'm", "a", "coder."]

// 也可指定字符
let splitArr2 = line.split(separator: "1")
// ["23Hi!", "23I'm", "23a", "23coder.", "23"]
1
2
3
4
5
6
7
joined函数,数组元素连接指定字符拼接成一个字符串。与Objective-C中的componentsJoinedByString:方法类似。

let joined = splitArr.joined(separator: "_")
// "Hi!_I'm_a_coder."

// 也可以只传入字符
let joined2 = splitArr2.joined(separator: "#")
// "23Hi!#23I'm#23a#23coder.#23"
1
2
3
4
5
6
22.zip函数
将两个数组合并为一个元组组成的数组。

let titles = ["aaa", "bbb", "ccc"]
let numbers = [111, 222, 333]
let zipA = zip(titles, numbers)
for (title, num) in zipA {
print("\(title)-\(num)")
}
1
2
3
4
5
6
打印结果:

aaa-111
bbb-222
ccc-333

原文链接:https://blog.csdn.net/wangyanchang21/article/details/89955249

收起阅读 »

iOS 八种经典排序算法

iOS
一、冒泡排序(Bubble Sort)冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会...
继续阅读 »

一、冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 稳定

1.2算法过程描述

  • <1>比较相邻的元素。如果第一个比第二个大,就交换它们两个;

  • <2>对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

  • <3>针对所有的元素重复以上的步骤,除了最后一个;

  • <4>重复步骤1~3,直到排序完成。

1.3代码实现

冒泡排序

1.4执行Log信息

冒泡排序Log1

冒泡排序Log2

二、选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n^2) 空间复杂度: O(1) 不稳定

2.2算法过程描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • <1>初始状态:无序区为R[1..n],有序区为空;

  • <2>第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

  • <3>n-1趟结束,数组有序化了。

2.3代码实现

选择排序

2.4执行Log信息

选择排序Log

三、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 稳定

3.2算法过程描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • <1>从第一个元素开始,该元素可以认为已经被排序;

  • <2>取出下一个元素,在已经排序的元素序列中从后向前扫描;

  • <3>如果该元素(已排序)大于新元素,将该元素移到下一位置;

  • <4>重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

  • <5>将新元素插入到该位置后;

  • <6>重复步骤2~5。

3.3代码实现

插入排序

3.4执行Log信息

插入排序Log

四、希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

4.1算法复杂度

时间平均复杂度:O(n^1.3) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 不稳定

4.2算法过程描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • <1>选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

  • <2>按增量序列个数k,对序列进行k 趟排序;

  • <3>每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.3代码实现

希尔排序

4.4执行Log信息

希尔排序Log

五、归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(nlog2^n) 最好复杂度: O(nlog2^n) 空间复杂度: O(n) 稳定

5.2算法过程描述

  • <1>把长度为n的输入序列分成两个长度为n/2的子序列;

  • <2>对这两个子序列分别采用归并排序;

  • <3>将两个排序好的子序列合并成一个最终的排序序列。

5.3代码实现

归并排序1

归并排序2

5.4执行Log信

归并排序Log

六、快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(n^2) 最好复杂度: O(nlog2^n) 空间复杂度: O(nlog2^n) 不稳定

6.2算法过程描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • <1>从数列中挑出一个元素,称为 “基准”(pivot);

  • <2>重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  • <3>递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.3代码实现

快速排序1

快速排序2

6.4执行Log信息

快速排序Log

七、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(nlog2^n) 最好复杂度: O(nlog2^n) 空间复杂度: O(1) 不稳定

7.2算法过程描述

  • <1>将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;

  • <2>将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];

  • <3>由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.3代码实现

堆排序1

堆排序2

7.4执行Log信息

堆排序Log1

堆排序Log2

八、计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1算法复杂度

时间平均复杂度:O(n+k) 最坏复杂度:O(n+k) 最好复杂度: O(n+k) 空间复杂度: O(n+k) 稳定

8.2算法过程描述

  • <1>找出待排序的数组中最大和最小的元素;

  • <2>统计数组中每个值为i的元素出现的次数,存入数组C的第i项;

  • <3>对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);

  • <4>反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

8.3代码实现

计数排序

8.4执行Log信息

计数排序Log

终于结束了,最后附两张快速排序和堆排序的动态展示图!!!!觉得不错的记得点个喜欢/关注哦!

附:

快速排序动态图

 堆排序动态图


链接 http://www.cocoachina.com/cms/wap.php?action=article&id=22988

收起阅读 »

iOS OC项目转Swift指南

iOS
运行环境:Xcode 11.1 Swift5.0最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。OC转Swift有一个大前提就是你要...
继续阅读 »

运行环境:Xcode 11.1 Swift5.0

最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。

OC转Swift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide

转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。

自动化工具

有一个比较好的自动化工具Swiftify,可以将OC文件甚至OC工程整个转成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感觉效果并不理想,因为没有使用过付费版,所以也不好评价它就是不好。

Swiftify还有一个Xcode的插件Swiftify for Xcode,可以实现对选中代码和单文件的转化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,需要手动再修改。

手动Swift化

桥接文件

如果你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个.h的桥接文件。如果不小心点了不添加还可以手动导入,就是自己手动生成一个.h文件,然后在Build Settings > Swift Compiler - General > Objective-C Bridging Header中填入该.h文件的路径。

image.png

这个桥接文件的作用就是供Swift代码引用OC代码,或者OC的三方库。

#import "Utility.h"
#import
复制代码

Bridging Header的下面还有一个配置项是Objective-C Generated Interface Header Name,对应的值是ProjectName-Swift.h。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分转成OC代码,OC部分的文件会类似pch一样全局引用这个头文件。因为是Build过程中生成的,所以只有.m文件中可以直接引用,对于在.h文件中的引用下文有介绍。

Appdelegate(程序入口)

Swift中没有main.m文件,取而代之的是@UIApplicationMain命令,该命令等效于原有的执行main.m。所以我们可以把main.m文件进行移除。

系统API

对于UIKit框架中的大部分代码转换可以直接查看系统API文档进行转换,这里就不过多介绍。

property(属性)

Swift没有property,也没有copynonatomic等属性修饰词,只有表示属性是否可变的letvar

注意点一 OC中一个类分.h.m两个文件,分别表示用于暴露给外接的方法,变量和仅供内部使用的方法变量。迁移到Swift时,应该将.m中的property标为private,即外接无法直接访问,对于.h中的property不做处理,取默认的internal,即同模块可访问。

对于函数的迁移也是相同的。

注意点二 有一种特殊情况是在OC项目中,某些属性在内部(.m)可变,外部(.h)只读。这种情况可以这么处理:

private(set) var value: String
复制代码

就是只对valueset方法就行private标记。

注意点三 Swift中针对空类型有个专门的符号?,对应OC中的nil。OC中没有这个符号,但是可以通过在nullablenonnull表示该种属性,方法参数或者返回值是否可以空。

如果OC中没有声明一个属性是否可以为空,那就去默认值nonnull

如果我们想让一个类的所有属性,函数返回值都是nonnull,除了手动一个个添加之外还有一个宏命令。

NS_ASSUME_NONNULL_BEGIN
/* code */
NS_ASSUME_NONNULL_END
复制代码

enum(枚举)

OC代码:

typedef NS_ENUM(NSInteger, PlayerState) {
PlayerStateNone = 0,
PlayerStatePlaying,
PlayerStatePause,
PlayerStateBuffer,
PlayerStateFailed,
};

typedef NS_OPTIONS(NSUInteger, XXViewAnimationOptions) {
XXViewAnimationOptionNone = 1 << 0,
XXViewAnimationOptionSelcted1 = 1 << 1,
XXViewAnimationOptionSelcted2 = 1 << 2,
}
复制代码

Swift代码

enum PlayerState: Int {
case none = 0
case playing
case pause
case buffer
case failed
}
struct ViewAnimationOptions: OptionSet {
let rawValue: UInt
static let None = ViewAnimationOptions(rawValue: 1<<0)
static let Selected1 = ViewAnimationOptions(rawValue: 1<<0)
static let Selected2 = ViewAnimationOptions(rawValue: 1 << 2)
//...
}
复制代码

Swift没有NS_OPTIONS的概念,取而代之的是为了满足OptionSet协议的struct类型。

懒加载

OC代码:

- (MTObject *)object {
if (!_object) {
_object = [MTObject new];
}
return _object;
}
复制代码

Swift代码:

lazy var object: MTObject = {
let object = MTObject()
return imagobjecteView
}()
复制代码

闭包

OC代码:

typedef void (^DownloadStateBlock)(BOOL isComplete);
复制代码

Swift代码:

typealias DownloadStateBlock = ((_ isComplete: Bool) -> Void)
复制代码

单例

OC代码:

+ (XXManager *)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
复制代码

Swift对单例的实现比较简单,有两种方式:

第一种

let shared = XXManager()// 声明在全局命名区(global namespace)
Class XXManager {
}
复制代码

你可能会疑惑,为什么没有dispatch_once,如何保证多线程下创建的唯一性?其实是这样的,Swift中全局变量是懒加载,在AppDelegate中被初始化,之后所有的调用都会使用该实例。而且全局变量的初始化是默认使用dispatch_once的,这保证了全局变量的构造器(initializer)只会被调用一次,保证了shard原子性

第二种

Class XXManager {
static let shared = XXManager()
private override init() {
// do something
}
}
复制代码

Swift 2 开始增加了static关键字,用于限定变量的作用域。如果不使用static,那么每一个shared都会对应一个实例。而使用static之后,shared成为全局变量,就成了跟上面第一种方式原理一致。可以注意到,由于构造器使用了 private 关键字,所以也保证了单例的原子性。

初始化方法和析构函数

对于初始化方法OC先调用父类的初始化方法,然后初始自己的成员变量。Swift先初始化自己的成员变量,然后在调用父类的初始化方法。

OC代码:

// 初始化方法
@interface MainView : UIView
@property (nonatomic, strong) NSString *title;
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title NS_DESIGNATED_INITIALIZER;
@end

@implementation MainView
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title {
if (self = [super initWithFrame:frame]) {
self.title = title;
}
return self;
}
@end
// 析构函数
- (void)dealloc {
//dealloc
}
复制代码

上面类在调用时

Swift代码:

class MainViewSwift: UIView {
let title: String
init(frame: CGRect, title: String) {
self.title = title
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
//deinit
}
}
复制代码

函数调用

OC代码:

// 实例函数(共有方法)
- (void)configModelWith:(XXModel *)model {}
// 实例函数(私有方法)
- (void)calculateProgress {}
// 类函数
+ (void)configModelWith:(XXModel *)model {}
复制代码
// 实例函数(共有方法)
func configModel(with model: XXModel) {}
// 实例函数(私有方法)
private func calculateProgress() {}
// 类函数(不可以被子类重写)
static func configModel(with model: XXModel) {}
// 类函数(可以被子类重写)
class func configModel(with model: XXModel) {}
// 类函数(不可以被子类重写)
class final func configModel(with model: XXModel) {}
复制代码

OC可以通过是否将方法声明在.h文件表明该方法是否为私有方法。Swift中没有了.h文件,对于方法的权限控制是通过权限关键词进行的,各关键词权限大小为: private < fileprivate < internal < public < open

其中internal为默认权限,可以在同一module下访问。

NSNotification(通知)

OC代码:

// add observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method) name:@"NotificationName" object:nil];
// post
[NSNotificationCenter.defaultCenter postNotificationName:@"NotificationName" object:nil];
复制代码

Swift代码:

// add observer
NotificationCenter.default.addObserver(self, selector: #selector(method), name: NSNotification.Name(rawValue: "NotificationName"), object: nil)
// post
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NotificationName"), object: self)
复制代码

可以注意到,Swift中通知中心NotificationCenter不带NS前缀,通知名由字符串变成了NSNotification.Name的结构体。

改成结构体的目的就是为了便于管理字符串,原本的字符串类型变成了指定的NSNotification.Name类型。上面的Swift代码可以修改为:

extension NSNotification.Name {
static let NotificationName = NSNotification.Name("NotificationName")
}
// add observer
NotificationCenter.default.addObserver(self, selector: #selector(method), name: .NotificationName, object: nil)
// post
NotificationCenter.default.post(name: .NotificationName, object: self)
复制代码

protocol(协议/代理)

OC代码:

@protocol XXManagerDelegate 
- (void)downloadFileFailed:(NSError *)error;
@optional
- (void)downloadFileComplete;
@end

@interface XXManager: NSObject
@property (nonatomic, weak) id delegate;
@end
复制代码

Swift中对protocol的使用拓宽了许多,不光是class对象,structenum也都可以实现协议。需要注意的是structenum为指引用类型,不能使用weak修饰。只有指定当前代理只支持类对象,才能使用weak。将上面的代码转成对应的Swift代码,就是:

@objc protocol XXManagerDelegate {
func downloadFailFailed(error: Error)
@objc optional func downloadFileComplete() // 可选协议的实现
}
class XXManager: NSObject {
weak var delegate: XXManagerDelegate?
}
复制代码

@objc是表明当前代码是针对NSObject对象,也就是class对象,就可以正常使用weak了。

如果不是针对NSObject对象的delegate,仅仅是普通的class对象可以这样设置代理:

protocol XXManagerDelegate: class {
func downloadFailFailed(error: Error)
}
class XXManager {
weak var delegate: XXManagerDelegate?
}
复制代码

值得注意的是,仅@objc标记的protocol可以使用@optional

Swift和OC混编注意事项

函数名的变化

如果你在一个Swift类里定义了一个delegate方法:

@objc protocol MarkButtonDelegate {
func clickBtn(title: String)
}
复制代码

如果你要在OC中实现这个协议,这时候方法名就变成了:

- (void)clickBtnWithTitle:(NSString *)title {
// code
}
复制代码

这主要是因为Swift有指定参数标签,OC却没有,所以在由Swift方法名生成OC方法名时编译器会自动加一些修饰词,已使函数作为一个句子可以"通顺"。

在OC的头文件里调用Swift类

如果要在OC的头文件里引用Swift类,因为Swift没有头文件,而为了让在头文件能够识别该Swift类,需要通过@class的方法引入。

@class SwiftClass;

@interface XXOCClass: NSObject
@property (nonatomic, strong) SwiftClass *object;
@end
复制代码

对OC类在Swift调用下重命名

因为Swift对不同的module都有命名空间,所以Swift类都不需要添加前缀。如果有一个带前缀的OC公共组件,在Swift环境下调用时不得不指定前缀是一件很不优雅的事情,所以苹果添加了一个宏命令NS_SWIFT_NAME,允许在OC类在Swift环境下的重命名:

NS_SWIFT_NAME(LoginManager)
@interface XXLoginManager: NSObject
@end
复制代码

这样我们就将XXLoginManager在Swift环境下的类名改为了LoginManager

引用类型和值类型

  • struct 和 enum 是值类型,类 class 是引用类型。
  • StringArray和 Dictionary都是结构体,因此赋值直接是拷贝,而NSStringNSArray 和NSDictionary则是类,所以是使用引用的方式。
  • struct 比 class 更“轻量级”,struct 分配在栈中,class 分配在堆中。

id类型和AnyObject

OC中id类型被Swift调用时会自动转成AnyObject,他们很相似,但却其实概念并不一致。Swift中还有一个概念是Any,他们三者的区别是:

  • id 是一种通用的对象类型,它可以指向属于任何类的对象,在OC中即是可以代表所有继承于NSObject的对象。
  • AnyObject可以代表任何class类型的实例。
  • Any可以代表任何类型,甚至包括func类型。

从范围大小比较就是:id < AnyObject < Any

其他语法区别及注意事项(待补充)

1、Swift语句中不需要加分号;

2、关于Bool类型更加严格,Swift不再是OC中的非0就是真,真假只对应truefalse

3、Swift类内一般不需要写self,但是闭包内是需要写的。

4、Swift是强类型语言,必须要指定明确的类型。在Swift中IntFloat是不能直接做运算的,必须要将他们转成同一类型才可以运算。

5、Swift抛弃了传统的++--运算,抛弃了传统的C语言式的for循环写法,而改为for-in

6、Swift的switch操作,不需要在每个case语句结束的时候都添加break

7、Swift对enum的使用做了很大的扩展,可以支持任意类型,而OC枚举仅支持Int类型,如果要写兼容代码,要选择Int型枚举。

8、Swift代码要想被OC调用,需要在属性和方法名前面加上@objc

9、Swift独有的特性,如泛型,struct,非Int型的enum等被包含才函数参数中,即使添加@objc也不会被编译器通过。

10、Swift支持重载,OC不支持。

11、带默认值的Swift函数再被OC调用时会自动展开。

语法检查

对于OC转Swift之后的语法变化还有很多细节值得注意,特别是对于初次使用Swift这门语言的同学,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,可以自动化的检查我们的代码是否符合Swift规范。

可以通过cocoapods进行引入,配置好之后,每次Build的过程,Lint脚本都会执行一遍Swift代码的语法检查操作,Lint还会将代码规范进行分级,严重的代码错误会直接报错,导致程序无法启动,不太严重的会显示代码警告(⚠️)。

如果你感觉SwiftLint有点过于严格了,还可以通过修改.swiftlint.yml文件,自定义属于自己的语法规范。

链接:https://juejin.im/post/5e5a4f20518825495a277aa7

收起阅读 »

iOS研发助手DoraemonKit技术实现(一)

一、前言一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于A...
继续阅读 »

一、前言

一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于App的信息进行快速的查看和修改。一键接入、使用方便,提高开发、测试、视觉同学的工作效率,提高我们App上线的完整度和稳定性。

目前DoraemonKit拥有的功能大概分为以下几点:

  1. 常用工具 : App信息展示,沙盒浏览、MockGPS、H5任意门、子线程UI检查、日志显示。
  2. 性能工具 : 帧率监控、CPU监控、内存监控、流量监控、自定义监控。
  3. 视觉工具 : 颜色吸管、组件检查、对齐标尺。
  4. 业务专区 : 支持业务测试组件接入到DoraemonKit面板中。

拿我们App接入效果如下:




面两行是业务线自定义的工具,接入方可以自定义。除此之外都是内置工具集合。

因为里面功能比较多,大概会分三篇文章介绍DoraemonKit的使用和技术实现,这是第一篇主要介绍常用工具集中的几款工具实现。

二、技术实现

2.1:App信息展示




我们要看一些手机信息或者App的一些基本信息的时候,需要到系统设置去找,比较麻烦。特别是权限信息,在我们app装的比较多的时候,我们很难快速找到我们app的权限信息。而这些信息从代码角度都是比较容易获取的。我们把我们感兴趣的信息列表出来直接查看,避免了去手机设置里查看或者查看源代码的麻烦。

获取手机型号

我们从手机设置里面是找不到我们的手机具体是哪一款的文字表述的,比如我的手机是iphone8 Pro,在手机型号里面显示的是MQ8E2CH/A。对于iPhone不熟悉的人很难从外表对iphone进行区分。而手机型号,我们从代码角度就很好获取。


+ (NSString *)iphoneType{
struct utsname systemInfo;
uname(&systemInfo);
NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];

//iPhone
if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 1G";
...
//其他对应关系请看下面对应表
return platform;
}

iPhone设备类型与通用手机类型一一对应关系表

设备类型通用类型
iPhone1,1iPhone 1G
iPhone1,2iPhone 3G
iPhone2,1iPhone 3GS
iPhone3,1iPhone 4
iPhone3,2iPhone 4
iPhone4,1iPhone 4S
iPhone5,1iPhone 5
iPhone5,2iPhone 5
iPhone5,3iPhone 5C
iPhone5,4iPhone 5C
iPhone6,1iPhone 5S
iPhone6,2iPhone 5S
iPhone7,1iPhone 6 Plus
iPhone7,2iPhone 6
iPhone8,1iPhone 6S
iPhone8,2iPhone 6S Plus
iPhone8,4iPhone SE
iPhone9,1iPhone 7
iPhone9,3iPhone 7
iPhone9,2iPhone 7 Plus
iPhone9,4iPhone 7 Plus
iPhone10,1iPhone 8
iPhone10.4iPhone 8
iPhone10,2iPhone 8 Plus
iPhone10,5iPhone 8 Plus
iPhone10,3iPhone X
iPhone10,6iPhone X
iPhone11,8iPhone XR
iPhone11,2iPhone XS
iPhone11,4iPhone XS Max
Phone11,6iPhone XS Max

获取手机系统版本

//获取手机系统版本
NSString *phoneVersion = [[UIDevice currentDevice] systemVersion];

获取App BundleId

一个app分为测试版本、企业版本、appStore发售版本,每一个app长得都一样,如何对他们进行区分呢,那就要用到BundleId这个属性了。


//获取bundle id
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];

获取App 版本号


//获取App版本号
NSString *bundleVersionCode = [[[NSBundle mainBundle]infoDictionary] objectForKey:@"CFBundleVersion"];

权限信息查看


当我们发现App运行不正常,比如无法定位,网络一直失败,无法收到推送信息等问题的时候,我们第一个反应就是去手机设置里面去看我们app相关的权限有没有打开。DoraemonKit集成了对于地理位置权限、网络权限、推送权限、相机权限、麦克风权限、相册权限、通讯录权限、日历权限、提醒事项权限的查询。

由于代码比较多,这里就不一一贴出来了。大家可以去DorameonKit/Core/Plugin/AppInfo中自己去查看。这里讲一下,权限查询结果几个值的意义。

  • NotDetermined => 用户还没有选择。
  • Restricted => 该权限受限,比如家长控制。
  • Denied => 用户拒绝使用该权限。
  • Authorized => 用户同意使用该权限。

2.2:沙盒浏览




以前如果我们要去查看App缓存、日志信息,都需要访问沙盒。由于iOS的封闭性,我们无法直接查看沙盒中的文件内容。如果我们要去访问沙盒,基本上有两种方式,第一种使用Xcode自带的工具,从Windows-->Devices进入设备管理界面,通过Download Container的方式导出整个app的沙盒。第二种方式,就是自己写代码,访问沙盒中指定文件,然后使用NSLog的方式打印出来。这两种方式都比较麻烦。

DoraemonKit给出的解决方案:就是自己做一个简单的文件浏览器,通过NSFileManager对象对沙盒文件进行遍历,同时支持对于文件和文件夹的删除操作。对于文件支持本地预览或者通过airdrop的方式或者其他分享方式发送到PC端进行更加细致的操作。

怎么用NSFileManager对象遍历文件和删除文件这里就不说了,大家可以参考DorameonKit/Core/Plugin/Sanbox中的代码。这里讲一下:如何将手机中的文件快速上传到Mac端?刚开始我们还绕了一点路,我们在手机端搭了一个微服务,mac通过浏览器去访问它。后来和同事聊天的时候知道了UIActivityViewController这个类,可以十分便捷地吊起系统分享组件或者是其他注册到系统分享组件中的分享方式,比如微信、钉钉。实现代码非常简单,如下所示:


- (void)shareFileWithPath:(NSString *)filePath{

NSURL *url = [NSURL fileURLWithPath:filePath];
NSArray *objectsToShare = @[url];

UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil];
NSArray *excludedActivities = @[UIActivityTypePostToTwitter, UIActivityTypePostToFacebook,
UIActivityTypePostToWeibo,
UIActivityTypeMessage, UIActivityTypeMail,
UIActivityTypePrint, UIActivityTypeCopyToPasteboard,
UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll,
UIActivityTypeAddToReadingList, UIActivityTypePostToFlickr,
UIActivityTypePostToVimeo, UIActivityTypePostToTencentWeibo];
controller.excludedActivityTypes = excludedActivities;

[self presentViewController:controller animated:YES completion:nil];
}

2.3:MockGPS




我们有些业务会根据地理位置不同,而有不同的业务处理逻辑。而我们开发或者测试,当然不可能去每一个地址都测试一遍。这种情况下,测试同学一般会找到我们让我们手动改掉系统获取经纬度的回调,或者修改GPX文件,然后再重新打一个包。这样也非常麻烦。

DoraemonKit给出的解决方案:提供一套地图界面,支持在地图中滑动选择或者手动输入经纬度,然后自动替换掉我们App中返回的当前经纬度信息。这里的难点是如何不需要重新打包自动替换掉系统返回的当前经纬度信息?

CLLocationManager的delegate中有一个方法如下:

/*
* locationManager:didUpdateLocations:
*
* Discussion:
* Invoked when new locations are available. Required for delivery of
* deferred locations. If implemented, updates will
* not be delivered to locationManager:didUpdateToLocation:fromLocation:
*
* locations is an array of CLLocation objects in chronological order.
*/

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations API_AVAILABLE(ios(6.0), macos(10.9));

我们通常是在这个函数中获取当前系统的经纬度信息。我们如果想要没有侵入式的修改这个函数的默认实现方式,想到的第一个方法就是Method Swizzling。但是真正在实现过程中,你会发现Method Swizzling需要当前实例和方法,方法是- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations 我们有了,但是实例,每一个app都有自己的实现,无法做到统一处理。我们就换了一个思路,如何能获取该实现了该定位方法的实例呢?就是使用Method Swizzling Hook住CLLocationManager的setDelegate方法,就能获取具体是哪一个实例实现了- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations 方法。

具体方法如下:

第一步: 生成一个CLLocationManager的分类CLLocationManager(Doraemon),在这个分类中,实现- (void)doraemon_swizzleLocationDelegate:(id)delegate这个方法,用来进行方法交换。

- (void)doraemon_swizzleLocationDelegate:(id)delegate {
if (delegate) {
//1、让所有的CLLocationManager的代理都设置为[DoraemonGPSMocker shareInstance],让他做中间转发
[self doraemon_swizzleLocationDelegate:[DoraemonGPSMocker shareInstance]];
//2、绑定所有CLLocationManager实例与delegate的关系,用于[DoraemonGPSMocker shareInstance]做目标转发用。
[[DoraemonGPSMocker shareInstance] addLocationBinder:self delegate:delegate];

//3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。
Protocol *proto = objc_getProtocol("CLLocationManagerDelegate");
unsigned int count;
struct objc_method_description *methods = protocol_copyMethodDescriptionList(proto, NO, YES, &count);
NSMutableArray *array = [NSMutableArray array];
for(unsigned i = 0; i < count; i++)
{
SEL sel = methods[i].name;
if ([delegate respondsToSelector:sel]) {
if (![[DoraemonGPSMocker shareInstance] respondsToSelector:sel]) {
NSAssert(NO, @"你在Delegate %@ 中所使用的SEL %@,暂不支持,请联系DoraemonKit开发者",delegate,sel);
}
}
}
free(methods);

}else{
[self doraemon_swizzleLocationDelegate:delegate];
}
}


在这个函数中主要做了三件事情,1、将所有的定位回调统一交给[DoraemonGPSMocker shareInstance]处理 2、[DoraemonGPSMocker shareInstance]绑定了所有CLLocationManager与它的delegate的一一对应关系。3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。

第二步:当有一个定位回调过来的时候,我们先传给[DoraemonGPSMocker shareInstance],然后[DoraemonGPSMocker shareInstance]再转发给它绑定过的所有的delegate。那我们App为例,绑定关系如下:


{
"0x2800a07a0_binder" = "";
"0x2800a07a0_delegate" = "";
"0x2800b59a0_binder" = "";
"0x2800b59a0_delegate" = "";
}

由此可见,我们App的统一定位KDDriverLocationManager和苹果地图的定位MAMapLocationManager都是使用都是CLLocationManager提供的。

具体 DoraemonGPSMocker这个类如何实现,请参考DorameonKit/Core/Plugin/GPS中的代码。


2.4:H5任意门





有的时候Native和H5开发同时开发一个功能,H5依赖native提供入口,而这个时候Native还没有开发好,这个时候H5开发就没法在App上看到效果。再比如,有些H5页面处于的位置比较深入,就像我们代驾司机端,做单流程比较多,有的H5界面需要很繁琐的操作才能展示到App上,不方便我们查看和定位问题。
这个时候我们可以为app做一个简单的浏览器,输入url,使用自带的容器进行跳转。因为每一个app的H5容器基本上都是自定义过得,都会有自己的bridge定制化,所以这个H5容器没有办法使用系统原生的UIWebView或者WKWebView,就只能交给业务方自己去完成。我们在DorameonKit初始化的时候,提供了一个回调让业务方用自己的H5容器去打开这个Url:

[[DoraemonManager shareInstance] addH5DoorBlock:^(NSString *h5Url) {
//使用自己的H5容器打开这个链接
}];

这个工具实现比较简单,就不多说了,代码路径在DorameonKit/Core/Plugin/H5.


2.5:子线程UI检查






在iOS中是不允许在子线程中对UI进行操作和渲染的,不然会造成未知的错误和问题,甚至会导致crash。我们在最近几个版本中发现新增了一些crash,调查原因就是在子线程中操作UI导致的。为了对于这种情况可以提早被我们发现,我在在DorameonKit中增加了子线程UI渲染检查查询。

具体事项思路,我们hook住UIView的三个必须在主线程中操作的绘制方法。1、setNeedsLayout 2、setNeedsDisplay 3、setNeedsDisplayInRect:。然后判断他们是不是在子线程中进行操作,如果是在子线程进行操作的话,打印出当前代码调用堆栈,提供给开发进行解决。具体代码如下:

@implementation UIView (Doraemon)

+ (void)load{
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsLayout) swizzledSel:@selector(doraemon_setNeedsLayout)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplay) swizzledSel:@selector(doraemon_setNeedsDisplay)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplayInRect:) swizzledSel:@selector(doraemon_setNeedsDisplayInRect:)];
}

- (void)doraemon_setNeedsLayout{
[self doraemon_setNeedsLayout];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplay{
[self doraemon_setNeedsDisplay];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplayInRect:(CGRect)rect{
[self doraemon_setNeedsDisplayInRect:rect];
[self uiCheck];
}

- (void)uiCheck{
if([[DoraemonCacheManager sharedInstance] subThreadUICheckSwitch]){
if(![NSThread isMainThread]){
NSString *report = [BSBacktraceLogger bs_backtraceOfCurrentThread];
NSDictionary *dic = @{
@"title":[DoraemonUtil dateFormatNow],
@"content":report
};
[[DoraemonSubThreadUICheckManager sharedInstance].checkArray addObject:dic];
}
}
}

@end

完整代码实现请参考DorameonKit/Core/Plugin/SubThreadUICheck


2.6:日志显示



这个主要是方便我们查看本地日志,以前我们如果要查看日志,需要自己写代码,访问沙盒导出日志文件,然后再查看。也是比较麻烦的。

DoraemonKit的解决方案是:我们每一次触发日志的时候,都把日志内容显示到界面上,方便我们查看。
如何实现的呢?因为我们这个工具并不是一个通用性的工具,只针对于底层日志库是CocoaLumberjack的情况。稍微讲一下的CocoaLumberjack原理,所有的log都会发给DDLog对象,其运行在自己的一个GCD队列中,之后,DDLog会将log分发给其下注册的一个或者多个Logger中,这一步在多核下面是并发的,效率很高。每一个Logger处理收到的log也是在它们自己的GCD队列下做的,它们询问其下的Formatter,获取Log消息格式,然后根据Logger的逻辑,将log消息分发到不同的地方。系统自带三个Logger处理器,DDTTYLogger,主要将日志发送到Xcode控制台;DDASLLogger,主要讲日志发送到苹果的日志系统Console.app; DDFileLogger,主要将日志发送到文件中保存起来,也是我们开发用到最多的。但是自带的Logger并不满足我们的需求,我们的需求是将日志显示到UI界面中,所以我们需要新建一个类DoraemonLogger,继承于DDAbstractLogger,然后重写logMessage方法,将每一条传过来的日志打印到UI界面中。





这个工具参考LumberjackConsole这个开源项目完成,因为刚出iOS11的时候,作者没有适配,所以我们自己拷贝一份代码出来,自己维护了。 完整代码实现请参考DorameonKit/WithLogger中.




作者:景铭巴巴
链接:https://www.jianshu.com/p/00763123dbc4







收起阅读 »

iOS 界面优化

卡顿原因计算机通过CPU、GPU、显示器三者协同工作将试图显示到屏幕上1、CPU将需要显示的内容计算出来,提交到GPU2、GPU将内容渲染完成后将渲染后的内容存放到FrameBuffer(帧缓冲区)3、视频控制器根据VSync(垂直同步)信号来读取FrameB...
继续阅读 »

卡顿原因

计算机通过CPUGPU显示器三者协同工作将试图显示到屏幕上

  • 1、CPU将需要显示的内容计算出来,提交到GPU
  • 2、GPU将内容渲染完成后将渲染后的内容存放到FrameBuffer(帧缓冲区)
  • 3、视频控制器根据VSync(垂直同步)信号来读取FrameBuffer中的数据
  • 4、将转换的数模传递给显示器显示


iOS设备中采用双缓存区+VSync

在收到VSync信号后,系统的图形服务通过CADisplayLink等机制通知App,在主程序中调度CPU计算显示的内容,随后将计算好的内容提交到GPU变换、合成、渲染,GPU将渲染结果提交帧缓冲区,等待下一个VSync信号到来时显示到屏幕上。由于垂直同步机制的原因,如果再一个VSync时间内,CPU或者GPU没有完成内容的处理,就会导致当前处理的帧丢弃,此时屏幕会保持上一帧的显示,造成掉帧


卡顿检测

  • FPS监控:因为iOS设备屏幕的刷新时间是60次/秒,一次刷新就是一次VSync信号,时间间隔是1000ms/60 = 16.67ms,所有如果咋16.67ms内下一帧数据没有准备好,就会产生掉帧
  • RunLoop监控:通过子线程检测主线程的RunLoop的状态,kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个状态之间的耗时是否达到一定的阈值

FPS监控

参照YYKit中的YYFPSLabel,其中通过CADisplayLink来实现,通过刷新次数/时间差得到刷新频率


class YPFPSLabel: UILabel {

fileprivate var link: CADisplayLink = {
let link = CADisplayLink.init()
return link
}()

fileprivate var count: Int = 0
fileprivate var lastTime: TimeInterval = 0.0
fileprivate var fpsColor: UIColor = {
return UIColor.green
}()
fileprivate var fps: Double = 0.0

override init(frame: CGRect) {
var f = frame
if f.size == CGSize.zero {
f.size = CGSize(width: 80.0, height: 22.0)
}

super.init(frame: f)

self.textColor = UIColor.white
self.textAlignment = .center
self.font = UIFont.init(name: "Menlo", size: 12)
self.backgroundColor = UIColor.lightGray
//通过虚拟类
link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

deinit {
link.invalidate()
}

@objc func tick(_ link: CADisplayLink){
guard lastTime != 0 else {
lastTime = link.timestamp
return
}

count += 1
//时间差
let detla = link.timestamp - lastTime
guard detla >= 1.0 else {
return
}

lastTime = link.timestamp
//刷新次数 / 时间差 = 刷新频次
fps = Double(count) / detla
let fpsText = "\(String.init(format: "%.2f", fps)) FPS"
count = 0

let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
if fps > 55.0 {
//流畅
fpsColor = UIColor.green
}else if (fps >= 50.0 && fps <= 55.0){
//一般
fpsColor = UIColor.yellow
}else{
//卡顿
fpsColor = UIColor.red
}

attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))

DispatchQueue.main.async {
self.attributedText = attrMStr
}
}

}

RunLoop监控

开辟子线程,通过监听主线程的kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个Activity之间的差值


#import "YPBlockMonitor.h"

@interface YPBlockMonitor (){
CFRunLoopActivity activity;
}

@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;

@end

@implementation YPBlockMonitor

+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}

- (void)start{
[self registerObserver];
[self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
// 创建信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (st != 0)
{
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
NSLog(@"检测到超过两次连续卡顿");
}
}
self->_timeoutCount = 0;
}
});
}

@end

界面优化

UIView和CALayer的关系

  • UIView是基于UIKit框架,继承自UIResponder,可以处理事件,管理子视图
  • CALayer是基于CoreAnimation的,继承自NSObject,只负责显示,不能处理事件
  • UIKit组件最终都会分解为layer,存储到图层树
  • UIView中的部分属性,frame、bounds、transform等,来自CALayer的映射
  • CALayer内部没有属性,在调用属性时,内部通过运行时resolveInstanceMethod方法为对象临时添加一个方法,并将对应属性值保存到内部的Dictionary,同时通知delegate、创建动画等

CPU层面的优化

  • 1、对于不需要触摸的控件使用CALayer代替UIView

  • 2、减少UIViewCALayer的属性修改

  • 3、大量对象释放时,移动到后台线程释放

  • 4、预排版:在异步子线程中提前计算好视图的大小

  • 5、Autolayout在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的。所以尽量使用代码布局

  • 6、文本处理

    • 对于文本没有特殊要求的,可以使用UILabel内部实现方法,放在子线程中执行
      • 计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
      • 文本绘制:[NSAttributedString drawWithRect:options:context:]
    • 使用自定义文本控件,通过TextKit或者CoreText进行异步文本绘制。CoreText对象创建后,可以直接获取文本宽高等信息。CoreText直接使用了CoreGraphics占用内存小,效率高
  • 7、图片优化
    在使用UIImage或者CGImageSource方法创建图片时,图片数据不会立即解码,而是在设置到UIImageView/CALayer.contents中,然后由CALayer提交到GPU渲染前才在主线程进行解码,可以参考SDWebImage中对图片的处理,在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap直接创建图片

    • 使用PNG图片,而非JPGE图片
    • 子线程中解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
    • 优化图片大小,避免动态缩放
    • 多图合成一张图片显示
  • 8、避免使用透明View,会导致GPU在计算像素时,会将下层图层的像素也计算进来,颜色混合处理

  • 9、按需加载:例如通过RunLoop分发任务,ScrollView滚动时不加载

  • 10、少使用addView 给cell动态添加view

GPU层面优化

GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上
1、避免短时间显示大量图片,可以将多张图片合成一张
2、控制图片尺寸不超过4096x4096,因为图片超过这个尺寸,CPU会先进行预处理再提交给GPU
3、减少视图层级和数量
4、避免离屏渲染
5、异步渲染,例如可以将cell中的所有控件、视图合成一张位图进行显示



作者:木扬音
链接:https://www.jianshu.com/p/2f9a06932879

收起阅读 »

iOS开发制作同时支持armv7,armv7s,arm64,i386,x86_64的静态库.a

iOS
一、概要平时项目开发中,可能使用第三方提供的静态库.a,如果.a提供方技术不成熟,使用的时候就会出现问题,例如:在真机上编译报错:No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arc...
继续阅读 »

一、概要

平时项目开发中,可能使用第三方提供的静态库.a,如果.a提供方技术不成熟,使用的时候就会出现问题,例如:

在真机上编译报错:No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=x86_64, VALID_ARCHS=i386).

在模拟器上编译报错:No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=armv7s, VALID_ARCHS=armv7 armv6).

要解决以上问题,就要了解一下Apple移动设备处理器指令集相关的一些细节知识。

 

二、几个重要概念

1、ARM

ARM处理器,特点是体积小、低功耗、低成本、高性能,所以几乎所有手机处理器都基于ARM,在嵌入式系统中应用广泛。

 

2、ARM处理器指令集

armv6|armv7|armv7s|arm64都是ARM处理器的指令集,这些指令集都是向下兼容的,例如armv7指令集兼容armv6,只是使用armv6的时候无法发挥出其性能,无法使用armv7的新特性,从而会导致程序执行效率没那么高。

还有两个我们也很熟悉的指令集:i386|x86_64 是Mac处理器的指令集,i386是针对intel通用微处理器32架构的。x86_64是针对x86架构的64位处理器。所以当使用iOS模拟器的时候会遇到i386|x86_64,iOS模拟器没有arm指令集。

 

3、目前iOS移动设备指令集

arm64:iPhone5S| iPad Air| iPad mini2(iPad mini with Retina Display)

armv7s:iPhone5|iPhone5C|iPad4(iPad with Retina Display)

armv7:iPhone3GS|iPhone4|iPhone4S|iPad|iPad2|iPad3(The New iPad)|iPad mini|iPod Touch 3G|iPod Touch4

armv6 设备: iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch(一般不需要去支持)

 

4、Xcode中指令集相关选项(Build Setting中)

(1)Architectures

Space-separated list of identifiers. Specifies the architectures (ABIs, processor models) to which the binary is targeted. When this build setting specifies more than one architecture, the generated binary may contain object code for each of the specified
architectures.

指定工程被编译成可支持哪些指令集类型,而支持的指令集越多,就会编译出包含多个指令集代码的数据包,对应生成二进制包就越大,也就是ipa包会变大。

(2)Valid Architectures

Space-separated list of identifiers. Specifies the architectures for which the binary may be built. During the build, this list is intersected with the value of ARCHS build setting; the resulting list specifies the architectures the binary can run on. If
the resulting architecture list is empty, the target generates no binary.

限制可能被支持的指令集的范围,也就是Xcode编译出来的二进制包类型最终从这些类型产生,而编译出哪种指令集的包,将由Architectures与Valid Architectures(因此这个不能为空)的交集来确定,例如:
比如,你的Valid Architectures设置的支持arm指令集版本有:armv7/armv7s/arm64,对应的Architectures设置的支持arm指令集版本有:armv7s,这时Xcode只会生成一个armv7s指令集的二进制包。

再比如:将Architectures支持arm指令集设置为:armv7,armv7s,对应的Valid Architectures的支持的指令集设置为:armv7s,arm64,那么此时,XCode生成二进制包所支持的指令集只有armv7s

 

在Xcode6.1.1里的 Valid Architectures  设置里, 默认为 Standard architectures(armv7,arm64),如果你想改的话,自己在other中更改。

原因解释如下:
使用 standard architectures (including 64-bit)(armv7,arm64) 参数,则打的包里面有32位、64位两份代码,在iPhone5s( iPhone5s的cpu是64位的 )下,会首选运行64位代码包, 其余的iPhone( 其余iPhone都是32位的,iPhone5c也是32位 ),只能运行32位包,但是包含两种架构的代码包,只有运行在ios6,ios7系统上。
这也就是说,这种打包方式,对手机几乎没要求,但是对系统有要求,即ios6以上。
而使用 standard architectures (armv7,armv7s) 参数, 则打的包里只有32位代码, iPhone5s的cpu是64位,但是可以兼容32位代码,即可以运行32位代码。但是这会降低iPhone5s的性能。 其余的iPhone对32位代码包更没问题, 而32位代码包,对系统也几乎也没什么限制。
所以总结如下:

要发挥iPhone5s的64位性能,就要包含64位包,那么系统最低要求为ios6。 如果要兼容ios5以及更低的系统,只能打32位的包,系统都能通用,但是会丧失iPhone5s的性能。

(3)Build Active Architecture Only

指定是否只对当前连接设备所支持的指令集编译

当其值设置为YES,这个属性设置为yes,是为了debug的时候编译速度更快,它只编译当前的architecture版本,而设置为no时,会编译所有的版本。 编译出的版本是向下兼容的,连接的设备的指令集匹配是由高到低(arm64 > armv7s > armv7)依次匹配的。比如你设置此值为yes,用iphone4编译出来的是armv7版本的,iphone5也可以运行,但是armv6的设备就不能运行。  所以,一般debug的时候可以选择设置为yes,release的时候要改为no,以适应不同设备。

1)

Architectures:  armv7, armv7s, arm64
ValidArchitectures:  armv6, armv7s, arm64
生成二进制包支持的指令集: arm64

2)

Architectures: armv6, armv7, armv7s
Valid Architectures:  armv6, armv7s, arm64
生成二进制包支持的指令集: armv7s

3)

Architectures: armv7, armv7s, arm64
Valid Architectures: armv7,armv7s

这种情况是报错的,因为允许使用指令集中没有arm64。

注:如果你对ipa安装包大小有要求,可以减少安装包的指令集的数量,这样就可以尽可能的减少包的大小。当然这样做会使部分设备出现性能损失,当然在普通应用中这点体现几乎感觉不到,至少不会威胁到用户体检。

 

三、制作静态库.a是指令集选择

现在回归到正题,如何制作一个“没有问题”的.a静态库,通过以上信息了解到,当我们做App的时候,为了追求高效率,并且减小包的大小,Build Active Architecture Only设置成YES,Architectures按Xcode默认配置就可以,因为arm64向前兼容。但制作.a静态库就不同了,因为要保证兼容性,包括不同iOS设备以及模拟器运行不出错,所以结合当前行业情况,要做到最大的兼容性。

ValidArchitectures设置为:armv7|armv7s|arm64|i386|x86_64

Architectures设置不变(或根据你需要):  armv7|arm64

然后分别选择iOS设备和模拟器进行编译,最后找到相关的.a进行合包,使用lipo -create 真机库.a的路径 模拟器库.a的的路径 -output 合成库的名字.a(详情可以参考http://blog.csdn.net/lizhongfu2013/article/details/12648633)

这样就制作了一个通用的静态库.a

链接:https://www.jishudog.com/30423/html

收起阅读 »

iOS 界面渲染流程分析

iOS
前言本文阅读建议 1.一定要辩证的看待本文. 2.本文所表达观点并不是最终观点,还会更新,因为本人还在学习过程中,有什么遗漏或错误还望各位指出. 3.觉得哪里不妥请在评论留下建议~ 4.觉得还行的话就点个小心心鼓励下我吧~ 在最近的面试中,我发现一道面试题,其...
继续阅读 »

前言

本文阅读建议
1.一定要辩证的看待本文.
2.本文所表达观点并不是最终观点,还会更新,因为本人还在学习过程中,有什么遗漏或错误还望各位指出.
3.觉得哪里不妥请在评论留下建议~
4.觉得还行的话就点个小心心鼓励下我吧~

在最近的面试中,我发现一道面试题,其考点是:围绕iOS App中一个视图从添加到完全渲染,在这个过程中,iOS系统都做了什么?

在进行了大量的文章查阅以及学习以后,将所有较为可靠的资料总结一下供大家参考。


面试题

本文可为以下面试题提供参考:

  1. app从点击屏幕(硬件)到完全渲染,中间发生了什么?越详细越好 要求讲到进程间通信?出处
  2. 一个UIImageView添加到视图上以后,内部是如何渲染到手机上的,请简述其流程?
  3. 在一个表内有很多cell,每个cell上有很多个视图,如何解决卡顿问题?
  4. UIView与CALayer的区别?

简答

iOS渲染视图的核心是Core Animation
其渲染层次依次为:图层树->呈现树->渲染树

  1. CPU阶段
    1. 布局(Frame)
    2. 显示(Core Graphics)
    3. 准备(QuartzCore/Core Animation)
    4. 通过IPC提交(打包好的图层树以及动画属性)
  2. OpenGL ES阶段
    1. 生成(Generate)
    2. 绑定(Bind)
    3. 缓存数据(Buffer Data)
    4. 启用(Enable)
    5. 设置指针(Set Pointers)
    6. 绘图(Draw)
    7. 清除(Delete)
  3. GPU阶段
    1. 接收提交的纹理(Texture)和顶点描述(三角形)
    2. 应用变换(transform)
    3. 合并渲染(离屏渲染等)

其iOS平台渲染核心原理的重点主要围绕前后帧缓存、Vsync信号、CADisplayLink

文字简答:

  1. 首先一个视图由CPU进行Frame布局,准备视图和图层的层级关系,查询是否有重写drawRect:drawLayer:inContext:方法,注意:如果有重写的话,这里的渲染是会占用CPU进行处理的
  2. CPU会将处理视图和图层的层级关系打包,通过IPC(内部处理通信)通道提交给渲染服务,渲染服务由OpenGL ES和GPU组成。
  3. 渲染服务首先将图层数据交给OpenGL ES进行纹理生成和着色。生成前后帧缓存,再根据显示硬件的刷新频率,一般以设备的VSync信号CADisplayLink为标准,进行前后帧缓存的切换。
  4. 最后,将最终要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换,应用纹理和混合。最终显示在屏幕上。

以上仅仅是对该题简单回答,其中的原理以及瓶颈和优化,后面会详细介绍。


知识点

  1. 重新认识Core Animation
  2. CPU渲染职能
  3. OpenGL ES渲染职能
  4. GPU渲染职能
  5. IPC内部通信(进程间通信)
  6. 前后帧缓存&Vsync信号
  7. 视图渲染优化&卡顿优化
  8. Metal渲染引擎
  9. 事件响应链&Runloop原理
  10. CALayer的职能

重新认识Core Animation

苹果官方文档-Core Animation
Core Animation并仅仅是字面意思的核心动画,而是整个显示核心都是围绕QuartzCore框架中的Core Animation

Core Animation是依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染,但在本文中,以及官方文档都是将OpenGL与GPU分开说明。

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

CPU渲染职能

在这里推荐大家去阅读落影loyinglin的文章iOS开发-视图渲染与性能优化

  • 显示逻辑
    • CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;
    • RenderServer解析提交的子树状态,生成绘制指令
    • GPU执行绘制指令
    • 显示渲染后的数据
  • 提交流程
    • 布局(Layout)
      • 调用layoutSubviews方法
      • 调用addSubview:方法
    • 显示(Display)
      • 通过drawRect绘制视图;
      • 绘制string(字符串);
    • 准备提交(Prepare)
      • 解码图片;
      • 图片格式转换;
    • 提交(Commit)
      • 打包layers并发送到渲染server;
      • 递归提交子树的layers;
      • 如果子树太复杂,会消耗很大,对性能造成影响;

CPU渲染职能主要体现在以下5个方面:

布局计算
如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤为明显,它应该是比老版的自动调整逻辑加强了CPU的工作。

视图懒加载
iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图 从一个nib文件中加载,或者涉及IO的图片显示,都会比CPU正常操作慢得多。

Core Graphics绘制
如果对视图实现了drawRect:drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在绘制任何东 西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。

解压图片
PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对 图层内容赋值的时候(直接或者间接使用 UIImageView )或者把它绘制到 Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。

图层打包
当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示 屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环 转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须 要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层 级关系中有太多的图层,就会导致CPU没一帧的渲染,即使这些事情不是你的应用 程序可控的。

OpenGL ES渲染职能

这里推荐大家去看《OpenGL ES应用开发实践指南:iOS卷》,因为篇幅过长,就不赘述OpenGL的原理。

简单来说,OpenGL ES是对图层进行取色,采样,生成纹理,绑定数据,生成前后帧缓存。

纹理的概念:纹理是一个用来保存图像的颜色元􏰈值的 OpenGL ES 缓存,可以简单理解为一个单位。

1)生成(Generate)— 请 OpenGL ES 为图形处理器制的缓存生成一个独一无二的标识符。 
2)绑定(Bind)— 告诉 OpenGL ES 为接下来的运算使用一个缓存。
3)缓存数据(Buffer Data)— 让 OpenGL ES 为当前定的缓存分配并初始化 够的内存(通常是从 CPU 制的内存复制数据到分配的内存)。
4)启用(Enable)或者(Disable)— 告诉 OpenGL ES 在接下来的渲染中是 使用缓存中的数据。
5)设置指(Set Pointers)— 告诉 Open-GL ES 在缓存中的数据的类型和所有需 要的数据的内存移值。
6)绘图(Draw) — 告诉 OpenGL ES 使用当前定并启用的缓存中的数据渲染 整个场景或者某个场景的一部分。
7)删除除(Delete)— 告诉 OpenGL ES 除以前生成的缓存并释相关的资源。

当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写-drawInContext方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext中绘制的东西放入到纹理的位图数据中。

iOS 操作系统不会让应用直接向前帧缓存或者 后帧缓存绘图,也不会让应用直接复制前帧缓存和后帧缓存之间的切换。操作系统为自 己保留了这些操作,以便它可以随时使用 Core Animation 合成器来控制显示的最终外观

最终,生成前后帧缓存会再交由GPU进行最后一步的工作。

GPU渲染职能

GPU会根据生成的前后帧缓存数据,根据实际情况进行合成,其中造成GPU渲染负担的一般是:离屏渲染,图层混合,延迟加载。

  • 普通的Tile-Based渲染流程
    • CommandBuffer,接受OpenGL ES处理完毕的渲染指令;
    • Tiler,调用顶点着色器,把顶点数据进行分块(Tiling);
    • ParameterBuffer,接受分块完毕的tile和对应的渲染参数;
    • Renderer,调用片元着色器,进行像素渲染;
      -RenderBuffer,存储渲染完毕的像素;
  • 离屏渲染 —— 遮罩(Mask)
    • 渲染layer的mask纹理,同Tile-Based的基本渲染逻辑;
    • 渲染layer的content纹理,同Tile-Based的基本渲染逻辑;
    • Compositing操作,合并1、2的纹理;
  • 离屏渲染 ——UIVisiualEffectView
  • 渲染等待
  • 光栅化
  • 组透明度

GPU用来采集图片和形状,运行变换,应用文理和混合,最终把它们输送到屏幕上。

太多的几何结构会影响GPU速度,但这并不是GPU的瓶颈限制原因,但由于图层在显示之前要通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。

重绘。主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以要避免重绘。


IPC内部通信(进程间通信)

在研究这个问题的过程中,我有想过去看一下源码,试着去理解在视图完全渲染之前,IPC是如何调度的,可惜苹果并没有开源绘制过程中的代码。这里推荐官方文章给大家了解一下iOS中IPC是如何运作的。

苹果官方文档-Mach内核编程 IPC通信

前后帧缓存&Vsync信号

虽然我们不能看到苹果内部是如何实现的,但是苹果官方也提供了我们可以参考的对象,也就是VSync信号CADisplayLink对象。

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

帧缓存:接收渲染结果的缓冲区,为GPU指定存储渲染结果的区域

帧缓存可以同时存在多个,但是屏幕显示像素受到保存在前帧缓存(front frame buffer)的特定帧缓存中的像素颜色元素的控制。
程序的渲染结果通常保存在后帧缓存(back frame buffer)在内的其他帧缓存,当渲染后的后帧缓存完成后,前后帧缓存会互换。(这部分操作由操作系统来完成)

前帧缓存决定了屏幕上显示的像素颜色,会在适当的时候与后帧缓存切换。

Core Animation的合成器会联合OpenGL ES层和UIView层、StatusBar层等,在后帧缓存混合产生最终的颜色,并切换前后帧缓存;
OpenGL ES坐标是以浮点数来存储,即使是其他数据类型的顶点数据也会被转化成浮点型;


视图加载

那么在了解iOS视图渲染流程以后,再来看一下第二题:
一个UIImageView添加到视图上以后,内部是如何渲染到手机上的,请简述其流程?

图片的显示分为三步:加载、解码、渲染。
通常,我们操作的只有加载,解码和渲染是由UIKit进行。
以UIImageView为例。当其显示在屏幕上时,需要UIImage作为数据源。
UIImage持有的数据是未解码的压缩数据,能节省较多的内存和加快存储。
当UIImage被赋值给UIImage时(例如imageView.image = image;),图像数据会被解码,变成RGB的颜色数据。
解码是一个计算量较大的任务,且需要CPU来执行;并且解码出来的图片体积与图片的宽高有关系,而与图片原来的体积无关。
此处引用-->iOS性能优化——图片加载和处理

我查看了较为流行的第三方库源码,例如YYImage、SDWebImage、FastImageCache,其中加载一个图片的流程大致为:

  1. 查看UIImageView的API我们可以发现,UIImage封装了一个CoreGraphics/CGImage的对象。
    1.+[UIImage imageWithContentsOfFile:]使用Image I/O创建CGImageRef内存映射数据。此时,图像尚未解码。
  2. 返回的图像被分配给UIImageView。
  3. 如果图像数据为未解码的PNG/JPG,解码为位图数据
  4. 隐式CATransaction捕获到UIImageView layer树的变化
  5. 在主运行循环的下一次迭代中,Core Animation提交隐式事务,这会涉及创建已设置为层内容的所有图像的副本,根据图像:
    1. 缓冲区被分配用于管理文件IO和解压缩操作。
    2. 文件数据从磁盘读入内存。
    3. 压缩的图像数据被解码成其未压缩的位图形式
    4. Core Animation使用未压缩的位图数据来渲染图层。

再看一下YYImage的源码,其流程也大致为:

  1. 获取图片二进制数据
  2. 创建一个CGImageRef对象
  3. 使用CGBitmapContextCreate()方法创建一个上下文对象
  4. 使用CGContextDrawImage()方法绘制到上下文
  5. 使用CGBitmapContextCreateImage()生成CGImageRef对象。
  6. 最后使用imageWithCGImage()方法将CGImage转化为UIImage。

当然YYImage不止做了这些,还有解码器编码器,支持webP等多种格式,并且还写了自定义的操作队列,对网络加载图片进行了优化。在此不赘述。

推荐文章:
苹果官方文档-CGImage位图
iOS图片加载速度极限优化—FastImageCache解析
Image I/O详解的文章
在这里同时推荐Y大的两篇文章
移动端图片格式调研
iOS 处理图片的一些小 Tip

视图渲染优化&卡顿优化

接下来我们看一下第三题:在一个表内有很多cell,每个cell上有很多个视图,如何解决卡顿问题?

什么是卡顿?苹果官方文章-显示帧率

当你的主线程操作卡顿超过16.67ms以后,你的应用就会出现掉帧,丢帧的情况。也就是卡顿。

一般来说造成卡顿的原因,就是CPU负担过重,响应时间过长。主要原因有以下几种:

  • 隐式绘制 CGContext
  • 文本CATextLayer 和 UILabel
  • 光栅化 shouldRasterize
  • 离屏渲染
  • 可伸缩图片
  • shadowPath
  • 混合和过度绘制
  • 减少图层数量
  • 裁切
  • 对象回收
  • Core Graphics绘制
  • -renderInContext: 方法

其中最常见的问题就是离屏渲染

离屏渲染:离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图 片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图 层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。

如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的 纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。

那么如何在需要渲染大量视图的情况下,还能保证流畅度,也就是保证FPS。
在这里推荐阅读郭曜源前辈的iOS 保持界面流畅的技巧
以及indulge_in的YYAsyncLayer剖析
我参考了YYAsyncLayer,他其中的原理大致是这样的:

YYAsyncLayer原理

YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了 [layer setNeedDisplay])时,它会向 delegate,也就是 UIView 请求一个异步绘制的任务。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消。

当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,我都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。

AsyncDisplayKit原理

ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。

Tips

优化方案围绕着 使用多线程调用,合理利用CPU计算位置,布局,层次,解压等,再合理调度GPU进行渲染,GPU负担常常要比CPU大,合理调度CPU进行计算可以减轻GPU渲染负担,使应用更加流畅。


Metal渲染引擎

当你现在再去查阅官方文档时,你会发现苹果官方已经使用Metal去替代OpenGL ES作为Core Animation的渲染。

苹果将Metal作为新的渲染引擎,更好的利用了GPU的性能,同时保证了低内存占用和省电,但我个人并没有深入研究Metal,这里可以有兴趣的同学可以看一下落影前辈的文章:
Metal入门教程总结
Metal入门教程(八)Metal与OpenGL ES交互
OpenGL 专题


事件响应链&原理

最后一题:UIView和CALayer的区别?

如果你已经做了几年iOS开发,相比对于这道题可能已经很熟悉。
最直接的回答就是UIView可以响应用户事件,而CALayer不能处理事件

首先要讲一下App中的事件响应链,它分为两部分:Hit-Testing事件传递 & Runloop原理

当用户对屏幕进行了操作,产生了一个用户事件。

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue()进行应用内部的分发。

_UIApplicationHandleEventQueue()会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel事件都是在这个回调中完成的。
此处引用-->深入理解Runloop-事件响应


当前前台运行中应用接收到UIEvent以后,当用户对屏幕进行了操作,系统先循环调用Hit-test遍历视图栈里的视图,顺序为视图层次的逆顺序,用Responder Chain响应链传递一层层给根视图AppDelegate处理。-->苹果官方文档-使用响应者和响应者链来处理事件

推荐两篇文章:
iOS 事件处理机制与图像渲染过程
iOS事件响应链中Hit-Test View的应用

CALayer的职能

CALayer 并不清楚具体的响应链,所以不能直接处理触摸事件或者手势。但是它提供了-containsPoint:-hitTest:来判断是否一个触点在图层的范围之内。

与UIView不同,CALayer着重于图层的绘制,大致为以下职能:

  • 阴影、圆角、边框、蒙版、拉伸、transform、动画。
  • 寄宿图:你可以给CALayer.contents传递一个CGImage来进行渲染,也可以调用- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;方法进行绘制。但通常我们会使用UIView的drawRect方法
  • CATextLayer:直接将字符串使用Core Graphics写入图层
  • CATransformLayer:能够用于构造一个层级的3D结构

CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

使用图层关联的视图而不是单独使用 CALayer 的好处在于,你能在使用所
有 CALayer 底层特性的同时,也可以使用 UIView 的高级API(比如自动排版, 布局和事件处理)。做一些对性能特别挑剔的工作,比如对 UIView 一些可忽略不计的操作都会引 起显著的不同

关于UIView动画以及CALayer的动画这里推荐两篇文章:
iOS-UIView与CALayer动画原理
CALayer与iOS动画 讲解及使用

参考

本文大量借助了引用文章的文字描述,在此感谢各位作者的文章对本问题的理解起了很大的帮助。也希望各位能去原文发表自己的看法。谢谢~

总结

iOS开发要学的东西还有很多,因为时间的推移,每年的iOS岗位要求都在提高,导致我们在iOS开发岗位的同学要学习很多知识。例如Runtime、Runloop、音视频处理、视图渲染等,各位一起加油吧。

链接:https://www.jianshu.com/p/39b91ecaaac8

收起阅读 »

UIKit -大话 iOS Layout

大话 iOS Layout在iOS的开发中,我们绝大部分的时间都是在跟UI打交道,例如UI怎么布局,UI怎么刷新,以及对复杂UI的优化,使我们的APP更加流畅。对于UI的布局,xcode提供了可视化的布局方式:xib、storyboard,这是非常便捷的布局方...
继续阅读 »

大话 iOS Layout

在iOS的开发中,我们绝大部分的时间都是在跟UI打交道,例如UI怎么布局,UI怎么刷新,以及对复杂UI的优化,使我们的APP更加流畅。

对于UI的布局,xcode提供了可视化的布局方式:xib、storyboard,这是非常便捷的布局方式,所见即所得,门槛也非常低,但占用的资源相对代码来说更多,而且在多人协作开发的过程中,处理xml格式的文件冲突是非常困难的,所以很多团队都不推荐使用这类方式的布局,适合需求相对简单的团队、需要快速迭代的项目。

纯代码方式的布局是我们必修课,苹果有提供 Frame 和 Auto Layout 两种方式的布局。Auto Layout是苹果为我们提供的一整套布局引擎(Layout Engine),这套引擎会将视图、约束、优先级、大小通过计算转化成对应的 frame,而当约束改变的时候,会再次触发该系统重新计算。Auto Layout本质就是一个线性方程解析Engine。基于Auto Layout,不再需要像frame时代一样,关注视图的尺寸、位置相关参数,转而关注视图之间的关系,描述一个表示视图间布局关系的约束集合,由Engine解析出最终数值。

在混合开发的布局中,同样都会有一个虚拟DOM机制,当布局发生改变的时候,框架会将修改提交到虚拟DOM虚拟DOM先进行计算,计算出各个节点新的相对位置,最后提交到真实DOM,以完成渲染,当多个修改同时被提交的时候,框架也会对这些修改做一个合并,避免每次修改都要刷新。这种机制跟iOS中的run loop的渲染机制非常类似。Layout Engine计算出视图的frame,等到下一次run loop到来的时候,将结果提交到渲染层以完成渲染,同样,也会对一些修改进行“合并”,直到下一次运行循环到来时,才将结果渲染出来。

这篇文章主要讲解在布局的过程中,视图分别在Auto Layout以及Frame的方式下,如何完成刷新。

Main run loop of an iOS app

主运行循环是iOS应用中用来处理所有的用户输入和触发合适的响应事件。iOS应用的所有用户交互都会被添加到一个事件队列(event queue)里面。UIApplication object会将所有的事件从这个队列中取出,然后分发到应用中的其他对象。它通过解释用户的输入事件并在application’s core objects中调用相似的处理,以执行运行循环。这些处理事件可以被开发者重写。一旦这这些方法调用结束,程序就会回到运行循环,然后开始更新周期(update cycle)更新周期(update cycle)负责视图的布局和重绘



Update cycle

更新周期(update cycle)开始的时间点:应用程序执行完所有事件处理后,控制权返回main run loop。在这个时间点后,系统开始布局、显示和约束。如果在系统正在执行事件处理时需要更改某个视图,系统会将这个视图标记为需要重绘。在下一个更新周期,系统将会执行这个视图的所有修改。为了让用户交互和布局更新之间的延迟不被用户察觉到。iOS应用程序以60fps的帧率刷新界面,也就是每一个更新周期的时间是1/60秒。由于更新周期存在一定的时间间隔,所以我们在布局界面的过程中,会遇到某些视图并不是我们想要实现的效果,拿到的其实是上一次运行循环的效果。(这里YY一下,大家可能都会在业务代码中遇到过这种问题,某个视图布局不对,我们加个0.5的延迟,然后就正确了,或者是异步添加到主队列,界面布局也正常了。这些都是取巧的操作,刷新相关的问题仍然存在,可能在你的这个界面不会出问题,但有可能会影响到别人的、或者其他的界面。)所以说,“出来混,迟早都是要还的”,问题也迟早都是要解决的。接下来将会介绍如何准确的知道视图的布局、绘制、约束触发的时间点,如何正确的去刷新视图。

60fps 的 1/60秒
1/60秒CPU+GPU整个计算+绘制的时间间隔,如果在这个时间段内,并没有完成显示数据的准备,那iOS应用将显示上一帧画面,这个就是所谓的掉帧。CPU中有大量的逻辑控制单元,而GPU中有大量的数据计算单元,所以GPU的计算效率远高于GPU。为了提高效率,我们可以尽量将计算逻辑交给GPU。关于具体GPU的绘制流程相关的文章,可以参考OpenGL专题

下图可以看出来,在 main run loop 结束后开始 更新周期(update cycle)



Layout

视图的布局指的它在屏幕上的位置和大小。每一个视图都有一个frame,用于定义它在父视图坐标系中的位置和大小。UIView会提供一些方法以通知视图的布局发生改变,同样也提供一系列方法供开发者重写,用来处理视图布局完成之后的操作。

layoutSubviews()

  • 这个方法用来处理一个视图和它所有的子视图的重新布局(位置、大小),它提供了当前视图和所有子视图的frame。

  • 这个方法的开销是昂贵的,因为它作用于所有的子视图,并逐级调用所有子视图的layoutSubviews()方法

  • 系统在重新计算frame的时候会调用这个方法,所以当我们需要设置特定的frame的时候,可以重写这个方法。

  • 永远不要直接调用这个方法来刷新frame。在运行循环期间,我们可以通过调用其他方法来触发这个方法,这样造成的开销会小的多。

  • 当UIView的layoutSubviews()调用完成之后,就会调用它的ViewController的viewDidLayoutSubviews()方法,而layoutSubviews()方法是视图布局更新之后的唯一可靠方法。所以对于依赖视图frame相关的逻辑代码应该放在viewDidLayoutSubviews()方法中,而不是viewDidLoadviewDidAppear中,这样就可以避免使用陈旧的布局信息。

layoutSubviews 是在系统重新计算frame之前调用,还是在重新计算frame之后调用。(初步估计是计算之后)

Automatic refresh triggers

有很多事件会自动标记一个视图已经更改了布局,以便于layoutSubviews()方法在下一次执行的时候调用,而不是由开发者手动去做这些事。

一些自动通知系统布局已经更改的方法有:

  • 调整视图的大小
  • 添加一个子视图
  • 用户滑动UIScrollView(UIScrollView和它的父视图将会调用layoutSubviews()
  • 用户旋转设备
  • 更新视图的约束

这些方法都会告诉系统需要重新计算视图的位置,而且最终也会自动调用到layoutSubviews()方法。除此之外,有方法可以直接触发layoutSubviews()的调用。

setNeedsLayout()

  • setNeedsLayout() 是触发 layoutSubviews() 造成开销最小的方法。它会直接告诉系统,view 的布局需要重新计算。setNeedsLayout()会立即执行并返回,而且在返回之前,是不会去更新 view。
  • 当系统逐级调用 layoutSubviews() 之后,view 会在下一个 更新周期(update cycle) 更新。尽管在 setNeedsLayout() 与视图的重绘和布局之间有一定的时间间隔,但这个时间间隔不会长到影响到用户交互。

setNeedsLayout() 是在什么时候 return

layoutIfNeeded()

  • 执行 layoutIfNeeded() 之后,如果 view 需要更新布局,系统会立刻调用 layoutSubviews() 方法去更新,而不是将 layoutSubviews() 方法加入队列,等待下一次 更新周期(update cycle) 再去调用;
  • 当我们在调用 setNeedsLayout() 或者是其他自动触发刷新的事件之后,执行 layoutIfNeeded() 方法,可以立即触发 layoutSubviews() 方法。
  • 如果一个 view 不需要更新布局,执行 layoutIfNeeded() 方法也不会触发 layoutSubviews() 方法。例如,当我们连续执行两次 layoutIfNeeded() 方法,第二次执行将不会触发 layoutSubviews() 方法。

使用 layoutIfNeeded() 方法,子视图的布局和重绘将立即发生,并且在该方法返回之前就可以完成(除非视图正在做动画)。如果需要依赖一个新的视图布局,并且不想等视图的更新到下一个 更新周期(update cycle) 才完成,使用 layoutIfNeeded() 方法时非常有用的。除了这种场景,一般使用 setNeedsLayout() 方法等到下一个 更新周期(update cycle) 去更新视图就可以了,这样可以保证在一次 run loop 里面只更新视图一次。

在使用约束动画的时候,这个方法是非常有用的。一般操作是,在动画开始前调用一次 layoutIfNeeded() 方法,以保证在动画之前布局的更新都已经完成。配置完我们的动画后,在 animation block 里面,再调一次 layoutIfNeeded() 方法,就可以更新到新的状态。

Display

视图的展示包含的属性不涉及视图及子视图的 size 和 position,例如:颜色、文本、图片和 Core Graphic drawing。显示通道包含于触发更新的布局通道类似的方法,它们都是当系统检测到有变更时,由系统调用,而且我们也都能手动的去触发刷新。

draw(_:)

UIView 的 draw(Objective-C里面是drawRect)方法,作用于视图的内容,就像 layoutSubviews() 作用于视图的 size 和 position。但是,这个方法不会触发子视图的 draw(Objective-C里面是drawRect)方法。这个方法也不能由开发者直接调用,我们应该在 run loop 期间,调用可以触发 draw方法的其他方法来触发draw方法。

setNeedsDisplay()

setNeedsDisplay() 方法等同于 setNeedsLayout() 方法。它会设置一个内部的标记来标记这个视图的内容需要更新,但它在视图重绘之前返回。然后,在下一个 更新周期(update cycle) 系统会遍历所有有这个标记的视图,然后调用它们的 draw 方法。如果只需要在下一个更新周期(update cycle)重绘视图的部分内容,可以调用setNeedsDisplay() 方法,并通过rect属性来设置我们需要重绘的部分。

大多数情况下,想要在下一个更新周期(update cycle)更新一个视图上的UI组件,通过自动设置内部的内容更新标记而不是手动调用setNeedsDisplay()方法。但是,如果一个视图(aView)并不是直接绑定到UI组件上的,但是我们又希望每次更新的时候都可以重绘这个视图,我们可以通过观察视图(aView)属性的setter方法(KVO),来调用setNeedsDisplay()方法以触发适当的视图更新。

当需要执行自定义绘制时,可以重写draw方法。下面可以通过一个例子来理解。

  • numberOfPointsdidSet方法中调用setNeedsDisplay()方法,可以触发draw方法。
  • 通过重写draw方法,以达到在不同情况下,绘制不同的样式的效果。
class MyView: UIView {
var numberOfPoints = 0 {
didSet {
setNeedsDisplay()
}
}

override func draw(_ rect: CGRect) {
switch numberOfPoints {
case 0:
return
case 1:
drawPoint(rect)
case 2:
drawLine(rect)
case 3:
drawTriangle(rect)
case 4:
drawRectangle(rect)
case 5:
drawPentagon(rect)
default:
drawEllipse(rect)
}
}
}


不像layoutIfNeeded可以立刻触发layoutSubviews那样,没有方法可以直接触发一个视图的内容更新。视图内容的更新必须等到下一个更新周期去重绘视图。

Constraints

在自动布局中,视图的布局和重绘需要三个步骤:

  1. 更新约束:系统会计算并设置视图上所有必须的约束。
  2. 布局阶段layout engine计算视图的frame,并将它们布局。
  3. 显示过程:结束更新循环并重绘视图内容,如果有必要,会调用draw方法。

updateConstraints()

  • updateConstraints在自动布局中的作用就像layoutSubviews在frame布局、以及draw在内容重绘中的作用一样。
  • updateConstraints方法只能被重写,不能被直接调用。
  • updateConstraints方法中一般只实现那些需要改变的约束,对于不需要改变的约束,我们尽可能的别写在里面。
  • Static constraints也应该在接口构造器、视图的初始化方法、或viewDidLoad()方法中实现,而不是放在updateConstraints方法中实现。

有以下一些方式可以自动触发约束的更新。在视图内部设置一个update constraints的标志,该标志会在下一个update cycle中,触发updateConstraints方法的调用。

  • 激活或停用约束;
  • 更改约束的优先级、常量值;
  • 移除约束;

除了自动触发约束的更新之外,同样也有以下方法可以手动触发约束的更新。

setNeedsUpdateConstraints()

调用setNeedsUpdateConstraints方法可以保证在下一个更新周期进行约束的更新。它触发updateConstraints方法的方式是通过标记视图的某个约束已经更新。这个方法的工作方式跟setNeedsLayoutsetNeedsDisplay类似。

updateConstrainsIfNeeded()

这个方法等同于layoutIfNeeded,但是在自动布局中,它会检查constraint update标记(这个标记可以被自动设置、也可以通过setNeedsUpdateConstraintsinvalidateInstinsicContentSize方法手动设置)。如果它确定约束需要更新,就会立即触发updateConstraints方法,而不是等到 run loop 结束。

invalidateInstinsicContentSize()

自动布局中某些视图拥有intrinsicContentSize属性,这是视图根据它的内容得到的自然尺寸。一个视图的intrinsicContentSize通常由所包含的元素的约束决定,但也可以通过重载提供自定义行为。调用invalidateIntrinsicContentSize()会设置一个标记表示这个视图的intrinsicContentSize已经过期,需要在下一个布局阶段重新计算。

How it all connects

布局、显示和约束都遵循着相似的模式,例如:他们更新的方式以及如何在 run loop 的不同时间点上强制更新。任一组件都有一个实际去更新的方法(layoutSubviewsdraw, 和updateConstraints),这些方法可以通过重写来手动操作视图,但任何情况下都不要显式调用。这个方法只在 run loop 的末端会被调用,如果视图被标记了告诉系统该视图需要被更新的标记话。有一些操作会自动设置这个标记,也有一些方法允许显式地设置它。对于布局和约束相关的更新,如果等不到在 run loop 结束才更新的话(例如:其他行为依赖于新布局),也有方法可以让你立即更新,并保证 update layout能被正确标记。下面的表格列出了任意组件会怎样更新及其对应方法。

LayoutDisplayConstraints方法意图
layoutSubviewsdrawupdateConstraints执行更新的方法,可以被重
写,但不能被调用
setNeedsLayoutsetNeedDisplaysetNeedsUpdateConstaints
invalidateInstrinsicContentSize
显示的标记视图需要在下一个更新循环更新
layoutIfNeeded--updateConstraintsIfNeeded立刻更新被标记的视图
添加视图
重设size
设置frame(需要改变bounds)
滑动ScrollView
旋转设备
发生在视图的bounds内部的改变激活、停用约束
修改约束的优先级和常量值
移除约束
隐式触发视图更新的事件

下图总结了更新周期(update cycle)事件循环(event loop)之间的交互,并且指示了上面这些方法在周期中下一步指向的位置。你可以现实的调用layoutIfNeededupdateConstraintsIfNeeded在run loop的任何地方,但需要注意的是,这两个方式是有潜在开销的。如果update constrintsupdate layoutneeds display标记被设置,在 run loop 的结尾处的更新周期就会更新约束、布局、显示内容。一但这些更新全部完成,run loop就会重新开始。






作者:修_远
链接:https://www.jianshu.com/p/98dec55a06c8

收起阅读 »

iOS - 图片显示类似LED的效果

iOS
LED灯的效果展示。整理了一下,自己所了解的知识。通过一些其他方式。在App界面展示出现LED的效果。屏幕快照 2020-10-27 上午9.43.30.png1.绘制图片 (或者是图片)2.通过获取到像素点的颜色去进行展示。每一个像素点有 RGB A 这个四...
继续阅读 »

LED灯的效果展示。
整理了一下,自己所了解的知识。通过一些其他方式。
在App界面展示出现LED的效果。



屏幕快照 2020-10-27 上午9.43.30.png

1.绘制图片 (或者是图片)
2.通过获取到像素点的颜色去进行展示。每一个像素点有 RGB A 这个四个
3.通过获取到的 bitMap 展示出LED的效果

第一步随意找一张图片 方便获取到他的像素点

第二步

//2.获取到图片的 bitMap
/**
- 传入图片的信息,返回图片的像素点信息
- 返回的数据排列。 R G B ,A :亮度, row:行数, col 列数
*/

func getImagePixel(_ image:UIImage) -> Array<Any>{
//存储像素的数据
let grayScale: [Pixel] = (image.pixelData.map {
//将RGB的颜色记录下来
return $0

})
//返回像素的数据
return grayScale
}


//MARK: 创建接受像素点的model
struct Pixel {
var r: Float
var g: Float
var b: Float
var a: Float
var row: Int
var col: Int
init(r: UInt8, g: UInt8, b: UInt8, a: UInt8, row: Int, col: Int) {
self.r = Float(r)
self.g = Float(g)
self.b = Float(b)
self.a = Float(a)
self.row = row
self.col = col
}
var color: UIColor {
return UIColor(
red: CGFloat(r/255.0),
green: CGFloat(g/255.0),
blue: CGFloat(b/255.0),
alpha: CGFloat(a/255.0)
)
}
var description: String {
return "\(r), \(g), \(b), \(a) ,\(row) ,\(col)"
}
}


//MARK: 读取像素点的方法
extension UIImage{

var pixelData: [Pixel] {

var pixelS = [Pixel]()
for row in 0 ..< Int(self.size.width){
for col in 0 ..< Int(self.size.height){
let coloR = self.cxg_getPointColor(withImage: self, point: CGPoint(x: row, y: col))
pixelS.append(Pixel(r: coloR![0], g: coloR![1], b: coloR![2], a: coloR![3], row: row, col: col))
}
}
//返回取出颜色的数组 返回RGB 亮度 行数、列数
return pixelS
}
/// - Parameters:
/// - image: 要获取颜色的图片
/// - point: 每一次要获取到的点的颜色
/// - Returns: 获取到的颜色
func cxg_getPointColor(withImage image: UIImage, point: CGPoint) -> [ UInt8]? {
guard CGRect(origin: CGPoint(x: 0, y: 0), size: image.size).contains(point) else {
return nil
}
let pointX = trunc(point.x);
let pointY = trunc(point.y);

let width = image.size.width;
let height = image.size.height;
let colorSpace = CGColorSpaceCreateDeviceRGB();
var pixelData: [UInt8] = [0, 0, 0, 0]

pixelData.withUnsafeMutableBytes { pointer in
if let context = CGContext(data: pointer.baseAddress, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue), let cgImage = image.cgImage {
context.setBlendMode(.copy)
context.translateBy(x: -pointX, y: pointY - height)
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
}
}
return pixelData
}
}

第三步

//3.通过获取到的 点阵的位置绘制出来点阵图
class TestView:UIView{
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
}
var x:CGFloat = 0.0
var y:CGFloat = 0.0

var imagePixel_Array:Array<Pixel>?
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
//获取绘图上下文
guard let context = UIGraphicsGetCurrentContext() else {
return
}
self.backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
if imagePixel_Array != nil{
//控制小格子有多大
let size: CGSize = .init(width: self.frame.size.width / 18, height: self.frame.size.height/12)
for (index, pixel) in (imagePixel_Array?.enumerated())! {

//创建一个矩形,它的所有边都内缩3点
let drawingRect = CGRect(x: x, y: y, width: size.width, height: size.height)
//创建并设置路径
let path = CGMutablePath()
//绘制矩形
path.addRect(drawingRect)
//添加路径到图形上下文
context.addPath(path)
//设置填充颜色
context.setFillColor(UIColor.init(red: CGFloat(pixel.r / 255), green: CGFloat(pixel.g / 255), blue: CGFloat(pixel.b / 255), alpha: CGFloat(pixel.a / 255)).cgColor)
//绘制路径并填充
context.drawPath(using: .fillStroke)
print("---\(pixel.description)")

y += size.height
if index % 12 == 0 && index != 0{
x += size.width
y = 0
}
}
}
}
}

如何调用

        let image_Png = UIImage.init(named: "1111")!
let imagePixel_Array_2 = self.getImagePixel(image_Png!)
let testView_2 = TestView.init(frame: CGRect(x: 100, y: 400, width: 18 * 12, height: 12 * 12))
testView_2.imagePixel_Array = (imagePixel_Array_2 as! Array<Pixel>)
testView_2.setNeedsDisplay()
self.view.addSubview(testView_2)

现在只是效果只是完成了一丢丢
后面接着去研发,去研究这个展示 led效果的功能
如果有疑惑,评论。我就会去和大家讨论


1人点赞
收起阅读 »

iOS-Cocoapods 的正确安装姿势

iOS
在安装过程中出现curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused 问题访问我的处理方式可能会对你有帮助. 文末附带rvm 无法在线安装的解...
继续阅读 »

在安装过程中出现curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused 问题访问我的处理方式可能会对你有帮助.

文末附带rvm 无法在线安装的解决办法.

文末还提供了pod install或者serach 过程中[!]CDN: trunk URL couldn't be downloaded:的解决办法.


1. Mac环境下 Cocoapods 的安装


1.1 总体步骤



下载Xcode —>安装rvm —>安装ruby —>安装home-brew —>安装cocoapods


1.2 安装前,先检查是否有安装残留


1. 如果之前装过cocopods,最好先卸载掉,卸载命令:
$ sudo gem uninstall cocoapods

2. 先查看本地安装过的cocopods相关东西,命令如下:
$ gem list --local | grep cocoapods
会显示如下:
cocoapods (1.7.2)
cocoapods-core (1.7.2)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.2.2)
cocoapods-plugins (1.0.0)
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.3.1)
cocoapods-try (1.1.0)

3. 使用删除命令, 逐个删除:
$ sudo gem uninstall cocoapods-core

1.3 Mac文件夹的显示隐藏命令行:


隐藏:defaults write com.apple.finder AppleShowAllFiles -bool true
显示:defaults write com.apple.finder AppleShowAllFiles -bool false

这里选择将隐藏文件显示出来; 退出终端,重启Finder. 如果不确定,可以把主目录下的隐藏文件都给删了.

1.4. RVM



  • Ruby Version Manager,Ruby版本管理器,包括Ruby的版本管理和Gem库管理(gemset)


1. 安装RVM
$ curl -sSL https://get.rvm.io | bash -s stable
期间可能需要管理员密码, 以及自动通过homebrew安装依赖包,等待一段时间就安装好了.

2. 载入 RVM 环境
$ source ~/.rvm/scripts/rvm

3. 检查一下是否安装正确
$ rvm -v
会显示如下:
rvm 1.29.8 (latest) by Michal Papis, Piotr Kuczynski, Wayne E. Seguin [https://rvm.io]
表示安装正确.

注意: 也可使用 ($ rvm -v) 来判断是否安装了rvm
// 结果类似如下代表没有安装rvm
zsh: command not found: rvm

1.5 用RVM安装Ruby环境


1. 列出已知的ruby版本
$ rvm list known

2. 选择最新版本进行安装(这里以2.6.0为例)
$ rvm install 2.6.0

同样继续等待漫长的下载,编译过程,完成以后,Ruby, Ruby Gems 就安装好了。

3. 查询已经安装的ruby
$ rvm list

卸载一个已安装版本的命令
$ rvm remove + 要卸载的版本号

4. RVM 装好以后,需要执行下面的命令将指定版本的 Ruby 设置为系统默认版本
$ rvm 2.6.0 --default

5. 测试操作是否正确(分 2 步)
$ ruby -v
会显示如下:
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]

$ gem -v
会显示如下:
3.0.4

注: RubyGems(简称 gems)是一个用于对 Ruby组件进行打包的 Ruby 打包系统。 它提供一个分发 Ruby 程序和库的标准格式,还提供一个管理程序包安装的工具。

1.6 更换镜像源


1. 查看当前镜像源
$ gem sources -l
会显示如下:
*** CURRENT SOURCES ***
http://rubygems.org/

2. 先删除, 再添加
$ gem sources --remove https://rubygems.org/
$ gem sources -a https://gems.ruby-china.com/

3. 再次查看, 测试是否成功
$ gem sources -l
会显示如下:
*** CURRENT SOURCES ***
https://gems.ruby-china.com/

到这里就已经把Ruby环境成功的安装到了Mac OS X上,接下来就可以进行相应的开发使用了。

1.7 安装home-brew




  • 也可选择跳过这步, 直接安装cocoapods, 引入库文件时, 会提示你自动安装home-brew

  • Homebrew: 是一个包管理器,用于在Mac上安装一些OS X没有的UNIX工具。

  • 官方网址: https://brew.sh/index_zh-cn

  • Homebrew是完全基于 Git 和 ruby.



1. 安装
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”
安装过程中需要按回车键确认

2. 检测是否存在冲突
$ brew doctor

3. 检查是否安装成功, 出现版本号就成功了.
$ brew --version

1.8 安装Cocoapods (步骤有点慢,不要急)


1. 坑点:
使用$ sudo gem install cocoapods安装cocoapods 极有可能报error: RPC failed / early EOF

2. 正确的使用方法:
A. 看到报这个错之后,需要在终端执行$ sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer 这句,具体如下: 先找到xcode,显示包内容,在Contents里找到Developer文件,然后在终端输入sudo xcode-select -switch ,把找到的Developer文件夹拖进终端,就得到后边的路径啦,然后执行。因为xcode位置和版本安装的不一样,可能路径会有所不同。我的最终是sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer 这个。
B. 执行$ sudo gem install -n /usr/local/bin cocoapods
C. Git clone https://git.coding.net/CocoaPods/Specs.git ~/.cocoapods/repos/master

然后就等待吧,下载完就ok了.

2. 离线安装RVM方式



// 离线包
curl -sSL https://github.com/rvm/rvm/tarball/stable -o rvm-stable.tar.gz
// 创建文件夹
mkdir rvm && cd rvm
// 解包
tar --strip-components=1 -xzf ../rvm-stable.tar.gz
// 安装
./install --auto-dotfiles
// 加载
source ~/.rvm/scripts/rvm
// if --path was specified when instaling rvm, use the specified path rather than '~/.rvm'

// 查询 ruby的版本
rvm list known


在查询 ruby的版本时可能会出现下面的错误:A RVM version () is installed yet 1.25.14 (master) is loaded.Please do one of the following:* 'rvm reload'* open a new shell* 'echo rvm_auto_reload_flag=1 >> ~/.rvmrc' # for auto reload with msg.* 'echo rvm_auto_reload_flag=2 >> ~/.rvmrc' # for silent auto reload.




解决办法: sudo rm -rf /users/your_username/.rvmThen close and reopen the terminal.

然后重新打开终端即可.



3. [!] CDN: trunk URL couldn't be downloaded:


CocoaPods 1.8 版本之后的一些说明!



我的解决方法



// 在podfile 文件中添加 (选一个就行)
source 'https://github.com/CocoaPods/Specs.git'

source 'https://cdn.cocoapods.org/'

.End

链接:https://www.jianshu.com/p/d80b06f6e4e7
收起阅读 »

iOS swift与oc混编问题解决

1、手动创建桥接文件2、桥接文件中导入 通过cocoapods pod下来的第三方OC文件,报找不到在target—>Build Setting里找到search Paths,双击User Header Search Paths后面的空白处,设置目录路径...
继续阅读 »
1、手动创建桥接文件



2、桥接文件中导入 通过cocoapods pod下来的第三方OC文件,报找不到


在target—>Build Setting里找到search Paths,双击User Header Search Paths后面的空白处,设置目录路径为${SRCROOT}
${SRCROOT}后边选择recursive递归根目录下的所有文件。


3、OC文件中调用swift文件,需要导入头文件,这个头文件叫啥呢?

一般为项目名称-swift.h

当然也可查看,地方在这里



4、Swift中 字符串转化为Class怎么做

在Swift中由于命名空间的存在,我们可以用下面的方法进行转化。

func getClass(stringName: String) -> Class {

guard let nameSpage = Bundle.main.infoDictionary!["CFBundleExecutable"] as? String else {
print("没有命名空间")
return
}

guard let childVcClass = NSClassFromString(nameSpage + "." + vcName) else {
print("没有获取到对应的class")
return
}

guard let childVcType = childVcClass as? UIViewController.Type else {
print("没有得到的类型")
return
}

//根据类型创建对应的对象
let vc = childVcType.init()

return vc

}
5、修改pod文件,运行调试时缓存之前数据,如下图


链接:https://www.jianshu.com/p/83f70b366ff4



收起阅读 »

一招搞定 iOS 14.2 的 libffi crash

苹果升级 14.2,全球 iOS 遭了秧。libffi 在 iOS14.2 上发生了 crash, 我司的许多 App 深受困扰,有许多基础库都是用了 libffi。经过定位,发现是 vmremap 导致的 code sign error。我们通过使用静态 t...
继续阅读 »

苹果升级 14.2,全球 iOS 遭了秧。libffi 在 iOS14.2 上发生了 crash, 我司的许多 App 深受困扰,有许多基础库都是用了 libffi。


经过定位,发现是 vmremap 导致的 code sign error。我们通过使用静态 trampoline 的方式让 libffi 不需要使用 vmremap,解决了这个问题。这里就介绍一下相关的实现原理。

libffi 是什么

高层语言的编译器生成遵循某些约定的代码。这些公约部分是单独汇编工作所必需的。“调用约定”本质上是编译器对函数入口处将在哪里找到函数参数的假设的一组假设。“调用约定”还指定函数的返回值在哪里找到。
一些程序在编译时可能不知道要传递给函数的参数。例如,在运行时,解释器可能会被告知用于调用给定函数的参数的数量和类型。Libffi 可用于此类程序,以提供从解释器程序到编译代码的桥梁。
libffi 库为各种调用约定提供了一个便携式、高级的编程接口。这允许程序员在运行时调用调用接口描述指定的任何函数。

ffi 的使用

简单的找了一个使用 ffi 的库看一下他的调用接口

ffi_type *returnType = st_ffiTypeWithType(self.signature.returnType);
NSAssert(returnType, @"can't find a ffi_type of %@", self.signature.returnType);

NSUInteger argumentCount = self->_argsCount;
_args = malloc(sizeof(ffi_type *) * argumentCount) ;

for (int i = 0; i < argumentCount; i++) {
ffi_type* current_ffi_type = st_ffiTypeWithType(self.signature.argumentTypes[i]);
NSAssert(current_ffi_type, @"can't find a ffi_type of %@", self.signature.argumentTypes[i]);
_args[i] = current_ffi_type;
}

// 创建 ffi 跳板用到的 closure
_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&xxx_func_ptr);

// 创建 cif,调用函数用到的参数和返回值的类型信息, 之后在调用时会结合call convention 处理参数和返回值
if(ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args) == FFI_OK) {

// closure 写入 跳板数据页
if (ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), xxx_func_ptr) != FFI_OK) {
NSAssert(NO, @"genarate IMP failed");
}
} else {
NSAssert(NO, @"");
}

看完这段代码,大概能理解 ffi 的操作。

  1. 提供给外界一个指针(指向 trampoline entry)
  2. 创建一个 closure, 将调用相关的参数返回值信息放到 closure 里
  3. 将 closure 写入到 trampoline 对应的 trampoline data entry 处

之后我们调用 trampoline entry func ptr 时,

  1. 会找到 写入到 trampoline 对应的 trampoline data entry 处的 closure 数据
  2. 根据 closure 提供的调用参数和返回值信息,结合调用约定,操作寄存器和栈,写入参数 进行函数调用,获取返回值。

那 ffi 是怎么找到 trampoline 对应的 trampoline data entry 处的 closure 数据 呢?

我们从 ffi 分配 trampoline 开始说起:

static ffi_trampoline_table *
ffi_remap_trampoline_table_alloc (void)
{
.....
/* Allocate two pages -- a config page and a placeholder page */
config_page = 0x0;
kt = vm_allocate (mach_task_self (), &config_page, PAGE_MAX_SIZE * 2,
VM_FLAGS_ANYWHERE);
if (kt != KERN_SUCCESS)
return NULL;

/* Allocate two pages -- a config page and a placeholder page */
//bdffc_closure_trampoline_table_page

/* Remap the trampoline table on top of the placeholder page */
trampoline_page = config_page + PAGE_MAX_SIZE;
trampoline_page_template = (vm_address_t)&ffi_closure_remap_trampoline_table_page;
#ifdef __arm__
/* bdffc_closure_trampoline_table_page can be thumb-biased on some ARM archs */
trampoline_page_template &= ~1UL;
#endif
kt = vm_remap (mach_task_self (), &trampoline_page, PAGE_MAX_SIZE, 0x0,
VM_FLAGS_OVERWRITE, mach_task_self (), trampoline_page_template,
FALSE, &cur_prot, &max_prot, VM_INHERIT_SHARE);
if (kt != KERN_SUCCESS)
{
vm_deallocate (mach_task_self (), config_page, PAGE_MAX_SIZE * 2);
return NULL;
}


/* We have valid trampoline and config pages */
table = calloc (1, sizeof (ffi_trampoline_table));
table->free_count = FFI_REMAP_TRAMPOLINE_COUNT/2;
table->config_page = config_page;
table->trampoline_page = trampoline_page;

......
return table;
}

首先 ffi 在创建 trampoline 时,会分配两个连续的 page

trampoline page 会 remap 到我们事先在代码中汇编写的 ffi_closure_remap_trampoline_table_page。

其结构如图所示:

图片

当我们 ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), entry1)) 写入 closure 数据时, 会写入到 entry1 对应的 closuer1。

ffi_status
ffi_prep_closure_loc (ffi_closure *closure,
ffi_cif* cif,
void (*fun)(ffi_cif*,void*,void**,void*),
void *user_data,
void *codeloc)
{
......
if (cif->flags & AARCH64_FLAG_ARG_V)
start = ffi_closure_SYSV_V; // ffi 对 closure的处理函数
else
start = ffi_closure_SYSV;

void **config = (void**)((uint8_t *)codeloc - PAGE_MAX_SIZE);
config[0] = closure;
config[1] = start;
......
}
这是怎么对应到的呢? closure1 和 entry1 距离其所属 Page 的 offset 是一致的,通过 offset,成功建立 trampoline entry 和 trampoline closure 的对应关系。
现在我们知道这个关系,我们通过代码看一下到底在程序运行的时候 是怎么找到 closure 的。
这四条指令是我们 trampoline entry 的代码实现,就是 ffi 返回的 xxx_func_ptr
adr x16, -PAGE_MAX_SIZE
ldp x17, x16, [x16]
br x16
nop

通过 .rept 我们创建 PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE 个跳板,刚好一个页的大小


# 动态remap的 page
.align PAGE_MAX_SHIFT
CNAME(ffi_closure_remap_trampoline_table_page):
.rept PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE
# 这是我们的 trampoline entry, 就是ffi生成的函数指针
adr x16, -PAGE_MAX_SIZE // 将pc地址减去PAGE_MAX_SIZE, 找到 trampoine data entry
ldp x17, x16, [x16] // 加载我们写入的 closure, start 到 x17, x16
br x16 // 跳转到 start 函数
nop /* each entry in the trampoline config page is 2*sizeof(void*) so the trampoline itself cannot be smaller that 16 bytes */
.endr

通过 pc 地址减去 PAGE_MAX_SIZE 就找到对应的 trampoline data entry 了。

静态跳板的实现

由于代码段和数据段在不同的内存区域。

我们此时不能通过 像 vmremap 一样分配两个连续的 PAGE,在寻找 trampoline data entry 只是简单的-PAGE_MAX_SIZE 找到对应关系,需要稍微麻烦点的处理。

主要是通过 adrp 找到_ffi_static_trampoline_data_page1 和 _ffi_static_trampoline_page1的起始地址,用 pc-_ffi_static_trampoline_page1的起始地址计算 offset,找到 trampoline data entry。

# 静态分配的page
#ifdef __MACH__
#include <mach/machine/vm_param.h>

.align 14
.data
.global _ffi_static_trampoline_data_page1
_ffi_static_trampoline_data_page1:
.space PAGE_MAX_SIZE*5
.align PAGE_MAX_SHIFT
.text
CNAME(_ffi_static_trampoline_page1):

_ffi_local_forwarding_bridge:
adrp x17, ffi_closure_static_trampoline_table_page_start@PAGE;// text page
sub x16, x16, x17;// offset
adrp x17, _ffi_static_trampoline_data_page1@PAGE;// data page
add x16, x16, x17;// data address
ldp x17, x16, [x16];// x17 closure x16 start
br x16
nop
nop
.align PAGE_MAX_SHIFT
CNAME(ffi_closure_static_trampoline_table_page):

#这个label 用来adrp@PAGE 计算 trampoline 到 trampoline page的offset
#留了5个用来调试。
# 我们static trampoline 两条指令就够了,这里使用4个,和remap的保持一致
ffi_closure_static_trampoline_table_page_start:
adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop

// 5 * 4
.rept (PAGE_MAX_SIZE*5-5*4) / FFI_TRAMPOLINE_SIZE
adr x16, #0
b _ffi_local_forwarding_bridge
nop
nop
.endr

.globl CNAME(ffi_closure_static_trampoline_table_page)
FFI_HIDDEN(CNAME(ffi_closure_static_trampoline_table_page))
#ifdef __ELF__
.type CNAME(ffi_closure_static_trampoline_table_page), #function
.size CNAME(ffi_closure_static_trampoline_table_page), . - CNAME(ffi_closure_static_trampoline_table_page)
#endif
#endif


转自:https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247488493&idx=1&sn=e86780883d5c0cf3bb34a59ec753b4f3&chksm=e9d0d80fdea751196c807991cd46f5928f6828fe268268872ec3582b4fdcad086e1cebcab2d5&scene=178&cur_album_id=1590407423234719749#rd

收起阅读 »

iOS 上的相机捕捉

第一台 iPhone 问世就装有相机。在第一个 SKDs 版本中,在 app 里面整合相机的唯一方法就是使用 UIImagePickerController,但到了 iOS 4,发布了更灵活的 AVFoundation 框架。在这篇文章里,我们将会看...
继续阅读 »

第一台 iPhone 问世就装有相机。在第一个 SKDs 版本中,在 app 里面整合相机的唯一方法就是使用 UIImagePickerController,但到了 iOS 4,发布了更灵活的 AVFoundation 框架。

在这篇文章里,我们将会看到如何使用 AVFoundation 捕捉图像,如何操控相机,以及它在 iOS 8 的新特性。


概述

AVFoundation vs. UIImagePickerController

UIImagePickerController 提供了一种非常简单的拍照方法。它支持所有的基本功能,比如切换到前置摄像头,开关闪光灯,点击屏幕区域实现对焦和曝光,以及在 iOS 8 中像系统照相机应用一样调整曝光。

然而,当有直接访问相机的需求时,也可以选择 AVFoundation 框架。它提供了完全的操作权,例如,以编程方式更改硬件参数,或者操纵实时预览图。

AVFoundation 相关类

AVFoundation 框架基于以下几个类实现图像捕捉 ,通过这些类可以访问来自相机设备的原始数据并控制它的组件。

  • AVCaptureDevice 是关于相机硬件的接口。它被用于控制硬件特性,诸如镜头的位置、曝光、闪光灯等。
  • AVCaptureDeviceInput 提供来自设备的数据。
  • AVCaptureOutput 是一个抽象类,描述 capture session 的结果。以下是三种关于静态图片捕捉的具体子类:
    • AVCaptureStillImageOutput 用于捕捉静态图片
    • AVCaptureMetadataOutput 启用检测人脸和二维码
    • AVCaptureVideoOutput 为实时预览图提供原始帧
  • AVCaptureSession 管理输入与输出之间的数据流,以及在出现问题时生成运行时错误。
  • AVCaptureVideoPreviewLayer 是 CALayer 的子类,可被用于自动显示相机产生的实时图像。它还有几个工具性质的方法,可将 layer 上的坐标转化到设备上。它看起来像输出,但其实不是。另外,它拥有session (outputs 被 session 所拥有)。

设置

让我们看看如何捕获图像。首先我们需要一个 AVCaptureSession 对象:

let session = AVCaptureSession()

现在我们需要一个相机设备输入。在大多数 iPhone 和 iPad 中,我们可以选择后置摄像头或前置摄像头 -- 又称自拍相机 (selfie camera) -- 之一。那么我们必须先遍历所有能提供视频数据的设备 (麦克风也属于 AVCaptureDevice,因此略过不谈),并检查 position 属性:

let availableCameraDevices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo)
for device in availableCameraDevices as [AVCaptureDevice] {
if device.position == .Back {
backCameraDevice = device
}
else if device.position == .Front {
frontCameraDevice = device
}
}

然后,一旦我们发现合适的相机设备,我们就能获得相关的 AVCaptureDeviceInput 对象。我们会将它设置为 session 的输入:

var error:NSError?
let possibleCameraInput: AnyObject? = AVCaptureDeviceInput.deviceInputWithDevice(backCameraDevice, error: &error)
if let backCameraInput = possibleCameraInput as? AVCaptureDeviceInput {
if self.session.canAddInput(backCameraInput) {
self.session.addInput(backCameraInput)
}
}

注意当 app 首次运行时,第一次调用 AVCaptureDeviceInput.deviceInputWithDevice() 会触发系统提示,向用户请求访问相机。这在 iOS 7 的时候只有部分国家会有,到了 iOS 8 拓展到了所有地区。除非得到用户同意,否则相机的输入会一直是一个黑色画面的数据流。

对于处理相机的权限,更合适的方法是先确认当前的授权状态。要是在授权还没有确定的情况下 (也就是说用户还没有看过弹出的授权对话框时),我们应该明确地发起请求。

let authorizationStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
switch authorizationStatus {
case .NotDetermined:
// 许可对话没有出现,发起授权许可
AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo,
completionHandler: { (granted:Bool) -> Void in
if granted {
// 继续
}
else {
// 用户拒绝,无法继续
}
})
case .Authorized:
// 继续
case .Denied, .Restricted:
// 用户明确地拒绝授权,或者相机设备无法访问
}

如果能继续的话,我们会有两种方式来显示来自相机的图像流。最简单的就是,生成一个带有 AVCaptureVideoPreviewLayer 的 view,并使用 capture session 作为初始化参数。

previewLayer = AVCaptureVideoPreviewLayer.layerWithSession(session) as AVCaptureVideoPreviewLayer
previewLayer.frame = view.bounds
view.layer.addSublayer(previewLayer)

AVCaptureVideoPreviewLayer 会自动地显示来自相机的输出。当我们需要将实时预览图上的点击转换到设备的坐标系统中,比如点击某区域实现对焦时,这种做法会很容易办到。之后我们会看到具体细节。

第二种方法是从输出数据流捕捉单一的图像帧,并使用 OpenGL 手动地把它们显示在 view 上。这有点复杂,但是如果我们想要对实时预览图进行操作或使用滤镜的话,就是必要的了。

为获得数据流,我们需要创建一个 AVCaptureVideoDataOutput,这样一来,当相机在运行时,我们通过代理方法 captureOutput(_:didOutputSampleBuffer:fromConnection:) 就能获得所有图像帧 (除非我们处理太慢而导致掉帧),然后将它们绘制在一个 GLKView 中。不需要对 OpenGL 框架有什么深刻的理解,我们只需要这样就能创建一个 GLKView

glContext = EAGLContext(API: .OpenGLES2)
glView = GLKView(frame: viewFrame, context: glContext)
ciContext = CIContext(EAGLContext: glContext)

现在轮到 AVCaptureVideoOutput

videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: dispatch_queue_create("sample buffer delegate", DISPATCH_QUEUE_SERIAL))
if session.canAddOutput(self.videoOutput) {
session.addOutput(self.videoOutput)
}

以及代理方法:

func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
let image = CIImage(CVPixelBuffer: pixelBuffer)
if glContext != EAGLContext.currentContext() {
EAGLContext.setCurrentContext(glContext)
}
glView.bindDrawable()
ciContext.drawImage(image, inRect:image.extent(), fromRect: image.extent())
glView.display()
}

一个警告:这些来自相机的样本旋转了 90 度,这是由于相机传感器的朝向所导致的。AVCaptureVideoPreviewLayer 会自动处理这种情况,但在这个例子,我们需要对 GLKView 进行旋转。

马上就要搞定了。最后一个组件 -- AVCaptureStillImageOutput -- 实际上是最重要的,因为它允许我们捕捉静态图片。只需要创建一个实例,并添加到 session 里去:

stillCameraOutput = AVCaptureStillImageOutput()
if self.session.canAddOutput(self.stillCameraOutput) {
self.session.addOutput(self.stillCameraOutput)
}

配置

现在我们有了所有必需的对象,应该为我们的需求寻找最合适的配置。这里又有两种方法可以实现。最简单且最推荐是使用 session preset:

session.sessionPreset = AVCaptureSessionPresetPhoto

AVCaptureSessionPresetPhoto 会为照片捕捉选择最合适的配置,比如它可以允许我们使用最高的感光度 (ISO) 和曝光时间,基于相位检测 (phase detection)的自动对焦, 以及输出全分辨率的 JPEG 格式压缩的静态图片。

然而,如果你需要更多的操控,可以使用 AVCaptureDeviceFormat 这个类,它描述了一些设备使用的参数,比如静态图片分辨率,视频预览分辨率,自动对焦类型,感光度和曝光时间限制等。每个设备支持的格式都列在 AVCaptureDevice.formats 属性中,并可以赋值给 AVCaptureDevice 的 activeFormat (注意你并不能修改格式)。

操作相机

iPhone 和 iPad 中内置的相机或多或少跟其他相机有相同的操作,不同的是,一些参数如对焦、曝光时间 (在单反相机上的模拟快门的速度),感光度是可以调节,但是镜头光圈是固定不可调整的。到了 iOS 8,我们已经可以对所有这些可变参数进行手动调整了。

我们之后会看到细节,不过首先,该启动相机了:

sessionQueue = dispatch_queue_create("com.example.camera.capture_session", DISPATCH_QUEUE_SERIAL)
dispatch_async(sessionQueue) { () -> Void in
self.session.startRunning()
}

在 session 和相机设备中完成的所有操作和配置都是利用 block 调用的。因此,建议将这些操作分配到后台的串行队列中。此外,相机设备在改变某些参数前必须先锁定,直到改变结束才能解锁,例如:

var error:NSError?
if currentDevice.lockForConfiguration(&error) {
// 锁定成功,继续配置
// currentDevice.unlockForConfiguration()
}
else {
// 出错,相机可能已经被锁
}

对焦

在 iOS 相机上,对焦是通过移动镜片改变其到传感器之间的距离实现的。

自动对焦是通过相位检测和反差检测实现的。然而,反差检测只适用于低分辨率和高 FPS 视频捕捉 (慢镜头)。

AVCaptureFocusMode 是个枚举,描述了可用的对焦模式:

  • Locked 指镜片处于固定位置
  • AutoFocus 指一开始相机会先自动对焦一次,然后便处于 Locked 模式。
  • ContinuousAutoFocus 指当场景改变,相机会自动重新对焦到画面的中心点。

设置想要的对焦模式必须在锁定之后实施:

let focusMode:AVCaptureFocusMode = ...
if currentCameraDevice.isFocusModeSupported(focusMode) {
... // 锁定以进行配置
currentCameraDevice.focusMode = focusMode
... // 解锁
}
}

通常情况下,AutoFocus 模式会试图让屏幕中心成为最清晰的区域,但是也可以通过变换 “感兴趣的点 (point of interest)” 来设定另一个区域。这个点是一个 CGPoint,它的值从左上角 {0,0} 到右下角 {1,1}{0.5,0.5} 为画面的中心点。通常可以用视频预览图上的点击手势识别来改变这个点,想要将 view 上的坐标转化到设备上的规范坐标,我们可以使用 AVVideoCaptureVideoPreviewLayer.captureDevicePointOfInterestForPoint()

var pointInPreview = focusTapGR.locationInView(focusTapGR.view)
var pointInCamera = previewLayer.captureDevicePointOfInterestForPoint(pointInPreview)
...// 锁定,配置

// 设置感兴趣的点
currentCameraDevice.focusPointOfInterest = pointInCamera

// 在设置的点上切换成自动对焦
currentCameraDevice.focusMode = .AutoFocus

...// 解锁

在 iOS 8 中,有个新选项可以移动镜片的位置,从较近物体的 0.0 到较远物体的 1.0 (不是指无限远)。

... // 锁定,配置
var lensPosition:Float = ... // 0.0 到 1.0的float
currentCameraDevice.setFocusModeLockedWithLensPosition(lensPosition) {
(timestamp:CMTime) -> Void in
// timestamp 对应于应用了镜片位置的第一张图像缓存区
}
... // 解锁

这意味着对焦可以使用 UISlider 设置,这有点类似于旋转单反上的对焦环。当用这种相机手动对焦时,通常有一个可见的辅助标识指向清晰的区域。AVFoundation 里面没有内置这种机制,但是比如可以通过显示 "对焦峰值 (focus peaking)"(一种将已对焦区域高亮显示的方式) 这样的手段来补救。我们在这里不会讨论细节,不过对焦峰值可以很容易地实现,通过使用阈值边缘 (threshold edge) 滤镜 (用自定义 CIFilter 或 GPUImageThresholdEdgeDetectionFilter),并调用 AVCaptureAudioDataOutputSampleBufferDelegate下的 captureOutput(_:didOutputSampleBuffer:fromConnection:) 方法将它覆盖到实时预览图上。

曝光

在 iOS 设备上,镜头上的光圈是固定的 (在 iPhone 5s 以及其之后的光圈值是 f/2.2,之前的是 f/2.4),因此只有改变曝光时间和传感器的灵敏度才能对图片的亮度进行调整,从而达到合适的效果。至于对焦,我们可以选择连续自动曝光,在“感兴趣的点”一次性自动曝光,或者手动曝光。除了指定“感兴趣的点”,我们可以通过设置曝光补偿 (compensation) 修改自动曝光,也就是曝光档位的目标偏移。目标偏移在曝光档数里有讲到,它的范围在 minExposureTargetBias 与 maxExposureTargetBias 之间,0为默认值 (即没有“补偿”)。

var exposureBias:Float = ... // 在 minExposureTargetBias 和 maxExposureTargetBias 之间的值
... // 锁定,配置
currentDevice.setExposureTargetBias(exposureBias) { (time:CMTime) -> Void in
}
... // 解锁

使用手动曝光,我们可以设置 ISO 和曝光时间,两者的值都必须在设备当前格式所指定的范围内。

var activeFormat = currentDevice.activeFormat
var duration:CTime = ... //在activeFormat.minExposureDuration 和 activeFormat.maxExposureDuration 之间的值,或用 AVCaptureExposureDurationCurrent 表示不变
var iso:Float = ... // 在 activeFormat.minISO 和 activeFormat.maxISO 之间的值,或用 AVCaptureISOCurrent 表示不变
... // 锁定,配置
currentDevice.setExposureModeCustomWithDuration(duration, ISO: iso) { (time:CMTime) -> Void in
}
... // 解锁

如何知道照片曝光是否正确呢?我们可以通过 KVO,观察 AVCaptureDevice 的 exposureTargetOffset 属性,确认是否在 0 附近。

白平衡

数码相机为了适应不同类型的光照条件需要补偿。这意味着在冷光线的条件下,传感器应该增强红色部分,而在暖光线下增强蓝色部分。在 iPhone 相机中,设备会自动决定合适的补光,但有时也会被场景的颜色所混淆失效。幸运地是,iOS 8 可以里手动控制白平衡。

自动模式工作方式和对焦、曝光的方式一样,但是没有“感兴趣的点”,整张图像都会被纳入考虑范围。在手动模式,我们可以通过开尔文所表示的温度来调节色温和色彩。典型的色温值在 2000-3000K (类似蜡烛或灯泡的暖光源) 到 8000K (纯净的蓝色天空) 之间。色彩范围从最小的 -150 (偏绿) 到 150 (偏品红)。

温度和色彩可以被用于计算来自相机传感器的恰当的 RGB 值,因此仅当它们做了基于设备的校正后才能被设置。

以下是全部过程:

var incandescentLightCompensation = 3_000
var tint = 0 // 不调节
let temperatureAndTintValues = AVCaptureWhiteBalanceTemperatureAndTintValues(temperature: incandescentLightCompensation, tint: tint)
var deviceGains = currentCameraDevice.deviceWhiteBalanceGainsForTemperatureAndTintValues(temperatureAndTintValues)
... // 锁定,配置
currentCameraDevice.setWhiteBalanceModeLockedWithDeviceWhiteBalanceGains(deviceGains) {
(timestamp:CMTime) -> Void in
}
}
... // 解锁

实时人脸检测

AVCaptureMetadataOutput 可以用于检测人脸和二维码这两种物体。很明显,没什么人用二维码 (编者注: 因为在欧美现在二维码不是很流行,这里是一个恶搞。链接的这个 tumblr 博客的主题是 “当人们在扫二维码时的图片”,但是 2012 年开博至今没有任何一张图片,暗讽二维码根本没人在用,这和以中日韩为代表的亚洲用户群体的使用习惯完全相悖),因此我们就来看看如何实现人脸检测。我们只需通过 AVCaptureMetadataOutput的代理方法捕获的元对象:var metadataOutput = AVCaptureMetadataOutput()

metadataOutput.setMetadataObjectsDelegate(self, queue: self.sessionQueue)
if session.canAddOutput(metadataOutput) {
session.addOutput(metadataOutput)
}
metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeFace]
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
for metadataObject in metadataObjects as [AVMetadataObject] {
if metadataObject.type == AVMetadataObjectTypeFace {
var transformedMetadataObject = previewLayer.transformedMetadataObjectForMetadataObject(metadataObject)
}
}

捕捉静态图片

最后,我们要做的是捕捉高分辨率的图像,于是我们调用 captureStillImageAsynchronouslyFromConnection(connection, completionHandler)。在数据时被读取时,completion handler 将会在某个未指定的线程上被调用。

如果设置使用 JPEG 编码作为静态图片输出,不管是通过 session .Photo 预设设定的,还是通过设备输出设置设定的,sampleBuffer 都会返回包含图像的元数据。如果在 AVCaptureMetadataOutput 中是可用的话,这会包含 EXIF 数据,或是被识别的人脸等:

dispatch_async(sessionQueue) { () -> Void in

let connection = self.stillCameraOutput.connectionWithMediaType(AVMediaTypeVideo)

// 将视频的旋转与设备同步
connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIDevice.currentDevice().orientation.rawValue)!

self.stillCameraOutput.captureStillImageAsynchronouslyFromConnection(connection) {
(imageDataSampleBuffer, error) -> Void in

if error == nil {

// 如果使用 session .Photo 预设,或者在设备输出设置中明确进行了设置
// 我们就能获得已经压缩为JPEG的数据

let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataSampleBuffer)

// 样本缓冲区也包含元数据,我们甚至可以按需修改它

let metadata:NSDictionary = CMCopyDictionaryOfAttachments(nil, imageDataSampleBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate)).takeUnretainedValue()

if let image = UIImage(data: imageData) {
// 保存图片,或者做些其他想做的事情
...
}
}
else {
NSLog("error while capturing still image: \(error)")
}
}
}

当图片被捕捉的时候,有视觉上的反馈是很好的体验。想要知道何时开始以及何时结束的话,可以使用 KVO 来观察 AVCaptureStillImageOutput 的 isCapturingStillImage 属性。

分级捕捉

在 iOS 8 还有一个有趣的特性叫“分级捕捉”,可以在不同的曝光设置下拍摄几张照片。这在复杂的光线下拍照显得非常有用,例如,通过设定 -1、0、1 三个不同的曝光档数,然后用 HDR 算法合并成一张。

以下是代码实现:

dispatch_async(sessionQueue) { () -> Void in
let connection = self.stillCameraOutput.connectionWithMediaType(AVMediaTypeVideo)
connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIDevice.currentDevice().orientation.rawValue)!

var settings = [-1.0, 0.0, 1.0].map {
(bias:Float) -> AVCaptureAutoExposureBracketedStillImageSettings in

AVCaptureAutoExposureBracketedStillImageSettings.autoExposureSettingsWithExposureTargetBias(bias)
}

var counter = settings.count

self.stillCameraOutput.captureStillImageBracketAsynchronouslyFromConnection(connection, withSettingsArray: settings) {
(sampleBuffer, settings, error) -> Void in

...
// 保存 sampleBuffer(s)

// 当计数为0,捕捉完成
counter--

}
}

这很像是单个图像捕捉,但是不同的是 completion handler 被调用的次数和设置的数组的元素个数一样多。

总结

我们已经详细看到如何在 iPhone 应用里面实现拍照的基础功能(呃…不光是 iPhone,用 iPad 拍照其实也是不错的)。你也可以查看这个例子。最后说下,iOS 8 允许更精确的捕捉,特别是对于高级用户,这使得 iPhone 与专业相机之间的差距缩小,至少在手动控制上。不过,不是任何人都喜欢在日常拍照时使用复杂的手动操作界面,因此请合理地使用这些特性。


原文:https://objccn.io/issue-21-3/

收起阅读 »

iOS 柱状图一种实现思路

对于iOS柱状图,不是有什么难度的效果,有很多优秀的第三方库,比如AAChartKit、XYPieChart、PNChart、Charts等好多,不过这些类库大多封装的太厉害了,如果你的项目只是单纯的几个柱状图、那么使用这些库其实挺费劲的(学习成本+项目大小)...
继续阅读 »

对于iOS柱状图,不是有什么难度的效果,有很多优秀的第三方库,比如AAChartKitXYPieChartPNChartCharts等好多,不过这些类库大多封装的太厉害了,如果你的项目只是单纯的几个柱状图、那么使用这些库其实挺费劲的(学习成本+项目大小),下面说说我的思路。

iOS绘图以及图形处理主要使用的是Core Graphics/QuartZ 2D,这也是大部分人写柱状图的方法,即使用UIBezierPath配合Core Graphics实现。我的思路是使用UICollectionView,不过使用UICollectionView实现柱状图,最好需求能满足以下二点:

  • 1.柱状图的柱子够宽,最好有点击需求
  • 2.柱状图的柱子比较多,需要滑动,这个更能体现出Cell复用

当然,也并不是一定要满足上面2点,接下来用几个小Demo演示一下(注:Demo是Objective-C实现)

DemoA

这个是基本的效果,使用UICollectionViewFlowLayout布局,将scrollDirection设置为UICollectionViewScrollDirectionHorizontal;每个cell内部有个绿色的UIView,根据数值调整这个绿色UIView的高度,就是图上的效果了,其实核心就是UICollectionViewFlowLayout,后面几个Demo也全是基于此。

UICollectionViewFlowLayout *fw = [[UICollectionViewFlowLayout alloc] init];
fw.scrollDirection = UICollectionViewScrollDirectionHorizontal;
fw.minimumLineSpacing = 10;
fw.minimumInteritemSpacing = 0;
fw.itemSize = CGSizeMake(220, 30);
fw.headerReferenceSize = CGSizeMake(10, 220);
fw.footerReferenceSize = CGSizeMake(10, 220);
DemoB

这个效果是加了横坐标值和渐变Cell,每个柱状图重新出现屏幕上时,会动画出现,需要注意的是,渐变使用的是CAGradientLayer,但是对含有CAGradientLayer的view使用frame动画,会造成渐变的卡顿和动画的不流畅,所以这里是使用CAGradientLayer生成一张渐变图,设置成柱状图柱子的背景即可。

DemoC

这个效果是始终以中间的Cell为基准显示,点击其他Cell也会自动滚到中心。因为UICollectionView继承于UIScrollView,所以实现这种效果,关键在于两个代理方法:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset;

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
DemoD

这个效果的目的是:有的需求是柱状图比较密集,当手指滑动时又要求可以显示出对应柱子的值。其实实现起来很简单,就是使用touchesBegan:withEvent:以及touchesMoved:withEvent:等几个方法即可。

DemoE

这个是有柱状图的同时,还有曲线图,实现方法是在UICollectionView上面加了一个透明的UIView,同时通过此UIViewhitTest:withEvent:方法,将事件给到UICollectionView,再通过UICollectionView的代理方法,获取界面上的Cell,绘制曲线到UIView上。需要注意的是,UICollectionViewvisibleCells方法,获取到的Cell,顺序不是界面上的顺序,需要排序之后再使用。

其实通过UIViewhitTest:withEvent:方法,能做很多神奇的事情,大家可以自行研究。

DemoF

这个没啥,就是说明如果有复杂的坐标,也是可以实现的,这个Demo的做法是在UICollectionView下面有一个UIView专门绘制坐标系。

DemoG


这个其实跟柱状图没有关系,大家都知道,安卓的刷新和iOS不一样,下拉刷新分为侵入式非侵入式,对于iOS而言,由于UIScrollViewBounce效果,所以使用侵入式下拉刷新,成了最好的选择,但是iOS能否实现安卓那样的非侵入式刷新呢?于是本Demo就简单研究了一下,目前是存在bug的,样式也粗糙,不过思路应该没有问题,提供给大家,可以研究研究
1. 添加 UITableView
2. 在TableView上覆盖一个无背景色的UIScrollView
3. 覆写UIScrollView的几个touchesBegan、touchesEnded等几个方法,使其点击事件传递到TableView
4. 在UIScrollView的代理方法scrollViewDidScroll里处理
4.1 scrollView.contentOffset.y小于0,处理刷新动画和刷新逻辑
4.2 scrollView.contentOffset.y大于0,同步设置TableView的contentOffset 来保持滚动一致
5. 应该始终让scrollView和TableView的contentSize保持一致

至此,本文就没了,其实本文没啥技术含量,说白就是UICollectionView的使用,不过主要目的是给大家提供思路,具体需求还得具体分析。


链接:https://www.jianshu.com/p/087e8d96fcdc/
收起阅读 »

iOS功能强大的富文本编辑与显示框架 -- YYText

功能强大的 iOS 富文本编辑与显示框架。(该项目是 YYKit 组件之一)特性API 兼容 UILabel 和 UITextView支持高性能的异步排版和渲染扩展了 CoreText 的属性以支持更多文字效果支持 UIImage、UIVi...
继续阅读 »


功能强大的 iOS 富文本编辑与显示框架。
(该项目是 YYKit 组件之一)

特性

  • API 兼容 UILabel 和 UITextView
  • 支持高性能的异步排版和渲染
  • 扩展了 CoreText 的属性以支持更多文字效果
  • 支持 UIImage、UIView、CALayer 作为图文混排元素
  • 支持添加自定义样式的、可点击的文本高亮范围
  • 支持自定义文本解析 (内置简单的 Markdown/表情解析)
  • 支持文本容器路径、内部留空路径的控制
  • 支持文字竖排版,可用于编辑和显示中日韩文本
  • 支持图片和富文本的复制粘贴
  • 文本编辑时,支持富文本占位符
  • 支持自定义键盘视图
  • 撤销和重做次数的控制
  • 富文本的序列化与反序列化支持
  • 支持多语言,支持 VoiceOver
  • 支持 Interface Builder
  • 全部代码都有文档注释

架构

YYText 和 TextKit 架构对比


文本属性

YYText 原生支持的属性



YYText 支持的 CoreText 属性



用法

基本用法

// YYLabel (和 UILabel 用法一致)
YYLabel *label = [YYLabel new];
label.frame = ...
label.font = ...
label.textColor = ...
label.textAlignment = ...
label.lineBreakMode = ...
label.numberOfLines = ...
label.text = ...

// YYTextView (和 UITextView 用法一致)
YYTextView *textView = [YYTextView new];
textView.frame = ...
textView.font = ...
textView.textColor = ...
textView.dataDetectorTypes = ...
textView.placeHolderText = ...
textView.placeHolderTextColor = ...
textView.delegate = ...

属性文本

// 1. 创建一个属性文本
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text, blabla..."];

// 2. 为文本设置属性
text.yy_font = [UIFont boldSystemFontOfSize:30];
text.yy_color = [UIColor blueColor];
[text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
text.yy_lineSpacing = 10;

// 3. 赋值到 YYLabel 或 YYTextView
YYLabel *label = [YYLabel new];
label.frame = ...
label.attributedString = text;

YYTextView *textView = [YYTextView new];
textView.frame = ...
textView.attributedString = text;

文本高亮

你可以用一些已经封装好的简便方法来设置文本高亮:

[text yy_setTextHighlightRange:range
color:[UIColor blueColor]
backgroundColor:[UIColor grayColor]
tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
NSLog(@"tap text range:...");
}];

或者用更复杂的办法来调节文本高亮的细节:

// 1. 创建一个"高亮"属性,当用户点击了高亮区域的文本时,"高亮"属性会替换掉原本的属性
YYTextBorder *border = [YYTextBorder borderWithFillColor:[UIColor grayColor] cornerRadius:3];

YYTextHighlight *highlight = [YYTextHighlight new];
[highlight setColor:[UIColor whiteColor]];
[highlight setBackgroundBorder:highlightBorder];
highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
NSLog(@"tap text range:...");
// 你也可以把事件回调放到 YYLabel 和 YYTextView 来处理。
};

// 2. 把"高亮"属性设置到某个文本范围
[attributedText yy_setTextHighlight:highlight range:highlightRange];

// 3. 把属性文本设置到 YYLabel 或 YYTextView
YYLabel *label = ...
label.attributedText = attributedText

YYTextView *textView = ...
textView.attributedText = ...

// 4. 接受事件回调
label.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
NSLog(@"tap text range:...");
};
label.highlightLongPressAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
NSLog(@"long press text range:...");
};

@UITextViewDelegate
- (void)textView:(YYTextView *)textView didTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
NSLog(@"tap text range:...");
}
- (void)textView:(YYTextView *)textView didLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
NSLog(@"long press text range:...");
}

图文混排

NSMutableAttributedString *text = [NSMutableAttributedString new];
UIFont *font = [UIFont systemFontOfSize:16];
NSMutableAttributedString *attachment = nil;

// 嵌入 UIImage
UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"];
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
[text appendAttributedString: attachment];

// 嵌入 UIView
UISwitch *switcher = [UISwitch new];
[switcher sizeToFit];
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
[text appendAttributedString: attachment];

// 嵌入 CALayer
CASharpLayer *layer = [CASharpLayer layer];
layer.path = ...
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
[text appendAttributedString: attachment];

文本布局计算

NSAttributedString *text = ...
CGSize size = CGSizeMake(100, CGFLOAT_MAX);
YYTextLayout *layout = [YYTextLayout layoutWithContainerSize:size text:text];

// 获取文本显示位置和大小
layout.textBoundingRect; // get bounding rect
layout.textBoundingSize; // get bounding size

// 查询文本排版结果
[layout lineIndexForPoint:CGPointMake(10,10)];
[layout closestLineIndexForPoint:CGPointMake(10,10)];
[layout closestPositionToPoint:CGPointMake(10,10)];
[layout textRangeAtPoint:CGPointMake(10,10)];
[layout rectForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];
[layout selectionRectsForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];

// 显示文本排版结果
YYLabel *label = [YYLabel new];
label.size = layout.textBoundingSize;
label.textLayout = layout;

文本行位置调整

// 由于中文、英文、Emoji 等字体高度不一致,或者富文本中出现了不同字号的字体,
// 可能会造成每行文字的高度不一致。这里可以添加一个修改器来实现固定行高,或者自定义文本行位置。

// 简单的方法:
// 1. 创建一个文本行位置修改类,实现 `YYTextLinePositionModifier` 协议。
// 2. 设置到 Label 或 TextView。

YYTextLinePositionSimpleModifier *modifier = [YYTextLinePositionSimpleModifier new];
modifier.fixedLineHeight = 24;

YYLabel *label = [YYLabel new];
label.linePositionModifier = modifier;

// 完全控制:
YYTextLinePositionSimpleModifier *modifier = [YYTextLinePositionSimpleModifier new];
modifier.fixedLineHeight = 24;

YYTextContainer *container = [YYTextContainer new];
container.size = CGSizeMake(100, CGFLOAT_MAX);
container.linePositionModifier = modifier;

YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text];
YYLabel *label = [YYLabel new];
label.size = layout.textBoundingSize;
label.textLayout = layout;

异步排版和渲染

// 如果你在显示字符串时有性能问题,可以这样开启异步模式:
YYLabel *label = ...
label.displaysAsynchronously = YES;

// 如果需要获得最高的性能,你可以在后台线程用 `YYTextLayout` 进行预排版:
YYLabel *label = [YYLabel new];
label.displaysAsynchronously = YES; //开启异步绘制
label.ignoreCommonProperties = YES; //忽略除了 textLayout 之外的其他属性

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 创建属性字符串
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text"];
text.yy_font = [UIFont systemFontOfSize:16];
text.yy_color = [UIColor grayColor];
[text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];

// 创建文本容器
YYTextContainer *container = [YYTextContainer new];
container.size = CGSizeMake(100, CGFLOAT_MAX);
container.maximumNumberOfRows = 0;

// 生成排版结果
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text];

dispatch_async(dispatch_get_main_queue(), ^{
label.size = layout.textBoundingSize;
label.textLayout = layout;
});
});

文本容器控制

YYLabel *label = ...
label.textContainerPath = [UIBezierPath bezierPathWith...];
label.exclusionPaths = @[[UIBezierPath bezierPathWith...];,...];
label.textContainerInset = UIEdgeInsetsMake(...);
label.verticalForm = YES/NO;

YYTextView *textView = ...
textView.exclusionPaths = @[[UIBezierPath bezierPathWith...];,...];
textView.textContainerInset = UIEdgeInsetsMake(...);
textView.verticalForm = YES/NO;

文本解析

// 1. 创建一个解析器

// 内置简单的表情解析
YYTextSimpleEmoticonParser *parser = [YYTextSimpleEmoticonParser new];
NSMutableDictionary *mapper = [NSMutableDictionary new];
mapper[@":smile:"] = [UIImage imageNamed:@"smile.png"];
mapper[@":cool:"] = [UIImage imageNamed:@"cool.png"];
mapper[@":cry:"] = [UIImage imageNamed:@"cry.png"];
mapper[@":wink:"] = [UIImage imageNamed:@"wink.png"];
parser.emoticonMapper = mapper;

// 内置简单的 markdown 解析
YYTextSimpleMarkdownParser *parser = [YYTextSimpleMarkdownParser new];
[parser setColorWithDarkTheme];

// 实现 `YYTextParser` 协议的自定义解析器
MyCustomParser *parser = ...

// 2. 把解析器添加到 YYLabel 或 YYTextView
YYLabel *label = ...
label.textParser = parser;

YYTextView *textView = ...
textView.textParser = parser;

安装

CocoaPods

  1. 在 Podfile 中添加 pod 'YYText'
  2. 执行 pod install 或 pod update
  3. 导入 <YYText/YYText.h>。

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYText"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入 <YYText/YYText.h>。

手动安装

  1. 下载 YYText 文件夹内的所有内容。
  2. 将 YYText 内的源文件添加(拖放)到你的工程。
  3. 链接以下 frameworks:
    • UIKit
    • CoreFoundation
    • CoreText
    • QuartzCore
    • Accelerate
    • MobileCoreServices
  4. 导入 YYText.h

注意

你可以添加 YYImage 或 YYWebImage 到你的工程,以支持动画格式(GIF/APNG/WebP)的图片。


链接:https://github.com/ibireme/YYText


收起阅读 »

iOS中可定制性商品计数按钮-PPNumberButton

iOS中一款高度可定制性商品计数按钮,使用简单!支持自定义加/减按钮的标题内容、背景图片;支持设置边框颜色;支持使用键盘输入;支持长按加/减按钮快速加减;支持block回调与delegate(代理)回调;支持使用xib创建、直接在IB面板设置相关属性;支持设置...
继续阅读 »

iOS中一款高度可定制性商品计数按钮,使用简单!

  • 支持自定义加/减按钮的标题内容、背景图片;
  • 支持设置边框颜色;
  • 支持使用键盘输入;
  • 支持长按加/减按钮快速加减;
  • 支持block回调与delegate(代理)回调;
  • 支持使用xib创建、直接在IB面板设置相关属性;
  • 支持设置maxValue(最大值)与minValue(最小值).
  • 支持按钮自定义为京东/淘宝样式,饿了么/美团外卖/百度外卖样式;

Requirements 要求

  • iOS 7+
  • Xcode 8+

Installation 安装

1.手动安装:

下载DEMO后,将子文件夹PPNumberButton拖入到项目中, 导入头文件PPNumberButton.h开始使用.

2.CocoaPods安装:

first pod 'PPNumberButton' then pod install或pod install --no-repo-update`

如果发现pod search PPNumberButton 不是最新版本,在终端执行pod setup命令更新本地spec镜像缓存(时间可能有点长),重新搜索就OK了

Usage 使用方法

实例化方法

[[PPNumberButton alloc] init];:默认的frame为CGRectMake(0, 0, 110, 30) 或[[PPNumberButton alloc] initWithFrame:frame];

或 [PPNumberButton numberButtonWithFrame:frame];: 类方法创建

1.自定义加减按钮文字标题

PPNumberButton *numberButton = [PPNumberButton numberButtonWithFrame:CGRectMake(100, 100, 110, 30)];
// 开启抖动动画
numberButton.shakeAnimation = YES;
// 设置最小值
numberButton.minValue = 2;
// 设置最大值
numberButton.maxValue = 10;
// 设置输入框中的字体大小
numberButton.inputFieldFont = 23;
numberButton.increaseTitle = @"+";
numberButton.decreaseTitle = @"-";

numberButton.resultBlock = ^(NSString *num){
NSLog(@"%@",num);
};
[self.view addSubview:numberButton];

2.边框状态

PPNumberButton *numberButton = [PPNumberButton numberButtonWithFrame:CGRectMake(100, 160, 150, 30)];
//设置边框颜色
numberButton.borderColor = [UIColor grayColor];
numberButton.increaseTitle = @"+";
numberButton.decreaseTitle = @"-";
numberButton.resultBlock = ^(NSString *num){
NSLog(@"%@",num);
};
[self.view addSubview:numberButton];

3.自定义加减按钮背景图片

PPNumberButton *numberButton = [PPNumberButton numberButtonWithFrame:CGRectMake(100, 220, 100, 30)];
numberButton.shakeAnimation = YES;
numberButton.increaseImage = [UIImage imageNamed:@"increase_taobao"];
numberButton.decreaseImage = [UIImage imageNamed:@"decrease_taobao"];
numberButton.resultBlock = ^(NSString *num){
NSLog(@"%@",num);
};
[self.view addSubview:numberButton];

4.饿了么,美团外卖,百度外卖样式

PPNumberButton *numberButton = [PPNumberButton numberButtonWithFrame:CGRectMake(100, 280, 100, 30)];
// 初始化时隐藏减按钮
numberButton.decreaseHide = YES;
numberButton.increaseImage = [UIImage imageNamed:@"increase_meituan"];
numberButton.decreaseImage = [UIImage imageNamed:@"decrease_meituan"];
numberButton.resultBlock = ^(NSString *num){
NSLog(@"%@",num);
};
[self.view addSubview:numberButton];

使用xib创建

在控制器界面拖入UIView控件,在右侧的设置栏中将class名修改为PPNumberButton,按回车就OK了 (注意:如果通过Cocopods导入, 使用XIB/SB创建按钮会显示不全,还可能会报错.但APP可以编译运行,这应该是Cocopods或Xcode的问题)示例图 注意!如果有的同学将控件拖线到代码中,千万不要忘记在拖线的代码文件中导入 "PPNumberButton.h"头文件,否则会报错.


链接:https://github.com/jkpang/PPNumberButton

收起阅读 »

ios列表布局三方库--SwipeTableView

功能类似半糖首页菜单与QQ音乐歌曲列表页面。即支持UITableview的上下滚动,同时也支持不同列表之间的滑动切换。同时可以设置顶部header view与列表切换功能bar,使用方式类似于原生UITableview的tableHeaderView的方式。使...
继续阅读 »

功能类似半糖首页菜单与QQ音乐歌曲列表页面。即支持UITableview的上下滚动,同时也支持不同列表之间的滑动切换。同时可以设置顶部header view与列表切换功能bar,使用方式类似于原生UITableview的tableHeaderView的方式。

使用 Cocoapods 导入

pod 'SwipeTableView'

Mode 1


  1. 使用UICollectionView作为item的载体,实现左右滑动的功能。

  2. 在支持左右滑动之后,最关键的问题就是是滑动后相邻item的对齐问题。
为实现前后item对齐,需要在itemView重用的时候,比较前后两个itemView的contentOffset,然后设置后一个itemView的contentOffset与前一个相同。这样就实现了左右滑动后前后itemView的offset是对齐的。

3.由于多个item共用一个headerbar,所以,headerbar必须是根视图的子视图,即与CollectionView一样是SwipeTableView的子视图,并且在CollectionView的图层之上。

headr & bar的滚动与悬停实现是,对当前的itemView的contentOffset进行KVO。然后在当前itemView的contentOffset发生变化时,去改变header与bar的Y坐标值。


  1. 顶部header & bar在图层的最顶部,所以每个itemView的顶部需要做出一个留白来作为header & bar的显示空间。在Mode 1中,采用修改UIScrollViewcontentInsetstop值来留出顶部留白。

  2. 由于header在图层的最顶部,所以要实现滑动header的同时使当前itemView跟随滚动,需要根据headerframe的变化回调给当前的itemView来改变contentOffset,同时也要具有ScrollView的弹性等效果。

Mode 2

1.Mode 2中,基本结构与Mode 1一样,唯一的不同在于每个itemView顶部留白的的方式。


通过设置UITabelView的tableHeaderView,来提供顶部的占位留白,CollectionView采用自定义STCollectionView的collectionHeaderView来实现占位留白。(目前不支持UIScrollView)

2 如何设置区分Mode 1Mode 2模式?

正常条件下即为Mode 1模式;在SwipeTableView.h中或者在工程PCH文件中设置宏#define ST_PULLTOREFRESH_HEADER_HEIGHT xx设置为Mode 2模式。

使用用法

怎样使用?使用方式类似UITableView

实现 SwipeTableViewDataSource 代理的两个方法:

- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView

返回列表item的个数

- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view

使用的swipeHeaderView必须是STHeaderView及其子类的实例。

如何支持下拉刷新?

下拉刷新有两种实现方式,一种用户自定义下拉刷新组件(局部修改自定义),一种是简单粗暴设置宏:

1. 一行代码支持常用的下拉刷新控件,只需要在项目的PCH文件中或者在SwipeTableView.h文件中设置如下的宏:

#define ST_PULLTOREFRESH_HEADER_HEIGHT xx

上述宏中的xx要与您使用的第三方下拉刷新控件的refreshHeader高度相同:
MJRefresh 为 MJRefreshHeaderHeightSVPullToRefresh 为 SVPullToRefreshViewHeight(注:此时视图结构为Model 2

新增下拉刷新代理,可以控制每个item下拉临界高度,并自由控制每个item是否支持下拉刷新

- (BOOL)swipeTableView:(SwipeTableView *)swipeTableView shouldPullToRefreshAtIndex:(NSInteger)index

根据item所在index,设置item是否支持下拉刷新。在设置#define ST_PULLTOREFRESH_HEADER_HEIGHT xx的时候默认是YES(全部支持),否则默认为NO。

- (CGFloat)swipeTableView:(SwipeTableView *)swipeTableView heightForRefreshHeaderAtIndex:(NSInteger)index

返回对应item下拉刷新的临界高度,如果没有实现此代理,在设置#define ST_PULLTOREFRESH_HEADER_HEIGHT xx的时候默认是ST_PULLTOREFRESH_HEADER_HEIGHT的高度。如果没有设置宏,并且想要自定义修改下拉刷新,必须实现此代理,提供下拉刷新控件RefreshHeader的高度(RefreshHeader全部露出的高度),来通知SwipeTableView触发下拉刷新。

2. 如果想要更好的扩展性,以及喜欢自己研究的同学,可以尝试修改或者自定义下拉控件来解决下拉刷新的兼容问题,同时这里提供一些思路:

如果下拉刷新控件的frame是固定的(比如header的frame),这样可以在初始化下拉刷新的header或者在数据源的代理中重设下拉header的frame。

获取下拉刷新的header,将header的frame的y值减去swipeHeaderViewswipeHeaderBar的高度和(或者重写RefreshHeader的setFrame方法),就可以消除itemView contentInsets顶部留白top值的影响(否则添加的下拉header是隐藏在底部的)。

- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view {
...
STRefreshHeader * header = scrollView.header;
header.y = - (header.height + (swipeHeaderView.height + swipeHeaderBar.height));
...
}


or


- (instancetype)initWithFrame:(CGRect)frame {
...
STRefreshHeader * header = [STRefreshHeader headerWithRefreshingBlock:^(STRefreshHeader *header) {

}];
header.y = - (header.height + (swipeHeaderView.height + swipeHeaderBar.height));
scrollView.header = header;
...
}

对于一些下拉刷新控件,RefreshHeader的frame设置可能会在layoutSubviews中,所以,对RefreshHeader frame的修改,需要等执行完layouSubviews之后,在 有效的方法 中操作,比如:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
STRefreshHeader * header = self.header;
CGFloat orginY = - (header.height + self.swipeTableView.swipeHeaderView.height + self.swipeTableView.swipeHeaderBar.height);
if (header.y != orginY) {
header.y = orginY;
}
}

如何判断下拉刷新的控件的frame是不是固定不变的呢?

一是可以研究源码查看RefreshHeader的frame是否固定不变;另一个简单的方式是,在ScrollView的滚动代理中log RefreshHeader的frame(大部分的下拉控件的frame都是固定的)。

如果使用的下拉刷新控件的frame是变化的(个人感觉极少数),那么只能更深层的修改下拉刷新控件或者自定义下拉刷新。也可以更直接的采用第一种设置宏的方式支持下拉刷新。

混合模式(UItableView & UICollectionView & UIScrollView)


  1. Mode 1模式下,属于最基本的模式,可扩展性也是最强的,此时,支持UITableViewUICollectionViewUIScrollView如果,同时设置shouldAdjustContentSizeYES,实现自适应contentSize,在UICollectionView内容不足的添加下,只能使用STCollectionView及其子类

    UICollectionView不支持通过contentSize属性设置contentSize


  2. Mode 2模式下,SwipeTableView支持的collectionView必须是STCollectionView及其子类的实例,目前,不支持UIScrollView

示例代码:

初始化并设置header与bar

self.swipeTableView = [[SwipeTableView alloc]initWithFrame:[UIScreen mainScreen].bounds];
_swipeTableView.delegate = self;
_swipeTableView.dataSource = self;
_swipeTableView.shouldAdjustContentSize = YES;
_swipeTableView.swipeHeaderView = self.tableViewHeader;
_swipeTableView.swipeHeaderBar = self.segmentBar;

实现数据源代理:

- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView {
return 4;
}

- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view {
UITableView * tableView = view;
if (nil == tableView) {
UITableView * tableView = [[UITableView alloc]initWithFrame:swipeView.bounds style:UITableViewStylePlain];
tableView.backgroundColor = [UIColor whiteColor];
...
}
// 这里刷新每个item的数据
[tableVeiw refreshWithData:dataArray];
...
return tableView;
}

STCollectionView使用方法:

MyCollectionView.h

@interface MyCollectionView : STCollectionView

@property (nonatomic, assign) NSInteger numberOfItems;
@property (nonatomic, assign) BOOL isWaterFlow;

@end



MyCollectionView.m

- (instancetype)initWithFrame:(CGRect)frame {

self = [super initWithFrame:frame];
if (self) {
STCollectionViewFlowLayout * layout = self.st_collectionViewLayout;
layout.minimumInteritemSpacing = 5;
layout.minimumLineSpacing = 5;
layout.sectionInset = UIEdgeInsetsMake(5, 5, 5, 5);
self.stDelegate = self;
self.stDataSource = self;
[self registerClass:UICollectionViewCell.class forCellWithReuseIdentifier:@"item"];
[self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header"];
[self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footer"];
}
return self;
}


- (NSInteger)collectionView:(UICollectionView *)collectionView layout:(STCollectionViewFlowLayout *)layout numberOfColumnsInSection:(NSInteger)section {
return _numberOfColumns;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(0, 100);
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
return CGSizeMake(kScreenWidth, 35);
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
return CGSizeMake(kScreenWidth, 35);
}

- (UICollectionReusableView *)stCollectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
UICollectionReusableView * reusableView = nil;
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header" forIndexPath:indexPath];
// custom UI......
}else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footer" forIndexPath:indexPath];
// custom UI......
}
return reusableView;
}

- (NSInteger)numberOfSectionsInStCollectionView:(UICollectionView *)collectionView {
return _numberOfSections;
}

- (NSInteger)stCollectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _numberOfItems;
}

- (UICollectionViewCell *)stCollectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"item" forIndexPath:indexPath];
// do something .......
return cell;
}

如果STCollectionViewFlowLayout已经不能满足UICollectionView的布局的话,用户自定义的flowlayout需要继承自STCollectionViewFlowLayout,并在重写相应方法的时候需要调用父类方法,并需要遵循一定规则,如下:

- (void)prepareLayout {
[super prepareLayout];
// do something in sub class......
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray * superAttrs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray * itemAttrs = [superAttrs mutableCopy];

// filter subClassAttrs to rect
NSArray * filteredSubClassAttrs = ........;

[itemAttrs addObjectsFromArray:fittesSubClassAttrs];

return itemAttrs;
}

- (CGSize)collectionViewContentSize {
CGSize superSize = [super collectionViewContentSize];

CGSize subClassSize = .......;
subClassSize.height += superSize.height;

// fit mincontentSize
STCollectionView * collectionView = (STCollectionView *)self.collectionView;
subClassSize.height = fmax(subClassSize.height, collectionView.minRequireContentSize.height);

return subClassSize;
}

使用的详细用法在SwipeTableViewDemo文件夹中,提供了五种示例:

  • SingleOneKindView
    数据源提供的是单一类型的itemView,这里默认提供的是 CustomTableView (UITableView的子类),并且每一个itemView的数据行数有多有少,因此在滑动到数据少的itemView时,再次触碰界面,当前的itemView会有回弹的动作(由于contentSize小的缘故)。

  • HybridItemViews
    数据源提供的itemView类型是混合的,即 CustomTableView 与 CustomCollectionViewUICollectionView的子类)。

  • `AdjustContentSize` 自适应调整cotentOffszie属性,这里不同的itemView的数据行数有多有少,当滑动到数据较少的itemView时,再次触碰界面并不会导致当前itemView的回弹,这里当前数据少的itemView已经做了最小contentSize的设置。

    在0.2.3版本中去除了 demo 中的这一模块,默认除了`SingleOneKindView`模式下全部是自适应 contentSize。
  • DisabledBarScroll
    取消顶部控制条的跟随滚动,只有在swipeHeaderView是nil的条件下才能生效。这样可以实现一个类似网易新闻首页的滚动菜单列表的布局。

  • HiddenNavigationBar 隐藏导航。自定义了一个返回按钮(支持手势滑动返回)。

  • Demo支持添加移除header(定义的UIImageView)与bar(自定义的 CutomSegmentControl)的功能。

  • 示例代码新增点击图片全屏查看。

  • Demo中提供简单的自定义下拉刷新控件STRefreshHeader,供参考


    链接:https://github.com/Roylee-ML/SwipeTableView










收起阅读 »

iOS 图片浏览器 (支持视频)-YBImageBrowser

iOS 图片浏览器,功能强大,易于拓展,性能优化和内存控制让其运行更加的流畅和稳健。一.特性支持 GIF,APNG,WebP 等本地和网络图片类型(由 YYImage、SDWebImage 提供支持)。支持系统相册图片和视频。支持简单的视频播放。支持高清图浏览...
继续阅读 »

iOS 图片浏览器,功能强大,易于拓展,性能优化和内存控制让其运行更加的流畅和稳健。

一.特性

  • 支持 GIF,APNG,WebP 等本地和网络图片类型(由 YYImage、SDWebImage 提供支持)。
  • 支持系统相册图片和视频。
  • 支持简单的视频播放。
  • 支持高清图浏览。
  • 支持图片预处理(比如添加水印)。
  • 支持根据图片的大小判断是否需要预先解码(精确控制内存)。
  • 支持图片压缩、裁剪的界限设定。
  • 支持修改下载图片的 NSURLRequest。
  • 支持主动旋转或跟随控制器旋转。
  • 支持自定义图标。
  • 支持自定义 Toast/Loading。
  • 支持自定义文案(默认提供中文和英文)。
  • 支持自定义工具视图(比如查看原图功能)。
  • 支持自定义 Cell(比如添加一个广告模块)。
  • 支持添加到其它父视图上使用(比如加到控制器上)。
  • 支持转场动效、图片布局等深度定制。
  • 支持数据重载、局部更新。
  • 支持低粒度的内存控制和性能调优。
  • 极致的性能优化和严格的内存控制让其运行更加的流畅和稳健。

二.安装

CocoaPods

支持分库导入,核心部分就是图片浏览功能,视频播放作为拓展功能按需导入。

1.在 Podfile 中添加:

pod 'YBImageBrowser'
pod 'YBImageBrowser/Video' //视频功能需添加

2.执行 pod install pod update

3.导入 <YBImageBrowser/YBImageBrowser.h>,视频功能需导入<YBImageBrowser/YBIBVideoData.h>。

4.注意:如果你需要支持 WebP,可以在 Podfile 中添加 pod 'YYImage/WebP'。

若搜索不到库,可执行pod repo update,或使用 rm ~/Library/Caches/CocoaPods/search_index.json 移除本地索引然后再执行安装,或更新一下 CocoaPods 版本。

去除 SDWebImage 的依赖(版本需 >= 3.0.4)

Podfile 相应的配置变为:

pod 'YBImageBrowser/NOSD'
pod 'YBImageBrowser/VideoNOSD' //视频功能需添加

这时你必须定义一个类实现YBIBWebImageMediator协议,并赋值给YBImageBrowser类的webImageMediator属性(可以参考 YBIBDefaultWebImageMediator的实现)。

手动导入


  1. 下载 YBImageBrowser 文件夹所有内容并且拖入你的工程中,视频功能还需下载 Video 文件夹所有内容。

  2. 链接以下 frameworks:


  • SDWebImage

  • YYImage


  1. 导入 YBImageBrowser.h,视频功能需导入YBIBVideoData.h

  2. 注意:如果你需要支持 WebP,可以在 Podfile 中添加 pod 'YYImage/WebP',或者到手动下载 YYImage 仓库 webP 支持文件。

用法


初始化YBImageBrowser并且赋值数据源id<YBIBDataProtocol>,默认提供YBIBImageData (图片) 和YBIBVideoData (视频) 两种数据源。

图片处理是组件的核心,笔者精力有限,视频播放做得很轻量,若有更高的要求最好是自定义 Cell,望体谅。

Demo 中提供了很多示例代码,演示较复杂的拓展方式,所以若需要深度定制最好是下载 Demo 查看。

建议不对YBImageBrowser进行复用,目前还存在一些逻辑漏洞。

基本使用

// 本地图片
YBIBImageData *data0 = [YBIBImageData new];
data0.imageName = ...;
data0.projectiveView = ...;

// 网络图片
YBIBImageData *data1 = [YBIBImageData new];
data1.imageURL = ...;
data1.projectiveView = ...;

// 视频
YBIBVideoData *data2 = [YBIBVideoData new];
data2.videoURL = ...;
data2.projectiveView = ...;

YBImageBrowser *browser = [YBImageBrowser new];
browser.dataSourceArray = @[data0, data1, data2];
browser.currentPage = ...;
[browser show];

设置支持的旋转方向

当图片浏览器依托的 UIViewController 仅支持一个方向:

这种情况通过YBImageBrowser.new.supportedOrientations设置图片浏览器支持的旋转方向。

否则:

上面的属性将失效,图片浏览器会跟随控制器的旋转而旋转,由于各种原因这种情况的旋转过渡有瑕疵,建议不使用这种方式。

自定义图标

修改YBIBIconManager.sharedManager实例的属性。

自定义文案

修改YBIBCopywriter.sharedCopywriter实例的属性。

自定义 Toast / Loading

实现YBIBAuxiliaryViewHandler协议,并且赋值给YBImageBrowser.new.auxiliaryViewHandler属性,可参考和协议同名的默认实现类。

自定义工具视图(ToolView)

默认实现的YBImageBrowser.new.defaultToolViewHandler处理器可以做一些属性配置,当满足不了业务需求时,最好是进行自定义,参考默认实现或 Demo 中“查看原图”功能实现。

定义一个或多个类实现YBIBToolViewHandler协议,并且装入YBImageBrowser.new.toolViewHandlers数组属性。建议使用一个中介者来实现这个协议,然后所有的工具视图都由这个中介者来管理,当然也可以让每一个自定义的工具 UIView 都实现YBIBToolViewHandler协议,请根据具体需求取舍。

自定义 Cell

当默认提供的YBIBImageData (图片) 和YBIBVideoData (视频) 满足不了需求时,可自定义拓展 Cell,参考默认实现或 Demo 中的示例代码。

定义一个实现YBIBCellProtocol协议的UICollectionViewCell类和一个实现YBIBDataProtocol协议的数据类,当要求不高时实现必选协议方法就能跑起来了,若对交互有要求就相对比较复杂,最好是参考默认的交互动效实现。

在某些场景下,甚至可以直接继承项目中的 Cell 来做自定义。

常见问题

SDWebImage Pods 版本兼容问题

SDWebImage 有两种情况会出现兼容问题:该库对 SDWebImage 采用模糊向上依赖,但将来 SDWebImage 可能没做好向下兼容;当其它库依赖 SDWebImage 更低或更高 API 不兼容版本。对于这种情况,可以尝试以下方式解决:

  • Podfile 中采用去除 SDWebImage 依赖的方式导入,只需要实现一个中介者(见安装部分)。
  • 更改其它库对 SDWebImage 的依赖版本。
  • 手动导入 YBImageBrowser,然后修改YBIBDefaultWebImageMediator文件。

为什么不去除依赖 SDWebImage 自己实现?时间成本太高。 为什么不拖入 SDWebImage 修改类名?会扩大组件的体积,若外部有 SDWebImage 就存在一份多余代码。

依赖的 YYImage 与项目依赖的 YYKit 冲突

实际上 YYKit 有把各个组件拆分出来,建议项目中分开导入

pod 'YYModel'
pod 'YYCache'
pod 'YYImage'
pod 'YYWebImage'
pod 'YYText'
...

而且这样更灵活便于取舍。

低内存设备 OOM 问题

组件内部会降低在低内存设备上的性能,减小内存占用,但若高清图过多,可能需要手动去控制(以下是硬件消耗很低的状态):

YBIBImageData *data = YBIBImageData.new;
// 取消预解码
data.shouldPreDecodeAsync = NO;
// 直接设大触发裁剪比例,绘制更小的裁剪区域压力更小,不过可能会让用户感觉奇怪,放很大才开始裁剪显示高清局部(这个属性很多时候不需要显式设置,内部会动态计算)
data.cuttingZoomScale = 10;

YBImageBrowser *browser = YBImageBrowser.new;
// 调低图片的缓存数量
browser.ybib_imageCache.imageCacheCountLimit = 1;
// 预加载数量设为 0
browser.preloadCount = 0;

视频播放功能简陋

关于大家提的关于视频的需求,有些成本过高,笔者精力有限望体谅。若组件默认的视频播放器满足不了需求,就自定义一个 Cell 吧,把成熟的播放器集成到组件中肯定更加的稳定

链接:https://github.com/indulgeIn/YBImageBrowser


收起阅读 »

iOS基于二进制文件重排的解决方案 APP启动速度提升超15%!

背景启动是App给用户的第一印象,对用户体验至关重要。业务迭代迅速,如果放任不管,启动速度会一点点劣化。为此iOS客户端团队做了大量优化工作,除了传统的修改业务代码方式,我们还做了些开拓性的探索,发现修改代码在二进制文件的布局可以提高启动性能,方案落地后在上启...
继续阅读 »

背景

启动是App给用户的第一印象,对用户体验至关重要。业务迭代迅速,如果放任不管,启动速度会一点点劣化。为此iOS客户端团队做了大量优化工作,除了传统的修改业务代码方式,我们还做了些开拓性的探索,发现修改代码在二进制文件的布局可以提高启动性能,方案落地后在上启动速度提高了约15%。

本文从原理出发,介绍了我们是如何通过静态扫描和运行时trace找到启动时候调用的函数,然后修改编译参数完成二进制文件的重新排布。

原理

Page Fault

进程如果能直接访问物理内存无疑是很不安全的,所以操作系统在物理内存的上又建立了一层虚拟内存。为了提高效率和方便管理,又对虚拟内存和物理内存又进行分页(Page)。当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次缺页中断(Page Fault),分配物理内存,有需要的话会从磁盘mmap读人数据。

通过App Store渠道分发的App,Page Fault还会进行签名验证,所以一次Page Fault的耗时比想象的要多:



Page Fault

重排

编译器在生成二进制代码的时候,默认按照链接的Object File(.o)顺序写文件,按照Object File内部的函数顺序写函数。

静态库文件.a就是一组.o文件的ar包,可以用ar -t查看.a包含的所有.o。




默认布局

简化问题:假设我们只有两个page:page1/page2,其中绿色的method1和method3启动时候需要调用,为了执行对应的代码,系统必须进行两个Page Fault。

但如果我们把method1和method3排布到一起,那么只需要一个Page Fault即可,这就是二进制文件重排的核心原理。





重排之后

我们的经验是优化一个Page Fault,启动速度提升0.6~0.8ms。

核心问题

为了完成重排,有以下几个问题要解决:

  • 重排效果怎么样 - 获取启动阶段的page fault次数

  • 重排成功了没 - 拿到当前二进制的函数布局

  • 如何重排 - 让链接器按照指定顺序生成Mach-O

  • 重排的内容 - 获取启动时候用到的函数

System Trace

日常开发中性能分析是用最多的工具无疑是Time Profiler,但Time Profiler是基于采样的,并且只能统计线程实际在运行的时间,而发生Page Fault的时候线程是被blocked,所以我们需要用一个不常用但功能却很强大的工具:System Trace。

选中主线程,在VM Activity中的File Backed Page In次数就是Page Fault次数,并且双击还能按时序看到引起Page Fault的堆栈:







System Trace

signpost

现在我们在Instrument中已经能拿到某个时间段的Page In次数,那么如何和启动映射起来呢?

我们的答案是:os_signpost

os_signpost是iOS 12开始引入的一组API,可以在Instruments绘制一个时间段,代码也很简单:


1os_log_t logger = os_log_create("com.bytedance.tiktok", "performance");2os_signpost_id_t signPostId = os_signpost_id_make_with_pointer(logger,sign);3//标记时间段开始4os_signpost_interval_begin(logger, signPostId, "Launch","%{public}s", "");5//标记结束6os_signpost_interval_end(logger, signPostId, "Launch");

通常可以把启动分为四个阶段处理:



启动阶段

有多少个Mach-O,就会有多少个Load和C++静态初始化阶段,用signpost相关API对对应阶段打点,方便跟踪每个阶段的优化效果。

Linkmap

Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File:


Build Settings

比如以下是一个单页面Demo项目的linkmap。



linkmap

linkmap主要包括三大部分:

  • Object Files 生成二进制用到的link单元的路径和文件编号

  • Sections 记录Mach-O每个Segment/section的地址范围

  • Symbols 按顺序记录每个符号的地址范围

ld

Xcode使用的链接器件是ld,ld有一个不常用的参数-order_file,通过man ld可以看到详细文档:

Alters the order in which functions and data are laid out. For each section in the output file, any symbol in that section that are specified in the order file file is moved to the start of its section and laid out in the same order as in the order file file.

可以看到,order_file中的符号会按照顺序排列在对应section的开始,完美的满足了我们的需求。

Xcode的GUI也提供了order_file选项:




order_file

如果order_file中的符号实际不存在会怎么样呢?

ld会忽略这些符号,如果提供了link选项-order_file_statistics,会以warning的形式把这些没找到的符号打印在日志里。

获得符号

还剩下最后一个,也是最核心的一个问题,获取启动时候用到的函数符号。

我们首先排除了解析Instruments(Time Profiler/System Trace) trace文件方案,因为他们都是基于特定场景采样的,大多数符号获取不到。最后选择了静态扫描+运行时Trace结合的解决方案。

Load

Objective C的符号名是+-[Class_name(category_name) method:name:],其中+表示类方法,-表示实例方法。

刚刚提到linkmap里记录了所有的符号名,所以只要扫一遍linkmap的__TEXT,__text,正则匹配("^\+\[.*\ load\]$")既可以拿到所有的load方法符号。

C++静态初始化

C++并不像Objective C方法那样,大部分方法调用编译后都是objc_msgSend,也就没有一个入口函数去运行时hook。

但是可以用-finstrument-functions在编译期插桩“hook”,但由于APP很多依赖由其他团队提供静态库,这套方案需要修改依赖的构建过程。二进制文件重排在没有业界经验可供参考,不确定收益的情况下,选择了并不完美但成本最低的静态扫描方案。

1. 扫描linkmap的__DATA,__mod_init_func,这个section存储了包含C++静态初始化方法的文件,获得文件号[ 5]


1//__mod_init_func20x100008060    0x00000008  [  5] ltmp73//[  5]对应的文件4[  5] .../Build/Products/Debug-iphoneos/libStaticLibrary.a(StaticLibrary.o)

2. 通过文件号,解压出.o。

1➜  lipo libStaticLibrary.a -thin arm64 -output arm64.a2ar -x arm64.a StaticLibrary.o

3. 通过.o,获得静态初始化的符号名_demo_constructor

1  objdump -r -section=__mod_init_func StaticLibrary.o23StaticLibrary.o:    file format Mach-O arm6445RELOCATION RECORDS FOR [__mod_init_func]:60000000000000000 ARM64_RELOC_UNSIGNED _demo_constructor

4. 通过符号名,文件号,在linkmap中找到符号在二进制中的范围:

10x100004A30    0x0000001C  [  5] _demo_constructor

5. 通过起始地址,对代码进行反汇编:

 1  objdump -d --start-address=0x100004A30 --stop-address=0x100004A4B demo_arm64  2 3_demo_constructor: 4100004a30:    fd 7b bf a9     stp x29, x30, [sp, #-16]! 5100004a34:    fd 03 00 91     mov x29, sp 6100004a38:    20 0c 80 52     mov w0, #97 7100004a3c:    da 06 00 94     bl  #7016  8100004a40:    40 0c 80 52     mov w0, #98 9100004a44:    fd 7b c1 a8     ldp x29, x30, [sp], #1610100004a48:    d7 06 00 14     b   #7004 

6. 通过扫描bl指令扫描子程序调用,子程序在二进制的开始地址为:100004a3c +1b68(对应十进制的7016)。

1100004a3c:    da 06 00 94     bl  #7016 

7. 通过开始地址,可以找到符号名和结束地址,然后重复5~7,递归的找到所有的子程序调用的函数符号。


小坑

STL里会针对string生成初始化函数,这样会导致多个.o里存在同名的符号,例如:

1__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1IDnEEPKc

类似这样的重复符号的情况在C++里有很多,所以C/C++符号在order_file里要带着所在的.o信息:

1//order_file.txt2libDemoLibrary.a(object.o):__GLOBAL__sub_I_demo_file.cpp

局限性

branch系列汇编指令除了bl/b,还有br/blr,即通过寄存器的间接子程序调用,静态扫描无法覆盖到这种情况。

Local符号

在做C++静态初始化扫描的时候,发现扫描出了很多类似l002的符号。经过一番调研,发现是依赖方输出静态库的时候裁剪了local符号。导致__GLOBAL__sub_I_demo_file.cpp 变成了l002。

需要静态库出包的时候保留local符号,CI脚本不要执行strip -x,同时Xcode对应target的Strip Style修改为Debugging symbol:








Strip Style

静态库保留的local符号会在宿主App生成IPA之前裁剪掉,所以不会对最后的IPA包大小有影响。宿主App的Strip Style要选择All Symbols,宿主动态库选择Non-Global Symbols。

Objective C方法

绝大部分Objective C的方法在编译后会走objc_msgSend,所以通过fishhook(https://github.com/facebook/fishhook) hook这一个C函数即可获得Objective C符号。由于objc_msgSend是变长参数,所以hook代码需要用汇编来实现:


 1//代码参考InspectiveC 2__attribute__((__naked__)) 3static void hook_Objc_msgSend() { 4    save() 5    __asm volatile ("mov x2, lr\n"); 6    __asm volatile ("mov x3, x4\n"); 7    call(blr, &before_objc_msgSend) 8    load() 9    call(blr, orig_objc_msgSend)10    save()11    call(blr, &after_objc_msgSend)12    __asm volatile ("mov lr, x0\n");13    load()14    ret()15}


子程序调用时候要保存和恢复参数寄存器,所以save和load分别对x0~x9, q0~q9入栈/出栈。call则通过寄存器来间接调用函数:


 1#define save() \ 2__asm volatile ( \ 3"stp q6, q7, [sp, #-32]!\n"\ 4... 5 6#define load() \ 7__asm volatile ( \ 8"ldp x0, x1, [sp], #16\n" \ 9...1011#define call(b, value) \12__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \13__asm volatile ("mov x12, %0\n" :: "r"(value)); \14__asm volatile ("ldp x8, x9, [sp], #16\n"); \15__asm volatile (#b " x12\n");


before_objc_msgSend中用栈保存lr,在after_objc_msgSend恢复lr。由于要生成trace文件,为了降低文件的大小,直接写入的是函数地址,且只有当前可执行文件的Mach-O(app和动态库)代码段才会写入:

iOS中,由于ALSR(https://en.wikipedia.org/wiki/Address_space_layout_randomization)的存在,在写入之前需要先减去偏移量slide:

1IMP imp = (IMP)class_getMethodImplementation(object_getClass(self), _cmd);2unsigned long imppos = (unsigned long)imp;3unsigned long addr = immpos - macho_slide

获取一个二进制的__text段地址范围:

1unsigned long size = 0;2unsigned long start = (unsigned long)getsectiondata(mhp,  "__TEXT", "__text", &size);3unsigned long end = start + size;

获取到函数地址后,反查linkmap既可找到方法的符号名。

Block

block是一种特殊的单元,block在编译后的函数体是一个C函数,在调用的时候直接通过指针调用,并不走objc_msgSend,所以需要单独hook。

通过Block的源码可以看到block的内存布局如下:

 1struct Block_layout { 2    void *isa; 3    int32_t flags; // contains ref count 4    int32_t reserved; 5    void  *invoke; 6    struct Block_descriptor1 *descriptor; 7}; 8struct Block_descriptor1 { 9    uintptr_t reserved;10    uintptr_t size;11};
其中invoke就是函数的指针,hook思路是将invoke替换为自定义实现,然后在reserved保存为原始实现。

1//参考 https://github.com/youngsoft/YSBlockHook2if (layout->descriptor != NULL && layout->descriptor->reserved == NULL)3{4    if (layout->invoke != (void *)hook_block_envoke)5    {6        layout->descriptor->reserved = layout->invoke;7        layout->invoke = (void *)hook_block_envoke;8    }9}

由于block对应的函数签名不一样,所以这里仍然采用汇编来实现hook_block_envoke

 1__attribute__((__naked__)) 2static void hook_block_envoke() { 3    save() 4    __asm volatile ("mov x1, lr\n"); 5    call(blr, &before_block_hook); 6    __asm volatile ("mov lr, x0\n"); 7    load() 8    //调用原始的invoke,即resvered存储的地址 9    __asm volatile ("ldr x12, [x0, #24]\n");10    __asm volatile ("ldr x12, [x12]\n");11    __asm volatile ("br x12\n");12}

before_block_hook中获得函数地址(同样要减去slide)。

1intptr_t before_block_hook(id block,intptr_t lr)2{3    Block_layout * layout = (Block_layout *)block;4    //layout->descriptor->reserved即block的函数地址5    return lr;6}

同样,通过函数地址反查linkmap既可找到block符号。


瓶颈

基于静态扫描+运行时trace的方案仍然存在少量瓶颈:

  • initialize hook不到

  • 部分block hook不到

  • C++通过寄存器的间接函数调用静态扫描不出来

目前的重排方案能够覆盖到80%~90%的符号,未来我们会尝试编译期插桩等方案来进行100%的符号覆盖,让重排达到最优效果。

整体流程

流程

  1. 设置条件触发流程

  2. 工程注入Trace动态库,选择release模式编译出.app/linkmap/中间产物

  3. 运行一次App到启动结束,Trace动态库会在沙盒生成Trace log

  4. 以Trace Log,中间产物和linkmap作为输入,运行脚本解析出order_file

总结

目前,在缺少业界经验参考的情况下,我们成功验证了二进制文件重排方案在iOS APP开发中的可行性和稳定性。基于二进制文件重排,我们在针对iOS客户端上的优化工作中,获得了约15%的启动速度提升。

抽象来看,APP开发中大家会遇到这样一个通用的问题,即在某些情况下,APP运行需要进行大量的Page Fault,这会影响代码执行速度。而二进制文件重排方案,目前看来是解决这一通用问题比较好的方案。


转载于字节跳动技术团队:https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q








收起阅读 »

iOS MachO文件

目标文件.aFramework可执行文件.dsym1.2.1 .out、可执行文件test.c文件,内容如下:#include int main() { printf("test\n"); return 0; }验证不指定默认生成...
继续阅读 »

一、MachO文件概述


Mach-O(Mach Object)是mac以及iOS上的格式, 类似于windows上的PE格式 (Portable Executable ),linux上的elf格式 (Executable and Linking Format)。

Mach-O是一种用于可执行文件目标代码动态库的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性。


1.1 MachO格式的常见文件

  • 目标文件.o
  • 库文件
  • .a
  • .dylib
  • Framework
  • 可执行文件
  • dyld
  • .dsym

1.2 格式验证

1.2.1 .o.out、可执行文件

新建test.c文件,内容如下:

#include 

int main() {
printf("test\n");
return 0;
}

验证.o文件:

clang -c  test.c
//clang -c test.c -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
不指定-c默认生成a.out,如果报找不到'stdio.h' file not found,则可以指定-isysroot。文章最后有具体的解决方案,
通过file指令查看文件格式:

file test.o
test.o: Mach-O 64-bit object x86_64

验证a.out可执行文件:

clang test.o
file a.out
a.out: Mach-O 64-bit executable x86_64
./a.out
test

验证可执行文件:

clang -o test2 test.c 
file test2
test2: Mach-O 64-bit executable x86_64
./test2
test

至此再生成一个test3可执行文件:

clang -o test3 test.o

那么生成的a.outtest2test3一样么?



可以看到生成的可执行文件md5相同。



⚠️原则上test3md5应该和test2a.out相同。源码没有变化,所以应该相同的。在指定-isysroot后生成的可能不同,推测和CommandLineTools有关(系统中一个,Xcode中一个)。

再创建一个test1.c文件,内容如下:


#include 

void test1Func() {
printf("test1 func \n");
}

修改test.c:

#include 

void test1Func();

int main() {
test1Func();
printf("test\n");
return 0;
}

这个时候相当于有多个文件了,编译生成可执行文件demodemo1demo2:


clang -o demo  test1.c test.c 
clang -c test1.c test.c
clang -o demo1 test.o test1.o
clang -o demo2 test1.o test.o



这里demo1demo2``md5不同是因为test.otest1.o顺序不同。

objdump --macho -d demo查看下macho:



这也就解释了md5不同的原因。这里很像XcodeBuild Phases -> Compile Sources中源文件的顺序。

⚠️源文件顺序不同,编译出来的二进制文件不同( 大小相同),二进制排列顺序不同。


1.2.2.a文件

直接创建一个library库查看:

//find /usr -name "*.a"
file libTestLibrary.a
libTestLibrary.a: current ar archive random library

1.2.3. .dylib

cd /usr/lib
file dyld
dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
dyld (for architecture i386): Mach-O dynamic linker i386

1.2.4 dyld

cd /usr/lib
file dyld
dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
dyld (for architecture i386): Mach-O dynamic linker i386

这里需要注意的是dyld不是可执行文件,是一个dynamic linker。系统内核触发。

1.2.5 .dsym

file TestDsym.app.dSYM
TestDsym.app.dSYM: directory

cd TestDsym.app.dSYM/Contents/Resources/DWARF

file TestDsym
TestDsym: Mach-O 64-bit dSYM companion file arm64

二、可执行文件

创建一个工程,默认生成的文件就是可执行文件,查看对应的MachO:

file TestDsym
TestDsym: Mach-O 64-bit executable arm64
可以看到是一个单一架构的可执行文件(⚠️11以上的系统都只支持64位架构,所以默认就没有32位的)。将Deployment Info改为iOS 10编译再次查看MachO

file TestDsym
TestDsym: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64:Mach-O 64-bit executable arm64]
TestDsym (for architecture armv7): Mach-O executable arm_v7
TestDsym (for architecture arm64): Mach-O 64-bit executable arm64

这个时候就有多个架构了。
当然也可以在Xcode中直观的看到支持的架构:



  • Architectures:支持的架构。
  • Build Active Architecture Only:默认情况下debug模式下只编译当前设备架构,release模式下需要根据支持的设备。
  • $(ARCHS_STANDARD):环境变量,代表当前支持的架构。

如果我们要修改架构直接在Architectures中配置(增加armv7s):



编译再次查看MachO:

file TestDsym
TestDsym: Mach-O universal binary with 3 architectures: [arm_v7:Mach-O executable arm_v7] [arm_v7s:Mach-O executable arm_v7s] [arm64:Mach-O 64-bit executable arm64]
TestDsym (for architecture armv7): Mach-O executable arm_v7
TestDsym (for architecture armv7s): Mach-O executable arm_v7s
TestDsym (for architecture arm64): Mach-O 64-bit executable arm64

2.1通用二进制文件(Universal binary


  • 苹果公司提出的一种程序代码,能同时适用多种架构的二进制文件。
  • 同一个程序包中同时为多种架构提供最理想的性能。
  • 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。
  • 由于多种架构有共同的非执行资源(代码以外的),所以并不会达到单一版本的多倍之多(特殊情况下,只有少量代码文件的情况下有可能会大于多倍)。
  • 由于执行中只调用一部分代码,运行起来不需要额外的内存。


当我们将通用二进制文件拖入Hopper时,能够看到让我们选择对应的架构:



2.2lipo命令

lipo是管理Fat File的工具,可以查看cpu架构,,提取特定架构,整合和拆分库文件。

使用lipo -info 可以查看MachO文件包含的架构
lipo -info MachO文件


lipo -info TestDsym
Architectures in the fat file: TestDsym are: armv7 armv7s arm64
使用lifo –thin 拆分某种架构
lipo MachO文件 –thin 架构 –output 输出文件路径

lipo TestDsym -thin armv7 -output macho_armv7
lipo TestDsym -thin arm64 -output macho_arm64
file macho_armv7
macho_armv7: Mach-O executable arm_v7
file macho_arm64
macho_arm64: Mach-O 64-bit executable arm64

使用lipo -create 合并多种架构
lipo -create MachO1 MachO2 -output 输出文件路径

lipo -create macho_armv7 macho_arm64 -output  macho_v7_64

file macho_v7_64
macho_v7_64: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64:Mach-O 64-bit executable arm64]
macho_v7_64 (for architecture armv7): Mach-O executable arm_v7
macho_v7_64 (for architecture arm64): Mach-O 64-bit executable arm64

三、MachO文件结构


Mach-O 的组成结构如图所示:

  • Header:包含该二进制文件的一般信息。

    • 字节顺序、架构类型、加载指令的数量等。
    • 快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么。
  • Load Commands:一张包含很多内容的表。

    • 内容包括区域的位置、符号表、动态符号表等。
  • Data:通常是对象文件中最大的部分。

    • 包含Segement的具体数据

通用二进制文件就是包含多个这种结构。

otool -f MachO文件查看Header信息:


otool -f TestDsym

Fat headers
fat_magic 0xcafebabe
nfat_arch 3
architecture 0
cputype 12
cpusubtype 9
capabilities 0x0
offset 16384
size 79040
align 2^14 (16384)
architecture 1
cputype 12
cpusubtype 11
capabilities 0x0
offset 98304
size 79040
align 2^14 (16384)
architecture 2
cputype 16777228
cpusubtype 0
capabilities 0x0
offset 180224
size 79760
align 2^14 (16384)

分析MachO最好的工具就是 MachOView了:



otool的内容相同,对于多架构MachO会有一个Fat Header其中包含了CPU类型和架构。OffsetSize代表了每一个每一个架构在二进制文件中的偏移和大小。

这里有个问题是16384+79040 = 95424 < 9830498304 - 16384 = 8192081920 / 4096 / 4 = 5,可以验证这里是以页对齐的。(iOS中一页16KMachO中都是以页为单位对齐的,这也就是为什么能在Load Commands中插入LC_LOAD_DYLIB的原因。)。

MachO对应结构如下:




3.1Header

Header数据结构:



对应dyld的定义如下(loader.h):

struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};

struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
  • magic:魔数,快速定位属于64位还是32位。
  • cputypeCPU类型,比如ARM
  • cpusubtypeCPU具体类型,arm64armv7
  • filetype:文件类型,比如可执行文件,具体包含类型如下:

  • #define MH_OBJECT   0x1     /* relocatable object file */
    #define MH_EXECUTE 0x2 /* demand paged executable file */
    #define MH_FVMLIB 0x3 /* fixed VM shared library file */
    #define MH_CORE 0x4 /* core file */
    #define MH_PRELOAD 0x5 /* preloaded executable file */
    #define MH_DYLIB 0x6 /* dynamically bound shared library */
    #define MH_DYLINKER 0x7 /* dynamic link editor */
    #define MH_BUNDLE 0x8 /* dynamically bound bundle file */
    #define MH_DYLIB_STUB 0x9 /* shared library stub for static
    linking only, no section contents */

    #define MH_DSYM 0xa /* companion file with only debug
    sections */

    #define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
    #define MH_FILESET 0xc /* a file composed of other Mach-Os to
    be run in the same userspace sharing
    a single linkedit. */

  • ncmdsNumber of Load CommandsLoad Commands条数。
  • sizeofcmdsSize of Load CommandsLoad Commands大小。
  • flags:标识二进制文件支持的功能,主要是和系统加载、链接有关。
  • reservedarm64特有,保留字段。

  • 3.2 LoadCommands


    Load Commands指示dyld如何加载二进制文件。
    一个基本的Load Comands如下:




    空指针陷阱,目的是为了和32位指令完全分开。(32位地址在4G以下,64位地址大于4G 0xffffffff = 4G)。
    __PAGEZERO不占用数据(file size0),唯一有的是VM Sizearm64 4Garmv7比较小)。

    VM Addr : 虚拟内存地址
    VM Size: 虚拟内存大小。运行时刻在内存中的大小,一般情况下和File size相同,__PAGEZERO例外。
    File offset:数据在文件中偏移量。
    File size: 数据在文件中的大小。
    我们定位是看VM Addr + ASLR

  • __TEXT__DATA__LINKEDIT:将文件中(32位/64位)的段映射到进程地址空间中。
    分为三大块,分别对应DATA中的Section__TEXT + __DATA)、__LINKEDIT。告诉dyld占用多大空间。

  • LC_DYLD_INFO_ONLY:动态链接相关信息。




  • Rebase:重定向(ASLR)偏移地址和大小。从Rebase Info Offset + ASLR开始加载336个字节数据。
    Binding:绑定外部符号。
    Weak Binding:弱绑定。
    Lazy Binding:懒绑定,用到的时候再绑定。
    Export info:对外开放的函数。

  • LC_SYMTAB:符号表地址。





  • LC_DSYMTAB:动态符号表地址。


    LC_LOAD_DYLINKER:使用何种动态加载器。iOS使用的是dyld







    • LC_FUNCTION_DYLIB:函数起始地址表。

    • LC_DATA_IN_CODE:定义在代码段内的非指令的表。

    • LC_DATA_SIGNATURE:代码签名。


    3.3Data

    Data包含Section__TEXT + __DATA)、__LINKEDIT

    3.3.1__TEXT



    __TEXT代码段,就是我们的代码。

    • __text:主程序代码。开始是代码起始位置,和Compile Sources中文件顺序有关。

    __stubs & __stub_helper:用于符号绑定。



    这里65a0就是325a0,这里是循环做符号绑定。

    • __objc_methname:方法名称

    • __objc_classname:类名称

    • __objc_methtype:方法类型

    • __cstring:字符串常量


    3.3.2__DATA


    __DATA数据段。

    • __got & __la_symbol_ptr:外部符号有两张表Non-LazyLazy


    Lazy懒加载表,表中的指针一开始都指向 __stub_helper


    • __cfstring:程序中使用的 Core Foundation 字符串(CFStringRefs)。

    • __objc_classlist:类列表。

    • __objc_protolist: 原型。

    • __objc_imageinfo:镜像信息

    • __objc_selrefsself 引用

    • __objc_classrefs:类引用

    • __objc_superrefs:超类引用

    • __data:初始化过的可变数据。


    3.3.3 __LINKEDIT

  • Dynamic Loader Info:动态加载信息

  • Function Starts:入口函数

  • Symbol Table:符号表

  • Dynamic Symbol Table:动态库符号表

  • String Table:字符串表

  • Code Signature:代码签名


  • 总结

  • MachO属于一种文件格式。
    • 包含:可执行文件、静态库、动态库、dyld等。
    • 可执行文件:
      • 通用二进制文件(Fat):集成了多种架构。
      • lipo命令:-thin拆分架构,-creat合并架构。
  • MachO结构:
    • Header:快速确定该文件的CPU类型,文件类型等。
    • Load Commands:知识加载器(dyld)如何设置并加载二进制数据。
    • Data:存放数据,代码、数据、字符串常量、类、方法等。


  • 作者:HotPotCat
    链接:https://www.jianshu.com/p/9f6955575213


    收起阅读 »

    iOS面试可以怼HR的点-应用重签名

    首先理解一件事:签名是可以被替换的。签名:原始数据->hash->加密重签名:原始数据->hash->加密这也就是签名可以被替换的原因。一、codesign重签名codesign安装Xcode就有,Xcode也是用的这个工具。签名包含:...
    继续阅读 »

    首先理解一件事:签名是可以被替换的。
    签名:原始数据->hash->加密
    重签名:原始数据->hash->加密
    这也就是签名可以被替换的原因。


    一、codesign重签名

    codesign安装Xcode就有,Xcode也是用的这个工具。
    签名包含:
    资源文件
    macho文件
    framework
    ...


    1.1终端命令

    1.1.1查看签名信息

    codesign -vv -d xxx.app


    1.1.2列出钥匙串里可签名的证书

    security find-identity -v -p codesigning


    1.1.3otool分析macho文件信息并导出到指定文件

    otool -l xxx > ~/Desktop/machoMessage.txt

    其中cryptid0表示没有用到加密算法(也就是脱壳的), 其它则表示加密。



    也可以直接过滤查看是否砸壳:

    otool -l xxx | grep cryptid

    1.1.4强制替换签名


    codesign –fs “证书串” 文件名

    codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" andromeda.framework

    1.1.5给文件添加权限

    chmod +x 可执行文件

    1.1.6查看描述文件


    security cms -D -i ../embedded.mobileprovision

    1.1.7macho签名

    codesign -fs “证书串” --no-strict --entitlements=权限文件.plist APP包

    1.1.8将输入文件压缩为输出文件

    zip –ry 输出文件 输入文件 

    1.1.9越狱的手机dump app 包

    // 建立连接
    sh usbConnect.sh
    //连接手机
    sh usbX.sh
    //查看进程
    ps -A
    //找到微信进程,拿到路径
    ps -A | grep WeChat
    //进入目标文件夹拷贝微信(这里是没有砸壳的)
    scp -r -p 12345 root@localhost:微信路径 ./

    1.2codesign命令重签名


    这里以砸过壳的微信(7.0.8)为例,使用免费开发者账号重签名微信,然后安装到非越狱手机上。

    1. 解压缩.ipa包,Payload中找到.app显示包内容。
      ⚠️由于免费证书没有办法签名PlugInsWatch,直接将这两个文件夹删除。

    2. 签名Frameworks
      逐个签名Frameworks目录下的framework(使用自己本机的免费证书)


    codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" andromeda.framework



    1. 确保要签名的appmacho文件的可执行权限
    ➜  WeChat.app ls -l WeChat
    -rwxr-xr-x@ 1 zaizai staff 126048560 10 16 2019 WeChat
    4.获取免费账号对应的描述文件

    创建空工程使用免费账号&真机编译获运行取描述文件。


    这个时候描述文件已经拷贝到手机中去了,并且已经信任设备。
    将获取到的描述文件拷贝到 WeChatapp包中。

    5.修改bundleId
    找到WeChat info.plist修改BundleId为我们生成描述文件的BundleId





    1. 获取描述文件的权限
    security cms -D -i embedded.mobileprovision

    找到对应的权限Entitlements:

        <dict>

    <key>application-identifier</key>
    <string>S48J667P47.com.guazai.TestWeChat</string>

    <key>keychain-access-groups</key>
    <array>
    <string>S48J667P47.*</string>
    </array>

    <key>get-task-allow</key>
    <true/>

    <key>com.apple.developer.team-identifier</key>
    <string>S48J667P47</string>

    </dict>

    创建一个.plist文件,将权限内容粘贴进去:



    内容如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <!--
    Entitlements.plist
    TestWeChat

    Created by ZP on 2021/4/19.
    Copyright (c) 2021 ___ORGANIZATIONNAME___. All rights reserved.
    -->

    <plist version="1.0">
    <dict>

    <key>application-identifier</key>
    <string>S48J667P47.com.guazai.TestWeChat</string>

    <key>keychain-access-groups</key>
    <array>
    <string>S48J667P47.*</string>
    </array>

    <key>get-task-allow</key>
    <true/>

    <key>com.apple.developer.team-identifier</key>
    <string>S48J667P47</string>

    </dict>
    </plist>

    将权限文件(Entitlements.plist)拷贝到和PayloadWeChat.app同一目录


    1. 签名Wechat
    codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" --no-strict --entitlements=entitlements.plist WeChat.app

    这里entitlments参数需要和上一步生成的权限文件名称对应上。




    这个时候通过XcodeWeChat.app包安装到手机就已经能正常安装了。

    通过debug->attach to process->WeChat就可以调试微信了:



    ⚠️这个时候不要用自己的常用账号登录重签名的微信(有可能被封号)。

    重签名步骤:

    1. 删除插件以及带有插件的.app包(如:watch
      PlugInsWatch文件夹
    2. Frameworks中的库重签名
      codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" andromeda.framework
    3. 对可执行文件+X(可执行)权限
      chmod +x WeChat
    4. 添加描述文件(创建工程,真机编译得到,并且需要运行将描述文件安装到手机)
    5. 替换info.plist BundleIdBundleId要和描述文件中的一致)
    6. 通过授权文件(entitlments)重签名.app
      a.获取描述文件权限内容security cms -D -i embedded.mobileprovision
      b.将描述文件权限内容拷贝生成plist文件Entitlements.plist
      c.用全线文件签名App包:codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" --no-strict --entitlements=entitlements.plist WeChat.app

    二、利用Xcode重签名调试三方应用

    1.新建和微信同名工程WeChat



    2.将空工程运行到真机上。

    3.解压.ipa包,并且删除WatchPlugIns文件夹

    4.重签名Frameworks

    5.修改BundleId

    6.将修改后的WeChat.app替换空工程的Products.app





    7.运行
    这个时候Products工程中有WeChat.appXcode认为有就直接使用这个了。这个时候就可以调试了(不需要attach



    ⚠️在某些系统下会出现启动重签名微信黑屏,建议通过脚本重签名。

    三、SHELL脚本

    shell是一种特殊的交互式工具,它为用户提供了启动程序、管理文件系统中文件以及运行在系统上的进程的途径。Shell一般是指命令行工具。它允许你输入文本命令,然后解释命令,并在内核中执行。
    Shell脚本,也就是用各类命令预先放入到一个文本文件中,方便一次性执行的一个脚本文件。


    脚本切换

    chsh -s /bin/zsh
    执行脚本的几种方式:
    有如下脚本:

    mkdir shell1
    cd shell1
    touch test.txt


  • source FileName  
    作用:在当前shell环境中读取并执行FileName中的命令
    特点:命令可以强行让一个脚本去立即影响当前的环境(一般用于加载配置文件)。
    命令会强制执行脚本中的全部命令,而忽略文件的权限。

  • bash FileNamezsh FileName  
    作用:重新建立一个子shell(进程),在子shell中执行脚本里面的句子。当前环境没有变化。

  • ./FileName
    作用:读取并执行文件中的命令。但有一个前提,脚本文件需要有可执行权限。


  • MAC中shell种类

    cd /private/etc
    cat shells





  • bashmacOS默认shell(老系统),新系统切换为zsh了。
  • csh:被tcsh替换了
  • dash:比bash小很多,效率高。
  • ksh:兼容bash
  • sh:已经被bash替换了
  • tcsh:整合了csh提供了更多功能
  • zsh:替换了bash

  • 四、用户组&文本权限

    UnixLinux都是多用户、多任务的系统,所以这样的系统里面就拥有了用户、组的概念。那么同样文件的权限也就有相应的所属用户和所属组。
    windows不同的是unixlinuxmacOS都是多用户的系统:





    4.1mac文件属性


    4.2权限

    权限有10位:

    drwx-r-xr-x

  • 1位文件类型d/-
    d目录(directory)
    -文件
  • 后面9位,文件权限:
    [r]:read,读
    [w]:write,写
    [x]:execute,执行
    ⚠️这三个权限的位置不会变,依次是rwx。出现-对应的位置代表没有此权限。
    • 一个文件的完整权限分为三组:
      第一组:文件所有者权限
      第二组:这一组其它用户权限
      第三组:非本组用户的权限


    4.3权限改变chmod

    文件权限的改变使用chmod命令。
    设置方法有两种:数字类型改变 和 符号类型改变。
    文件权限分为三种身份:[user][group][other]  
    三个权限:[read] [write] [execute]

    4.3.1数字类型


    各个权限数字对照:r:4(0100)  w:2(0010)  x:1(0001),这么设计的好处是可以按位或。与我们开发中位移枚举同理。
    如果一个文件权限为[!–rwxr-xr-x],则对应:
    User : 4+2+1 = 7
    Group: 4+0+1 = 5
    Other: 4+0+1 = 5
    命令为:chmod 755 文件名

    数字与权限对应表:


    0代表没有任何权限。

    4.3.2符号类型

    chmod [u(User)、g(Group)、o(Other)、a(All)] [+(加入)、-(除去)、=(设置)]  [r、w、x] 文件名称
    例:

    chmod a+x test.txt

    默认是all

    五、通过shell脚本自动重签名

    脚本实现逻辑和codesign逻辑相同。
    完整脚本如下:

    #临时解压目录
    TEMP_PATH="${SRCROOT}/Temp"
    #资源文件夹,我们提前在工程目录下新建一个APP文件夹,里面放ipa包(砸壳后的)
    ASSETS_PATH="${SRCROOT}/APP"
    #目标ipa包路径
    TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"


    #清空&创建Temp文件夹
    rm -rf TEMP_PATH
    mkdir -p TEMP_PATH


    # 1. 解压IPA到Temp目录下
    unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
    # 拿到解压后的临时的APP的路径
    TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")

    #2. 将解压出来的.app拷贝进入工程下
    #2.1拿到当前工程目标Target路径
    # BUILT_PRODUCTS_DIR 工程生成的APP包的路径
    # TARGET_NAME target名称
    TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
    echo "app path:$TARGET_APP_PATH"

    #2.2删除工程本身的Target,将解压的Target拷贝到工程本身的路径
    rm -rf "$TARGET_APP_PATH"
    mkdir -p "$TARGET_APP_PATH"
    cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"


    # 3. 删除extension和WatchAPP,个人证书没法签名Extention
    rm -rf "$TARGET_APP_PATH/PlugIns"
    rm -rf "$TARGET_APP_PATH/Watch"



    # 4. 更新info.plist文件 CFBundleIdentifier
    # 设置:"Set : KEY Value" "目标文件路径",PlistBuddy是苹果自带的。
    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"

    #删除UISupportedDevices设备相关配置(越狱手机dump ipa包需要删除相关配置)
    /usr/libexec/PlistBuddy -c "Delete :UISupportedDevices" "$TARGET_APP_PATH/Info.plist"

    # 5. 给MachO文件上执行权限
    # 拿到MachO文件的名称
    APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
    #上可执行权限
    chmod +x "$TARGET_APP_PATH/$APP_BINARY"



    # 6. 重签名第三方 FrameWorks
    TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
    if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
    then
    for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
    do
    #签名 --force --sign 就是-fs
    /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
    done
    fi
    使用方式
    1.创建空工程,编译运行空工程至真机上(信任证书)。
    2.将appResign.sh脚本拷贝到工程根目录(要有可执行权限)。
    3.在工程根目录创建APP文件夹,并将微信.ipa拷贝到APP文件夹。
    4.配置脚本




    这个时候就可以调试微信了




    六、如何调试一个任意app?

    6.1获取对应ipa

    使用越狱手机dump ipa

    下载旧版本ipa包可以通过抓取iTunes的下载链接改版本号(后缀是app的版本,直接改版本)

    6.2砸壳

    砸壳后由于是dump越狱手机上的正版包,所以需要将info.plist中支持的设备信息(UISupportedDevices)删除。(当然可以写在脚本中)

    #删除UISupportedDevices设备相关配置(越狱手机dump ipa包需要删除相关配置)
    /usr/libexec/PlistBuddy -c "Delete :UISupportedDevices" "$TARGET_APP_PATH/Info.plist"

    删除完毕保存重新打包ipa

    zip -ry WeChat1.ipa Payload/

    总结

    • 重签名
      • codesign重签名
        • 删除不能签名的文件:Extensionwatch(包含了Extension
        • 重签名Frameworks(里面的库)
        • MachO添加可执行权限
        • 修改Info.plist文件(BundleID
        • 拷贝描述文件(该描述文件要在iOS真机中信任过)
        • 利用描述文件中的权限文件签名整个App
      • Xcode重签名
        • 删除不能签名的文件:Extensionwatch(包含了Extension
        • 重签名Frameworks(里面的库)
        • MachO添加可执行权限
        • 修改Info.plist文件(BundleID
        • App包拷贝进入Xcode工程目录中(剩下的交给Xcode
    • shell
      • 切换shell
        • $chsh -s shell路径
        • 现在macOSshell默认zsh(早期bash
        • 配置文件 zsh:.zshrc  bash:.bash_profile
      • 文件权限&用户组
        • 每个文件都有所属的用户、组、其它
        • 文件权限
          • 归属:用户、组、其它
          • 权限 :写、读、执行
        • 修改权限chmod
          • 数字:r:4 w:2 x:1
            • chmod 751 文件名
              • user4+2+1 = 7
              • group4+0+1 = 5
              • other0+0+1 = 1
          • 字符
            • 归属:u(用户) g(组) o(其它) a(所有)
            • +(添加) -(去掉) =(设置)
            • 默认achmod + x


    作者:HotPotCat
    链接:https://www.jianshu.com/p/ecf3d9957ebd



    收起阅读 »

    iOS 中的事件传递和响应机制 - 原理篇

    注:根据史上最详细的iOS之事件的传递和响应机制-原理篇重新整理(适当删减及补充)。在 iOS 中,只有继承了 UIReponder(响应者)类的对象才能接收并处理事件。其公共子类包括 UIView 、UIViewController 和 UIApplicat...
    继续阅读 »

    注:根据史上最详细的iOS之事件的传递和响应机制-原理篇重新整理(适当删减及补充)。

    在 iOS 中,只有继承了 UIReponder(响应者)类的对象才能接收并处理事件。其公共子类包括 UIView 、UIViewController 和 UIApplication 。

    UIReponder 类中提供了以下 4 个对象方法来处理触摸事件:

    /// 触摸开始
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
    /// 触摸移动
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
    /// 触摸取消(在触摸结束之前)
    /// 某个系统事件(例如电话呼入)会打断触摸过程
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}
    /// 触摸结束
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}

    注意:

    如果手指同时触摸屏幕,touches(_:with:) 方法只会调用一次,Set<UITouch> 包含两个对象;

    如果手指前后触摸屏幕,touches(_:with:) 会依次调用,且每次调用时 Set<UITouch> 只有一个对象

    iOS 中的事件传递

    事件传递和响应的整个流程

    触发事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中;
    UIApplication 会从事件队列中取出最前面的事件,将之分发出去以便处理,通常,先发送事件给应用程序的主窗口( keyWindow );
    主窗口会在视图层次结构中<u>找到一个最适合的视图</u>来处理触摸事件;
    找到适合的视图控件后,就会调用该视图控件的 touches(_:with:) 方法;
    touches(_:with:) 的默认实现是将事件顺着响应者链(后面会说)一直传递下去,直到连 UIApplication 对象也不能响应事件,则将其丢弃。

    如何寻找最适合的控件来处理事件

    当事件触发后,系统会调用控件的 hitTest(_:with:) 方法来遍历视图的层次结构,以确定哪个子视图应该接收触摸事件,过程如下:

    调用自己的 hitTest(_:with:) 方法;
    判断自己能否触发事件、是否隐藏、alpha <= 0.01;
    调用 point(inside:with:) 来判断触摸点是否在自己身上;
    倒序遍历 subviews ,并重复前面三个步骤。直到找到包含触摸点的最上层视图,并返回这个视图,那么该视图就是那个最适合的处理事件的 view;
    如果没有符合条件的子控件,就认为自己最适合处理事件,也就是自己是最适合的 view;
    通俗一点来解释就是,其实系统也无法决定应该让哪个视图处理事件,那么就用遍历的方式,依次找到包含触摸点所在的最上层视图,则认为该视图最适合处理事件。

    注意:

    触摸事件传递的过程是从父控件传递到子控件的,如果父控件也不能接收事件,那么子控件就不可能接收事件。

    寻找最适合的的 view 的底层剖析

    hitTest(_:with:) 的调用时机

    事件开始产生时会调用;
    只要事件传递给一个控件,就会调用这个控件的 hitTest(_:with:) 方法(不管这个控件能否处理事件或触摸点是否自己身上)。
    hitTest(_:with:) 的作用

    返回一个最适合的 view 来处理触摸事件。

    注意:

    如果 hitTest(_:with:) 方法中返回 nil ,那么该控件本身和其 subview 都不是最适合的 view,而是该控件的父控件。

    在默认的实现中,如果确定最终父控件是最适合的 view,那么仍然会调用其子控件的 hitTest(_:with:) 方法(不然怎么知道有没有更适合的 view?参考 如何寻找最适合的控件来处理事件。)

    hitTest(_:with:) 的默认实现

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // 1. 判断自己能否触发事件
    if !self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01 {
    return nil
    }
    // 2.判断触摸点是否在自己身上
    if !self.point(inside: point, with: event) {
    return nil
    }
    // 3. 倒序遍历 `subviews` ,并重复前面两个步骤;
    // 直到找到包含触摸点的最前面的视图,并返回这个视图,那么该视图就是那个最合适的接收事件的 view;
    for view in subviews.reversed() {
    // 把坐标转换成控件上的坐标
    let p = self.convert(point, to: view)
    if let hitView = view.hitTest(p, with: event) {
    return hitView
    }
    }

    return self
    }

    iOS 中的事件响应

    找到最适合的 view 接收事件后,如果不重写实现该 view 的 touches(_:with:) 方法,那么这些方法的默认实现是将事件顺着响应者链向下传递, 将事件交给下一个响应者去处理。


    可以说,响应者链是由多个响应者对象链接起来的链条。UIReponder 的一个对象属性 next 能够很好的解释这一规则。

    UIReponder().next

    返回响应者链中的下一个响应者,如果没有下一个响应者,则返回 nil 。

    例如,UIView 调用此属性会返回管理它的 UIViewController 对象(如果有),没有则返回它的 superview;UIViewController 调用此属性会返回其视图的 superview;UIWindow 返回应用程序对象;共享的 UIApplication 对象则通常返回 nil 。

    例如,我们可以通过 UIView 的 next 属性找到它所在的控制器:

    extension UIView {
    var next = self.next
    while next != nil { // 符合条件就一直循环
    if let viewController = next as? UIViewController {
    return viewController
    }
    // UIView 的下一个响应控件,直到找到控制器。
    next = next?.next
    }
    return nil
    }

    转自:https://www.jianshu.com/p/024f0c719715

    收起阅读 »

    iOS开发笔记(十)— Xcode、UITabbar、特殊机型问题分析

    前言本文分享iOS开发中遇到的问题,和相关的一些思考。正文一、Xcode10.1 import头文件无法索引【问题表现】如图,当import头文件的时候,索引无效,无法联想出正确的文件;【问题分析】通过多个文件尝试,发现并非完全不能索引头文件,而是只能索引和当...
    继续阅读 »

    前言

    本文分享iOS开发中遇到的问题,和相关的一些思考。

    正文

    一、Xcode10.1 import头文件无法索引
    【问题表现】如图,当import头文件的时候,索引无效,无法联想出正确的文件;


    【问题分析】通过多个文件尝试,发现并非完全不能索引头文件,而是只能索引和当前文件在同级目录的头文件;
    有点猜测是Xcode10.1的原因,但是在升级完的半年多时间里,都没有出现过索引。
    从已有的知识来分析,很可能是Xcode的头文件搜索路径有问题,于是尝试把工程文件下的路径设置递归搜索,结果又出现以下问题:


    【问题解决】在多次尝试无效之后,最终还是靠Google解决该问题。
    如下路径,修改设置
    Xcode --> File --> Workspace Settings --> Build System --> Legacy Build System


    二、NSAssert的断点和symbolic 断点

    【问题表现】NSAssert是常见的断言,可以在debug阶段快速暴露问题,但是在触发的时候无法保持上下文;
    【问题分析】NSAssert的本质就是抛出一个异常,可以通过Xcode添加一个Exception Breakpoint:


    如下,便可以NSAssert触发时捕获现场。


    同理,在Exception Breakpoint,还有Smybolic Breakpoint较为常用。
    以cookie设置接口为例,以下为一段设置cookies的代码
    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies];
    但是有时候设置cookies的地方可能较多,此时可以添加一个Smybolic Breakpoint并设置符号为cookies。
    如下,可以看到所有设置cookies的接口:


    三、.m文件改成.mm文件后编译失败

    【问题表现】Pointer is missing a nullability type specifier (_Nonnull, _Nullable, or _Null_unspecified)
    出错代码行: typedef void(^SSDataCallback)(NSError *error, id obj);
    手动给参数添加 nullable的声明并无法解决。

    【问题分析】
    首先确定的是,这个编译失败实际上是一个warning,只是因为工程设置了把warning识别为error;
    其次.m文件可以正常编译,并且.m文件也是开启了warning as error的设置;而从改成.mm就报错的表现和提示log来看,仍然是因为参数为空的原因导致。

    【问题解决】
    经过对比正常编译的.mm文件,找到一个解决方案:
    1,添加NS_ASSUME_NONNULL_BEGIN在代码最前面,NS_ASSUME_NONNULL_END在代码最后面;
    2、手动添加_Nullable到函数的参数;
    typedef void(^SSDataCallback)(NSError * _Nullable error, id _Nullable obj);

    四、UITabbar疑难杂症

    问题1、batItem的染色异常问题

    【问题表现】添加UITabBarItem到tabbar上,但是图片会被染成蓝色;
    【问题分析】tabbar默认会帮我们染色,所以我们创建的UITabBarItem默认会被tinkColor染色的影响。
    解决办法就是添加参数imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal,这样UITabBarItem的图片变不会受到tinkColor影响。

    UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"商城" image:[UIImage imageNamed:@"tabbar_item_store"] selectedImage:[[UIImage imageNamed:@"tabbar_item_store_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];

    问题2、tabbar的背景色问题

    【问题表现】设置tabbar的背景色是0xFFFFFF的白色,但是实际的效果确是灰白色,并不是全白色;
    【问题分析】tabbar默认是透明的(属性translucent),会对tabbar下面的视图进行高斯模糊,然后再与背景色混合。
    【问题解决】
    1、自由做法,addSubview:一个view到tabbar上,接下来自己绘制4个按钮;(可操作性强,缺点是tabbar的逻辑需要自己再实现一遍)
    2、改变tabbar透明度做法,设置translucent=YES,再修改背景色;(引入一个巨大的坑,导致UITabbarViewController上面的子VC的self.view属性高度会变化!)
    3、空白图做法,把背景图都用一张空白的图片替代,如下:(最终采纳的做法)

    self.tabBar.backgroundImage = [[UIImage alloc] init];
    self.tabBar.backgroundColor = [UIColor whiteColor];

    问题3、tabbar顶部的线条问题

    【问题表现】UITabbar默认在tabbar的顶部会有一条灰色的线,但是并没有一个属性可以修改其颜色。
    【问题分析】从Xcode的工具来看,这条线是一个UIImageView:


    再从UITabbar的头文件来看,这条线的图片可能是shadowImage。
    【问题解决】将shadowImage用一张空白的图片替代,然后自己再添加想要的线条大小和颜色。

    self.tabBar.shadowImage = [[UIImage alloc] init];
    UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tabBar.width, 0.5)];
    lineView.backgroundColor = [UIColor colorWithHexString:@"e8e8e8"];
    [self.tabBar addSubview:lineView];

    五、特殊机型出现的异常现象

    1、iOS 11.4 充电时无法正常获取电量

    【问题表现】在某个场景需要获取电池,于是通过以下addObserverForName:UIDeviceBatteryLevelDidChangeNotification的方式监听电量的变化,在iOS 12的机型表现正常,但是在iOS 11.4的机型上会出现无法获取电量的原因。

    void (^block)(NSNotification *notification) = ^(NSNotification *notification) {
    SS_STRONG_SELF(self);
    NSLog(@"%@", self);
    self.batteryView.width = (self.batteryImageView.width - Padding_battery_width) * [UIDevice currentDevice].batteryLevel;
    };
    //监视电池剩余电量
    [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryLevelDidChangeNotification
    object:nil
    queue:[NSOperationQueue mainQueue]
    usingBlock:block];

    【问题分析】从电量获取的api开始入手分析,在获取电量之前,需要显式调用接口
    [UIDevice currentDevice].batteryMonitoringEnabled = YES;
    于是点击batteryMonitoringEnabled属性进入UIDevice.h,发现有个batteryState属性,里面有一个状态是充电UIDeviceBatteryStateCharging,但是对问题并无帮助;
    点击UIDeviceBatteryLevelDidChangeNotification发现还有一个通知是UIDeviceBatteryStateDidChangeNotification,猜测可能是充电状态下的回调有所不同;
    【问题解决】最终通过添加新通知的监听解决。该问题并不太难,但是养成多看.h文件相关属性的习惯,还是会有好处。

    [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryStateDidChangeNotification
    object:nil
    queue:[NSOperationQueue mainQueue]
    usingBlock:block];

    2、iOS 10.3的UILabel富文本排版异常

    【问题表现】有一段文本的显示需要设置首行缩进,所以用的富文本添加段落属性的方式;但是在iOS 10.3的6p机型上出现异常现象,如下:
    测试文本:contentStr=@"一年佛山电脑放山东难道是防空洞念佛"
    如下,最后的字符没有显示完全。
    实现方式是计算得到富文本,然后赋值给UILabel,再调用-sizeToFit的接口。


    以上的问题仅在一行的时候出现异常,两行又恢复正常。


    【问题分析】
    从表现来看,是sizeToFit的时候宽度结算出错;通过多次尝试,发现是少计算了大概两个空格的距离,也即是首行缩进的距离。
    【问题解决】
    方法1、去除首行缩进,每行增加两个空格;
    方法2、一行的时候,把宽度设置到最大;
    如何判断1行的情况,可以用以下的代码简短判断

    if (self.contentLabel.height < self.contentLabel.font.lineHeight * 2) { // 一行的情况
    self.contentLabel.width = self.width - 40;
    }

    总结

    日常开发遇到的问题,如果解决过程超过10分钟,我都会记录下来。
    这些问题有的很简单,仅仅是改个配置(如第一个Xcode索引问题),但是在解决过程中还是走了一些弯路,因为完全没想过可能会去改Workspace setting,都是在Build setting修改进行尝试。
    还有些问题纯粹是特定现象,比如说特殊机型问题,只是做一个备忘和提醒


    链接:https://www.jianshu.com/p/6c964411fc03

    收起阅读 »

    iOS逆向安防从入门到秃头--OC反汇编

    前面和兄弟们写了好多汇编的知识,今天我们开始步入正题了:OC的汇编1. 方法的调用我们开始就简单写个OC对象,看下他的汇编吧@interface XGPerson : NSObject+(XGPerson *)person;@end@implementatio...
    继续阅读 »

    前面和兄弟们写了好多汇编的知识,今天我们开始步入正题了:OC的汇编

    1. 方法的调用
    我们开始就简单写个OC对象,看下他的汇编吧
    @interface XGPerson : NSObject
    +(XGPerson *)person;
    @end

    @implementation XGPerson
    + (XGPerson *)person{
    return [self alloc];
    }
    @end

    int main(int argc, char * argv[]) {
    XGPerson *p = [XGPerson person];
    }

      1. 大家应该知道方法的本质就是消息的发送:objc_msgSend

    1.1. 动态分析

      1. 我们先看下汇编代码

    1.png

    我们知道objc_msgSend会有2个默认的参数(id self, SEL _cmd)

    这个根据前面的知识我们就更能理解,为什么参数最好控制在6个以内了

      1. 我们可以动态看下x0,x1的值
    • 2.png

      1.2. 静态分析

      动态调试是舒服,但是我们逆向开发的时候好多时候都会静态分析~

        1. 我们还是要老规矩分析一波~

      3-1.png

        1. 然后我们验证我们分析的正确性(iOS是小端模式,所以取出地址,从右向左读)

      4.png

      看来我们的静态分析没有错

      1.2.1. 工具分析

      说实在的:静态分析一个方法,我人都快傻掉了。要是真正的工程(成千上万的方法),我不瓦特了?

      估计那些·啥啥家·也是真想的~

      • 咱们先用Hopper看下二进制文件,会不会效果好点

      5.png

      那些imp指向的方法的实现,其实都是objc源码里面的方法---很早之前写了一篇博客objc源码调试 (目前最新的是objc4-818.2,其实差不多)

      2. block反汇编

      关于block,我也写了一篇博客block底层分析 --- 实不相瞒,太早了,有点忘记了,不过应该还可以参考

        1. 不过曾经没有看过汇编。现在看汇编又可以明白好多东西
        1. 先写一些代码
      int main(int argc, char * argv[]) {
      void(^block)(NSInteger index) = ^(NSInteger index){
      NSLog(@"block -- %ld",index);
      };

      block(1);
      }
      复制代码
        1. 我稍稍的画了个小小的图(小谷艺术细菌比较少,兄弟们多担待~)

      6.png

      我的理解:block其实也是个对象 --- 就是有点特殊

      3. 总结

      • hopper是专门做OC的反汇编之类的。但是我们项目中好多都会有C++和C代码,而且这个伪代码不太友好 --- 以后可能会用一个其他的工具

      • 写了好多汇编的博客,其实就那么些指令。我需要的时候就是一边查着看--接下来就要搞搞传说中的逆向了~

      • 还有谢谢兄弟们的点赞和浏览,坚持学习到了现在,非常真诚的给兄弟们鞠个躬Thanks♪(・ω・)ノ

      • 好了!兄弟们,等待我的下一篇产出 ~

      • 更多文章观看:https://github.com/uzi-yyds-code/IOS-reverse-security


    收起阅读 »

    Flutter实战详解--高仿好奇心日报

    前言最近Flutter一直比较火,我也它也是非常感兴趣,看了下官网的基础教程后我决定直接上手做一个App,一是这样学的比较快印象更加深刻,二是可以记录其中遇到的一些坑,帮助大家少走一些弯路.本篇文章我会尽可能详细的讲到每一个点上.项目地址Github,如果觉得...
    继续阅读 »

    前言

    最近Flutter一直比较火,我也它也是非常感兴趣,看了下官网的基础教程后我决定直接上手做一个App,一是这样学的比较快印象更加深刻,二是可以记录其中遇到的一些坑,帮助大家少走一些弯路.本篇文章我会尽可能详细的讲到每一个点上.

    项目地址

    Github,如果觉得不错,欢迎Star

    注意事项

    1.下载项目后报错是因为没有添加依赖,在pubspec.yaml文件中点击Packages get下载依赖,有时候会在这里出现卡死的情况,可以配置一下环境变量.在终端执行vi ~/.bash_profile,再添加export PUB_HOSTED_URL=https://pub.flutter-io.cn和
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn.详情请看修改Flutter环境变量.
    2.需要将File Encodings里的Project Encoding设置为UTF-8,否则有时候安卓会报错
    3.如果cocoapods不是最新可能会出现Error Running Pod Install,请更新cocoapods.
    4.由于flutter_webview_plugin这个插件只支持加载url,于是就需要做一些修改.

    iOS 在FlutterWebviewPlugin.m文件中的- (void)navigate:(FlutterMethodCall*)call方法中的最后一排,将[self.webview loadRequest:request]方法改为[self.webview loadHTMLString:url baseURL:nil]
    Android 在WebViewManager.java文件中webView.loadUrl(url)方法改为webView.loadData(url, "text/html", "UTF-8"),以及下面那排的void reloadUrl(String url) { webView.loadUrl(url); }改为void reloadUrl(String url) { webView.loadData(url, "text/html", "UTF-8"); }
    先看看效果图吧.

    iOS效果图


    Android效果图


    正题

    怎么搭建Flutter环境我就不多说了,官网上讲的很详细,还没有搭建开发环境的可以看看这个Flutter中文网.

    1导航栏Tabbar


    这里我用到了DefaultTabController这个控件,使用DefaultTabController包裹需要用到Tab的页面即可,它的child为Scaffold,Scaffold有个appBar属性,在AppBar中设置具体的样式,大家看代码会更加清楚.相关注释也都写上了.

    home: new DefaultTabController(
    length: titleList.length,
    child: new Scaffold(
    appBar: new AppBar(
    elevation: 0.0,//导航栏下面那根线
    title: new TabBar(
    isScrollable: false,//是否可滑动
    unselectedLabelColor: Colors.black26,//未选中按钮颜色
    labelColor: Colors.black,//选中按钮颜色
    labelStyle: TextStyle(fontSize: 18),//文字样式
    indicatorSize: TabBarIndicatorSize.label,//滑动的宽度是根据内容来适应,还是与整块那么大(label表示根据内容来适应)
    indicatorWeight: 4.0,//滑块高度
    indicatorColor: Colors.yellow,//滑动颜色
    indicatorPadding: EdgeInsets.only(bottom: 1),//与底部距离为1
    tabs: titleList.map((String text) {//tabs表示具体的内容,是一个数组
    return new Tab(
    text: text,
    );
    }).toList(),
    ),
    ),
    //body表示具体展示的内容
    body:TabBarView(children: [News(url: 'http://app3.qdaily.com/app3/homes/index_v2/'),News(url: 'http://app3.qdaily.com/app3/papers/index/')]) ,
    ),
    ),

    大家也可以看看官网的示例Flutter官网示例

    2. 不同样式的item

    样式一


    这种布局的大概结构如下


    注意这里图片是紧贴着右边屏幕的,所以这里需要用到Expanded控件,用于自动填充子控件.

    样式二


    这个样式的控件布局就很简单了,结构如下


    样式三


    这个和样式二差不多,只不过最上面多了一块.

    这里需要注意的是,那个你猜这个图片是堆叠在整个大图上面的,所以需要用到Stack这个控件,其中Stack中有个属性const FractionalOffset(double dx, double dy)用于表示子控件相对于父控件的位置

    样式四


    这种样式稍微复杂一点,结构如下


    3、数据抓取

    用青花瓷抓取了好奇心数据.青花瓷使用教程


    简单分析一下,has_more表示是否可以加载更多,last_key用于上拉加载的时候请求用的,feeds就是每一条数据,banners就是轮播图的信息,columns就是横向滚动的ListView的相关数据,这个后面讲.接下来就做json序列化相关的了.

    4.Json序列化

    首先在pubspec.yaml中导入

    dependencies:
    json_annotation: ^2.0.0
    dev_dependencies:
    build_runner: ^1.0.0
    json_serializable: ^2.0.0

    创建一个model.dart文件
    引入文件

    import 'package:json_annotation/json_annotation.dart';
    part 'model.g.dart';

    其中这个model.g.dart等会儿会自动生成.这里需要掌握两个知识点

    1.@JsonSerializable() 这是表示告诉编译器这个类是需要生成Model类的
    2,@JsonKey 由于服务器返回的部分数据名称在Dart语言中是不被允许的,比如has_more,Dart中命名不能出现下划线,所以就需要用到@JsonKey来告诉编译器这个参数对于json中的哪个字段

    @JsonSerializable()
    class Feed {
    String image;
    int type;
    @JsonKey(name: 'index_type')
    int indexType;
    Post post;
    @JsonKey(name: 'news_list')
    List<News> newsList;
    Feed(this.image,this.type,this.post,this.indexType,this.newsList);
    factory Feed.fromJson(Map<String,dynamic> json) => _$FeedFromJson(json);
    Map<String, dynamic> toJson() => _$FeedToJson(this);
    }

    好了,写完后会报错,因为FeedFromJson和FeedToJson没有找到,这个时候在控制到输入flutter packages pub run build_runner build指令后会自动生成一个moded.g.dart文件,于是在网络请求下来数据后就可以用Feed feed = Feed.fromJson(data)这个方法来将Json中数据转换保存在Feed这个实例中了.在model类中还有些复杂的Json嵌套,但是也都很简单,大家看一眼应该就会了,哈哈.JSON和序列化具体教程

    5.轮播图

    Flutter中的轮播图我用到了Fluuter_Swiper这个组件,这里设置小圆点属性的时候稍微麻烦了点,网上好像也没有讲到,我这里讲一下.
    首先要创建DotSwiperPaginationBuilder

    DotSwiperPaginationBuilder builder = DotSwiperPaginationBuilder(
    color: Colors.white,//未选中圆点颜色
    activeColor: Colors.yellow,//选中圆点颜色
    size:7,//未选中大小
    activeSize: 7,//选中圆点大小
    space: 5//圆点间距
    );

    然后在Swiper中的pagination属性中设置它

    pagination: new SwiperPagination(
    builder: builder,
    ),

    6.网络请求

    首先,展示页面要继承自StatefulWidget,因为需要动态更新数据和列表.
    网络请求插件我用的Dio,非常好用.
    在initState方法中请求数据表示刚加载页面的时候进行网络请求,请求数据方法如下

    void getData()async{
    if (lastKey == '0'){
    dataList = [];//下拉刷新的时候将DataList制空
    }
    Dio dio = new Dio();
    Response response = await dio.get("$url$lastKey.json");
    Reslut reslut = Reslut.fromJson(response.data);
    if(!reslut.response.hasMore){
    return;//如果没有数据就不继续了
    }
    if(reslut.response.columns != null) {
    columnList = reslut.response.columns;
    }
    lastKey = reslut.response.lastKey;//更新lastkey
    setState(() {
    if (reslut.response.banners != null){
    banners = reslut.response.banners;//给轮播图赋值
    }
    dataList.addAll(reslut.response.feeds);//给数据源赋值
    });
    }

    因为用到了setState()方法,所以在该方法中改变了的数据会对其相应的地方进行刷新,比如设置了ListView的itemCount个数为dataList.length,如果在SetState方法中dataList.length改变了,那么ListView的itemCount树也会自动改变并刷新ListView.

    7. 上拉刷新与加载

    Flutter中有RefreshIndicator用于下拉刷新,它有个onRefresh闭包方法,表示下拉的时候执行的方法,一般用于网络请求.onRefresh方法如下

    Future<void> _handleRefresh() {
    final Completer<void> completer = Completer<void>();
    Timer(const Duration(seconds: 1), () {
    completer.complete();
    });
    return completer.future.then<void>((_) {
    lastKey = '0';
    getData();
    });
    }

    下拉加载的话需要初始化一个ScrollController,将它设为ListView的controller,并对其进行监听,当滑动到最底部的时候进行网络请求.

    @override
    void initState() {
    url = widget.url;
    getData();
    _scrollController.addListener(() {
    ///判断当前滑动位置是不是到达底部,触发加载更多回调
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
    getData();
    }
    });
    }
    final ScrollController _scrollController = new ScrollController();

    上拉加载loading框用到了flutter_spinkit插件,提供了大量的加载样式.


    代码如下

    ///上拉加载更多
    Widget _buildProgressIndicator() {
    ///是否需要显示上拉加载更多的loading
    Widget bottomWidget = new Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
    ///loading框
    new SpinKitThreeBounce(color: Color(0xFF24292E)),
    new Container(
    width: 5.0,
    ),
    ]);
    return new Padding(
    padding: const EdgeInsets.all(20.0),
    child: new Center(
    child: bottomWidget,
    ),
    );
    }

    8. ListView赋值

    由于最上面有一个轮播图,最下面有加载框,所以ListView的itemCount个数为dataList.length+2,又因为每个item之间都有一个浅灰色的风格线,所以需要用到ListView.separated,具体代码如下:

    Widget build(BuildContext context) {
    return RefreshIndicator(
    onRefresh:(()=> _handleRefresh()),
    color: Colors.yellow,//刷新控件的颜色
    child: ListView.separated(
    physics: const AlwaysScrollableScrollPhysics(),
    itemCount: _getListCount(),//item个数
    controller: _scrollController,//用于监听是否滑到最底部
    itemBuilder: (context,index){
    if(index == 0){
    return SwiperWidget(context, banners);//如果是第一个,则展示banner
    }else if(index < dataList.length + 1){
    return WidgetUtils.GetListWidget(context, dataList[index - 1]);//展示数据
    }else {
    return _buildProgressIndicator();//展示加载loading框
    }
    },
    separatorBuilder: (context,idx){//分割线
    return Container(
    height: 5,
    color: Color.fromARGB(50,183, 187, 197),
    );
    },
    ),
    );
    }

    9. ListView嵌套横向滑动ListView

    这种的话也稍微复杂一点,有两种样式.并且到滑到最右边的时候可以继续请求并加载数据.


    首先来分析一下数据


    这个colunmns就是横向滑动列表的重要数据.


    里面的id是请求参数,show_type表示列表的样式,location表示插入的位置.而且通过抓取接口发现,当横向列表快要展示出来的时候,才会去请求横向列表的具体接口.
    那么思路就很清晰了,在请求获得数据后遍历colunmns,根据每个colunmn的location插入一个Map,如下

    data.insert(colunm.location,  {'id':colunm.id,'showType':colunm.showType});

    再创建一个ColumnsListWidget类,继承自StatefulWidget,是一个新item,在滑动到该列表的位置的时候,会将该Map数据传给ColumnsListWidget,这个时候ColumnsListWidget就会加载数据并展示出来了,滑到最右边的时候加载和滑到最底部加载的方法一样,就不多说了.具体可以查看源码,关键代码如下:

    static Widget GetListWidget(BuildContext context, dynamic data) {
    Widget widget;
    if(data.runtimeType == Feed) {
    if (data.indexType != null) {
    widget = NewsListWidget(context, data);
    } else if (data.type == 2) {
    widget = ListImageTop(context, data);
    } else if (data.type == 0) {
    widget = ActivityWidget(context, data);
    } else if (data.type == 1) {
    widget = ListImageRight(context, data);
    }
    }else{
    widget = ColumnsListWidget(id: data['id'],showType: data['showType'],);
    }

    1.横向ListView外需要用Flexible包裹,Flexible组件可以使Row、Column、Flex等子组件在主轴方向有填充可用空间的能力(例如,Row在水平方向,Column在垂直方向),但是它与Expanded组件不同,它不强制子组件填充可用空间。
    2.ListView初始位置用到padding: new EdgeInsets.symmetric(horizontal: 12.0),用padding: EdgeInsets.only(left: 12)的话会让ListView和最左边一直有条线

    10.webview加载复杂的Html字段


    获取到网页详情的数据发现是Html字段,并且其中的css是url地址,试了很多Flutter加载Html的插件发现样式都不正确,最后决定使用原生和Flutter混编,这时候发现flutter_webview_plugin这个插件是使用原生网页的,不过它只支持加载url,于是就需要做一些修改.
    iOS
    在FlutterWebviewPlugin.m文件中的- (void)navigate:(FlutterMethodCall*)call方法中的最后一排,将[self.webview loadRequest:request]方法改为[self.webview loadHTMLString:url baseURL:nil]
    Android
    在WebViewManager.java文件中webView.loadUrl(url)方法改为webView.loadData(url, "text/html", "UTF-8"),以及下面那排的void reloadUrl(String url) { webView.loadUrl(url); }改为void reloadUrl(String url) { webView.loadData(url, "text/html", "UTF-8"); }
    由于服务器端返回的Html中的css和js文件地址是/assets/app3开头的,所以需要替换成绝对路径,所以要用到这个方法htmlBody.replaceAll( '/assets/app3','http://app3.qdaily.com/assets/app3')
    好了,这下就可以呈现出漂亮的网页了.

    11.ListView嵌套GridView

    在点击横向滑动列表的总标题的时候,会进入到相关栏目的详情页,如图


    这个ListView包含上下两部分.上面这部分为:


    结构如下


    下面就是一个GridView,不过有时候下面会是ListView,根据shouwType字段来判断,GridView的代码如下:

    Widget ColumnsDetailTypeTwo(BuildContext context,List<Feed> feesList){
    return GridView.count(
    physics: NeverScrollableScrollPhysics(),
    crossAxisCount: 2,
    shrinkWrap: true,
    mainAxisSpacing: 10.0,
    crossAxisSpacing: 15.0,
    childAspectRatio: 0.612,
    padding: new EdgeInsets.symmetric(horizontal: 20.0),
    children: feesList.map((Feed feed) {
    return ColumnsTypeTwoTile(context, feed);
    }).toList()
    );
    }

    其中 childAspectRatio表示宽高比.

    圆角头像需要用到
    CircleAvatar(backgroundImage:NetworkImage(url),),这个控件

    12、在切换Tab的时候防止执行initState

    在切换顶部tab的时候会发现下面的界面会自动滑动到顶(位置重置)并执行initState,同时每次滑到横向ListView的时候,它也会执行initState并且位置也会重置,要让它只执行一次initState方法的话需要这么做.

    class _XXXState extends State<XXX> with AutomaticKeepAliveClientMixin{
    @override
    bool get wantKeepAlive => true;

    这样它就会只执行一次initState方法了.

    总结
    做了这个项目最大的感受就是界面布局是真的很方便很简单,因为做了一遍对很多知识点也理解的更深了.如果觉得有帮助到你的话,希望可以给个 Star

    项目地址
    Github

    链接:https://www.jianshu.com/p/4a0185b5a8f5

    收起阅读 »

    iOS多设备适配简史以及相应的API支撑实现

    远古的iPhone3和iPhone4时代,设备尺寸都是固定3.5inch,没有所谓的适配的问题,只需要用视图的frame属性进行硬编码即可。随着时间的推移,苹果的设备种类越来越多,尺寸也越来越大,单纯的frame已经不能简单解决问题了,于是推出了AutoLay...
    继续阅读 »

    远古的iPhone3和iPhone4时代,设备尺寸都是固定3.5inch,没有所谓的适配的问题,只需要用视图的frame属性进行硬编码即可。随着时间的推移,苹果的设备种类越来越多,尺寸也越来越大,单纯的frame已经不能简单解决问题了,于是推出了AutoLayout技术和SizeClasses技术来解决多种设备的适配问题。一直在做iOS开发的程序员相信在下面的两个版本交界处需要处理适配的坎一定让你焦头烂额过:

    1、iOS7出来后视图控制器的根视图默认的尺寸是占据整个屏幕的,如果有半透明导航条的话也默认是延伸到导航栏和状态栏的下面。这段时间相信你对要同时满足iOS7和以下的版本进行大面积的改版和特殊适配处理,尤其是状态栏的高度问题尤为棘手。

    2、iOS11出来后尤其是iPhoneX设备推出,iPhoneX设备的特殊性表现为顶部的状态栏高度由20变为了44,底部还出现了一个34的安全区,当横屏时还需要考虑左右两边的44的缩进处理。你需要对所有的布局代码进行重新适配和梳理以便兼容iPhoneX和其他设备,这里面还是状态栏的高度以及底部安全区的的高度尤为棘手。

    个人认为这两个版本的发布是iOS开发人员遇到的需要大量布局改版的版本。为了达到完美适配我们可能需要写大量的if,else以及写很多宏以及版本兼容来进行特殊处理。当然苹果也为上面两次大改版提供了诸多的解决方案:

    1、iOS7中对视图控制器提供了如下属性来解决版本兼容性的问题:

    @property(nonatomic,assign) UIRectEdge edgesForExtendedLayout NS_AVAILABLE_IOS(7_0); // Defaults to UIRectEdgeAll
    @property(nonatomic,assign) BOOL extendedLayoutIncludesOpaqueBars NS_AVAILABLE_IOS(7_0); // Defaults to NO, but bars are translucent by default on 7_0.
    @property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets API_DEPRECATED_WITH_REPLACEMENT("Use UIScrollView's contentInsetAdjustmentBehavior instead", ios(7.0,11.0),tvos(7.0,11.0)); // Defaults to YES

    @property(nonatomic,readonly,strong) id<UILayoutSupport> topLayoutGuide API_DEPRECATED_WITH_REPLACEMENT("-[UIView safeAreaLayoutGuide]", ios(7.0,11.0), tvos(7.0,11.0));
    @property(nonatomic,readonly,strong) id<UILayoutSupport> bottomLayoutGuide API_DEPRECATED_WITH_REPLACEMENT("-[UIView safeAreaLayoutGuide]", ios(7.0,11.0), tvos(7.0,11.0));

    2、iOS11中提出了一个安全区的概念,要求我们的可操作视图都放置在安全区内,并对视图和滚动视图提供了如下扩展属性:

    @property (nonatomic,readonly) UIEdgeInsets safeAreaInsets API_AVAILABLE(ios(11.0),tvos(11.0));
    - (void)safeAreaInsetsDidChange API_AVAILABLE(ios(11.0),tvos(11.0));

    /* The top of the safeAreaLayoutGuide indicates the unobscured top edge of the view (e.g, not behind
    the status bar or navigation bar, if present). Similarly for the other edges.
    */
    @property(nonatomic,readonly,strong) UILayoutGuide *safeAreaLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));
    /* When contentInsetAdjustmentBehavior allows, UIScrollView may incorporate
    its safeAreaInsets into the adjustedContentInset.
    */
    @property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0),tvos(11.0));

    /* Also see -scrollViewDidChangeAdjustedContentInset: in the UIScrollViewDelegate protocol.
    */
    - (void)adjustedContentInsetDidChange API_AVAILABLE(ios(11.0),tvos(11.0)) NS_REQUIRES_SUPER;

    /* Configure the behavior of adjustedContentInset.
    Default is UIScrollViewContentInsetAdjustmentAutomatic.
    */
    @property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));

    /* contentLayoutGuide anchors (e.g., contentLayoutGuide.centerXAnchor, etc.) refer to
    the untranslated content area of the scroll view.
    */
    @property(nonatomic,readonly,strong) UILayoutGuide *contentLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));

    /* frameLayoutGuide anchors (e.g., frameLayoutGuide.centerXAnchor) refer to
    the untransformed frame of the scroll view.
    */
    @property(nonatomic,readonly,strong) UILayoutGuide *frameLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));

    这些属性的具体意义这里就不多说了,网络上以及苹果的官方都有很多资料在介绍这些属性的意思。从上面的这些属性中可以看出苹果提出的这些解决方案其主要是围绕解决视图和导航条、滚动视图、状态栏、屏幕边缘之间的关系而进行的。因为iOS7和iOS11两个版本中控制器中的视图和上面所列出的一些内容之间的关系变化最大。

    NSLayoutConstraint约束以及iOS9上的封装改进
    在iOS6时代苹果推出了AutoLayout的技术解决方案,这是一套采用以相对约束来替代硬编码的解决方法,然而糟糕的方法名和使用方式导致使用成本和代码量的急剧增加。比如下面的一段代码:

    UIButton *button = [self createDemoButton:NSLocalizedString(@"Pop layoutview at center", "") action:@selector(handleDemo1:)];
    button.translatesAutoresizingMaskIntoConstraints = NO; //button使用AutoLayout
    [scrollView addSubview:button];

    //下面的代码是iOS6以来自带的约束布局写法,可以看出代码量较大。
    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];

    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTop multiplier:1 constant:10]];

    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:40]];

    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeWidth multiplier:1 constant:-20]];


    一个简单的将按钮放到一个UIScrollView中去的代码,当用AutoLayout来实现时出现了代码量风暴问题。对于约束的设置到了iOS9以后有了很大的改进,苹果对约束的设置进行了封装,提供了三个类:NSLayoutXAxisAnchor, NSLayoutYAxisAnchor, NSLayoutDimension来简化约束的设置,还是同样的功能用新的类来写约束就简洁清晰很多了:

    UIButton *button = [self createDemoButton:NSLocalizedString(@"Pop layoutview at center", "") action:@selector(handleDemo1:)];
    button.translatesAutoresizingMaskIntoConstraints = NO; //button使用AutoLayout
    [scrollView addSubview:button];
    [button.centerXAnchor constraintEqualToAnchor:scrollView.centerXAnchor].active = YES;
    [button.topAnchor constraintEqualToAnchor:scrollView.topAnchor constant:10].active = YES;
    [button.heightAnchor constraintEqualToConstant:40].active = YES;
    [button.widthAnchor constraintEqualToAnchor:scrollView.widthAnchor multiplier:1 constant:-20].active = YES;

    UIStackView
    在iOS9中还提供了一个UIStackView的类来简化那些视图需要从上往下或者从左往右依次添加排列的场景,通过UIStackView容器视图的使用就不再需要为每个子视图添加冗余的依赖约束关系了。在大量的实践中很多应用的各板块其实都是按顺序从上到下排列或者从左到右排列的。所以如果您的应用最低支持到iOS9的话就可以大量的应用这个类来构建你的程序了。

    占位视图类UILayoutGuide
    在iOS9以前两个视图之间的间距和间隔是无法支持浮动和可伸缩设置的,以及我们可以需要在两个视图之间保留一个浮动尺寸的空白区域,解决的方法是在它们中间加入一个透明颜色的UIView来进行处理,不管如何只要是View都需要进行渲染和绘制从而有可能一定程度上影响程序的性能,而在iOS9以后提供了一个占位视图类UILayoutGuide,这个类就像是一个普通的视图一样可以为它设置约束,也可以将它添加进入视图中去,也可以将这个占位视图作为其他视图的约束依赖项,唯一的不同就是占位视图不会进行任何的渲染和绘制,它只会参与布局处理。因此这个类的引入可以很大程度上解决那些浮动间距的问题。

    SizeClasses多屏幕适配
    当我们的程序可能需要同时在横屏和竖屏下运行并且横屏和竖屏下的布局还不一致时,而且希望我们的应用在小屏幕上和大屏幕上(比如iPhone8 Plus 以及iPhoneX S Max)的布局有差异时,我们可能需要用到苹果的SizeClasses技术。这是苹果在iOS8中推出来的一个概念。 但是在实际的实践中我们很少有看到使用SizeClasses的例子和场景以及在我们开发中很少有使用到这方面的技术,所以我认为这应该是苹果的一个多屏幕适配的失败解决的方案。从字面理解SizeClasses就是尺寸的种类,苹果将设备的宽和高分为了压缩和常规两种尺寸类型,因此我们可以得到如下几种类型的设备:


    很欣慰的是如果您的应用是一个带有系统导航条的应用时很多适配的问题都能够得到很好的解决,因为系统已经为你做了很多事情,你不需要做任何特殊的处理。而如果你的应用的某个界面是present出来的,或者是你自己实现的自定义导航条的话,那么你可能就需要自己来处理各种版本的适配问题了。并且如果你的应用可能还有横竖屏的话那这个问题就更加复杂了。

    最后除了可以用系统提供的API来解决所有的适配问题外,还向大家推荐我的开源布局库:MyLayout。它同时支持Objective-C以及Swift版本。而且用这个库后上面的所有适配问题都不是问题。

    转自:https://www.jianshu.com/p/b43b22fa40e3

    收起阅读 »

    关于Socket,看我这几篇就够了(二)之HTTP

    在上一篇中,我们初步的讲述了socket的定义,以及socket中的TCP的简单用法。这篇我们主要讲的是HTTP相关的东西。什么是HTTPHTTP -> Hyper Text Transfer Protocol(超文本传输协议),它是基于TCP/IP协议...
    继续阅读 »

    在上一篇中,我们初步的讲述了socket的定义,以及socket中的TCP的简单用法。

    这篇我们主要讲的是HTTP相关的东西。

    什么是HTTP

    HTTP -> Hyper Text Transfer Protocol(超文本传输协议),它是基于TCP/IP协议的一种无状态连接

    特性

    无状态

    无状态是指,在标准情况下,客户端的发出每一次请求,都是独立的,服务器并不能直接通过标准http协议本身获得用户对话的上下文。

    这里,可能很多人会有疑问,我们平时使用的http不是这样的啊,服务器能识别我们请求的身份啊,要不免登录怎么做啊?

    所以额外解释下,我们说的这些状态,如cookie/session是由服务器与客户端双方约定好,每次请求的时候,客户端填写,服务器获取到后查询自身记录(数据库、内存),为客户端确定身份,并返回对应的值。

    从另一方面也可说,这个特性和http协议本身无关,因为服务器不是从这个协议本身获取对应的状态。

    无状态也可这样理解: 从同一客户端连续发出两次http请求到服务器,服务器无法从http协议本身上获取两次请求之间的关系

    无连接

    无连接指的是,服务器在响应客户端的请求后,就主动断开连接,不继续维持连接

    结构

    http 是超文本传输协议,顾名思义,传输的是一定格式的文本,所以,我们接下来讲述一下这个协议的格式

    在http中,一个很重要的分割符就是 CRLF(Carriage-Return Line-Feed) 也就是 \r 回车符 + \n 换行符,它是用来作为识别的字符

    请求 Request


    上图为请求格式

    请求行

    GET / HTTP/1.1\r\n

    首行也叫请求行,是用来告诉服务器,客户端调用的请求类型,请求资源路径,请求协议类型

    请求类型也就是我们常说的(面试官总问的)GET,POST等等发送的位置,它位于请求的最开始

    请求资源路径是提供给服务器内部的寻址路径,用来告诉服务器客户端希望访问什么资源,在浏览器中访问 https://www.jianshu.com/p/6cfbc63f3a2b (用简书做一波示范了),则我们请求的就是 /p/6cfbc63f3a2b

    请求协议类型目前使用最多的是HTTP/1.1说不定在不远的未来,将会被HTTP/2.0所取代

    注:

    所使用链接为https链接,但是其内容与http一样,因此使用该链接做为例子,ssl 将会在接下来的几篇文章中讲述

    请求行的不同内容需要用 " "空格符 来做分割

    请求行的结尾需要添加CRLF分割符

    请求头Request Headers

    请求行之后,一直到请求体(body),之间的部分,被我们成为请求头。

    请求头的长度并不固定,我们可以放置无限多的内容到请求头中。

    但是请求头的格式是固定的,我们可以把它看做是键值对。

    格式:

    key: value\r\n

    我们通常所说的cookie便是请求头中的一项

    一些常用的http头的定义与作用: https://blog.csdn.net/philos3/article/details/76946029

    注:

    当所有请求头都已经结束(即我们要发送body)的时候,我们需要额外增加一个空行(CRLF) 告诉服务器请求头已经结束

    请求体Request Body

    如果说header我们没有那么多的使用机会的话,那么body则是几乎每个开发人员都必须接触的了。

    通常,当我们进行 POST 请求的时候,我们上传的参数就在这里了。

    服务器是如何获得我们上传的完整Body呢?换句话说,就是服务器怎么知道我们的body已经传输完毕了呢?

    我们想一下,如果我们在需要实现这个协议的时候,我们会怎么做?

    可以约定特殊字节作为终止字符,当读取到指定字符时,即认为读取完毕

    发送方肯定知道要发送的数据的大小,直接告诉接收方,接收方只需要在收到指定大小的数据的时候就可以停止接收了

    发送方也不知道数据的大小(或者他需要花很大成本才能知道数据的大小),就先告诉接收方,我现在也不知道有多少,等发送的时候看,真正发送的时候告诉接收方,"我这次要发送多少",最后告诉接收方,"我发完了",接收方以此停止接收。‘

    也许你会有别的想法,那恭喜你,你可以自己实现类似的接收方法了。

    目前,服务器是依靠上述三种方法接收的:

    约定特殊字节:
    客户端在发送完数据后,就调用关闭socket连接,服务器在收到关闭请求后开始解析数据,并返回结果,最后关闭连接

    确定数据大小:
    客户端在请求头中给定字段 Content-Length,服务器解析到对应数据后接受body,当body数据达到指定长度后,服务器开始解析数据,并返回结果

    不确定数据大小(Http/1.1 可用)
    客户端在请求头中给定头 Transfer-Encoding: chunked,随后开始准备发送数据

    发送的每段数据都有特定的格式,

    格式为:

    长度行:
    每段数据的开头的文本为该段真实发送的数据的16进制长度加CRLF分割符

    数据行:
    真实发送的数据加CRLF分割符

    例:

    12\r\n // 长度行 16进制下的12就是10进制下的 18
    It is a chunk data\r\n // 数据行 CRLF 为分割符

    结尾段:

    用以告诉服务器数据发送完成,开始解析或存储数据。

    结尾段格式固定

    0\r\n
    \r\n

    目前,客户端使用这种方法的不多。

    到这里,如何告诉服务器应该接收多少数据的部分已经完成了

    接下来就到了,告诉服务器,数据究竟是什么了

    同样也是头部定义:Content-Type

    Content-Type介绍:
    https://blog.csdn.net/qq_23994787/article/details/79044908

    到这里,Request的基本格式已经讲完

    响应 Response



    相应结构

    其实Response 和 Request 从协议上分析,他们是一样的,但是他们是对Http协议中文本协议的不同的实现。

    响应行

    HTTP/1.1 200 OK\r\n

    首行也叫响应行,是用来告诉客户端当前请求的处理状况的,由请求协议类型,服务器状态码,对应状态描述构成

    请求协议类型 是用来告诉客户端,服务器采用的协议是什么,以便于客户端接下来的处理。

    服务器状态码 是一个很重要的返回值,它是用来通知服务器对本次客户端请求的处理结果。

    状态码非常多,但是对于我们开发一般用到的是如下几个状态码


    完整错误码请参照网址:
    https://baike.baidu.com/item/HTTP%E7%8A%B6%E6%80%81%E7%A0%81/5053660?fr=aladdin

    响应头Response Headers 及 响应体Response Body
    这些内容与Request中对应部分并无区别,顾不赘述了

    我们已经从特性与结构两部分讲述了Http相关的属性,到这里这篇文章的主要内容基本上算是结束了,接下来我要讲讲一些其他的http相关的知识

    跨域
    作为移动端开发人员,我们对这个的了解不是很多,也几乎用不到,但是我这里还是需要说明。因为现在已经到了前端的时代,万一我们以后需要踏足前端,了解跨域,至少能为我们解决不少事情。

    这篇文章不会详细讲解如何解决跨域,只会讲解跨域形成的原因

    什么是 跨域
    在讲跨域的时候,需要先讲什么是域

    什么是域
    在上一课讲解socket的过程中,我们已经发现了,想建立一个TCP/IP的连接需要知道至少两个事情

    对方的地址(host)
    对方的门牌号(port)

    我们只有依靠这两个才能建立TCP/IP 的连接,其中host标明我们该怎么找到对方,port表示,我们应该连接具体的那个端口。

    服务器应用是一直在监听着这个端口的,这样才能保证在有连接进入的时候,服务器直接响应对应的信息

    向上聊聊吧,我们通常讲的服务器指的是服务器应用,比如常说Tomcat,Apache 等等,他们启动的时候一般会绑定好一个指定的端口(通常不会同时绑定两个端口)。所以呢,作为客户端,就可以用host+port来确定一个指定的服务器应用

    由此,域的概念就此生成,就是host + port

    举个例子: http://127.0.0.1:8056/

    这个网址所属的域就是127.0.0.1+8056 也可以写成127.0.0.1:8056

    这时候有人就会问了,那localhost:8056和127.0.0.1:8056是同一域么,他们实际是等价的啊。

    他们不属于同一域,规定的很死,因为他们的host的表示不同,所以不是。

    跨域
    我们已经知道域了,跨域也就出现了,就是一个域访问另一个域。

    我们从http协议中可以发现,服务器并不任何强制规定域,也就是说,服务器并不在乎这个访问是从哪个域访问过来的,同时,作为客户端,我们也并没有域这么一说。

    那么跨域究竟是什么呢?


    这就要说跨域的来源了,我们日常访问的网站,它实际上就是html代码,服务器将代码下发到了浏览器,由浏览器渲染并展示给我们。

    开发浏览器的程序员在开发的时候,也不知道这个网页究竟要做什么,但是他们为了安全着想,不能给网页和客户端(socket)同样的权限,因此他们限制了某些操作,在本域的网页的某些请求操作在对方的服务器没有添加允许该域的访问权限的时候,访问操作将不会被执行,这些操作会对浏览器的安全性有很大到的影响。

    所以跨域就此产生。

    跨域从头到尾都只是一个客户端的操作行为,从某种角度上说,它与服务器毫无关系,因为服务器无法得知某次请求是否来自于某一网页(在客户端不配合的情况下),也就无从禁止了

    对于我们移动端,了解跨域后我们至少可以说,跨域与我们无关-_-

    socket实现简单的http请求
    事实上,一篇文章如果没有代码上的支撑,只是纯理念上的阐述,终究还是感觉缺点什么,本文将在上篇文章代码的基础上做些小的改进。

    这里就以菜鸟教程网的http教程作为本篇文章的测试(http://www.runoob.com/http/http-tutorial.html)(ip:47.246.3.228:80)

    // MARK: - Create 建立
    let socketFD = Darwin.socket(AF_INET, SOCK_STREAM, 0)

    func converIPToUInt32(a: Int, b: Int, c: Int, d: Int) -> in_addr {
    return Darwin.in_addr(s_addr: __uint32_t((a << 0) | (b << 8) | (c << 16) | (d << 24)))
    }
    // MARK: - Connect 连接
    var sock4: sockaddr_in = sockaddr_in()

    sock4.sin_len = __uint8_t(MemoryLayout.size(ofValue: sock4))
    // 将ip转换成UInt32
    sock4.sin_addr = converIPToUInt32(a: 47, b: 246, c: 3, d: 228)
    // 因内存字节和网络通讯字节相反,顾我们需要交换大小端 我们连接的端口是80
    sock4.sin_port = CFSwapInt16HostToBig(80)
    // 设置sin_family 为 AF_INET表示着这个为IPv4 连接
    sock4.sin_family = sa_family_t(AF_INET)
    // Swift 中指针强转比OC要复杂
    let pointer: UnsafePointer<sockaddr> = withUnsafePointer(to: &sock4, {$0.withMemoryRebound(to: sockaddr.self, capacity: 1, {$0})})

    var result = Darwin.connect(socketFD, pointer, socklen_t(MemoryLayout.size(ofValue: sock4)))
    guard result != -1 else {
    fatalError("Error in connect() function code is \(errno)")
    }
    // 组装文本协议 访问 菜鸟教程Http教程
    let sendMessage = "GET /http/http-tutorial.html HTTP/1.1\r\n"
    + "Host: http://www.runoob.com\r\n"
    + "Connection: keep-alive\r\n"
    + "USer-Agent: Socket-Client\r\n\r\n"
    //转换成二进制
    guard let data = sendMessage.data(using: .utf8) else {
    fatalError("Error occur when transfer to data")
    }
    // 转换指针
    let dataPointer = data.withUnsafeBytes({UnsafeRawPointer($0)})

    let status = Darwin.write(socketFD, dataPointer, data.count)

    guard status != -1 else {
    fatalError("Error in write() function code is \(errno)")
    }
    // 设置32Kb字节存储防止溢出
    let readData = Data(count: 64 * 1024)

    let readPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)})
    // 记录当前读取多少字节
    var currentRead = 0

    while true {
    // 读取socket数据
    let result = Darwin.read(socketFD, readPointer + currentRead, readData.count - currentRead)

    guard result >= 0 else {
    fatalError("Error in read() function code is \(errno)")
    }
    // 这里睡眠是减少调用频率
    sleep(2)
    if result == 0 {
    print("无新数据")
    continue
    }
    // 记录最新读取数据
    currentRead += result
    // 打印
    print(String(data: readData, encoding: .utf8) ?? "")

    }

    对应代码例子已经放在github上,地址:https://github.com/chouheiwa/SocketTestExample

    总结
    越学习越觉得自己懂得越少,我们现在走的每一步,都是在学习。

    题外话:画图好费劲啊,都是用PPT画的-_-

    注: 本文原创,若希望转载请联系作者

    链接:https://www.jianshu.com/p/2b56a9cdf49d

    收起阅读 »

    Xcode 设置启动页

    前言:IOS 中设置启动页有两种方式 Launch Image 和 LaunchScreen一、Launch Image1.在工程 targets--Build Settings 搜索 Asset Catalog Launch Image Set Name 然...
    继续阅读 »

    前言:IOS 中设置启动页有两种方式 Launch Image 和 LaunchScreen

    一、Launch Image

    1.在工程 targets--Build Settings 搜索 Asset Catalog Launch Image Set Name 然后设置创建的启动页名字即可如下图所示。


    2.再在Info.plist中删除 Launch screen interface file base name并添加 Launch image并设置LaunchImage 

    3.资源文件中添加LaunchImage放入不同尺寸的图片,如何所示:


    4.删除已安装的App 重新打包


    二、Launch Screen

    1.再在Info.plist中添加 Launch screen interface file base name并设置LaunchScreen


    2.在工程 targets -- General 中 设置 Launch Screen File


    3.在LaunchScreen.storyboard文件中设计启动页样式


    end

    收起阅读 »

    iOS逆向-逆向比较实用的工具

    ChiselChisel is a collection of LLDB commands to assist in the debugging of iOS apps通过github上面说明安装一下pviews 找所有的视图pviews -u 查看上一层视图...
    继续阅读 »

    Chisel

    Chisel is a collection of LLDB commands to assist in the debugging of iOS apps
    通过github上面说明安装一下
    pviews 找所有的视图
    pviews -u 查看上一层视图
    pvc 打印所有的控制器
    pmethods 0x107da5370 打印所有方法
    pinternals 0x107da5370 打印所有成员
    fvc -v 0x107da5370,根据视图找到控制器
    fv



    flicker 会让视图闪烁两次

    LLDB

    search class 搜索对象
    methods 0x 方法
    b -a 0x02 下断点
    sbt 恢复方法符号

    cycript

    ./cycript 开始
    ctrl + d 退出
    首先要配置cycript,我这里面配置的是moneyDev,因为moneyDev里面包含cycript
    ./cycript -r 192.168.1.101:6666找到ip地址+:调试端口号默认6666

    cy# keyWd .recursiveDescription().toString()层级视图



    choose (UIButton)
    这个工具不会阻塞进程
    只要进程不被kill,ctrl+d在重新进入变量是都在的、
    使用自己的cy

    封装成脚本,在任意位置sh cyConnect.sh


    配置.zshrc



    使用


    这里面也可以使用pviews() pvcs()等


    转自链接:https://www.jianshu.com/p/a1c619e2da97
    收起阅读 »

    iOS逆向-18:LLDB调试

    在逆向环境中,拿不到源码,只能通过指令设置断点LLDB(Low Lever Debug)默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足...
    继续阅读 »

    在逆向环境中,拿不到源码,只能通过指令设置断点

    LLDB(Low Lever Debug)

    默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足实际需要。

    这里列举了一些常用的命令:

    断点设置

    • 设置断点
      breakpoint set -n XXX
      set 是子命令
      -n 是选项 是--name 的缩写!

    • 查看断点列表
      breakpoint list

    • 删除
      breakpoint delete 组号

    • 禁用/启用
      breakpoint disable 禁用
      breakpoint enable 启用

    • 遍历整个项目中满足Game:这个字符的所有方法
      breakpoint set -r Game:

    流程控制

    • 继续执行
      continue c

    • 单步运行,将子函数当做整体一步执行
      n next
      单步运行,遇到子函数会进去
      s

    • stop-hook
      让你在每次stop的时候去执行一些命令,只对breadpoint,watchpoint

    • 其他命令
      image list
      p expression 除了打印还可以执行一些代码
      b -[xxx xxx]
      x16进制打印
      register read 读寄存器
      po
      b -r xx断住所有包含的方法
      b -selector xx断住所有xx方法
      help xx查看指令

    函数调用栈

    bt //所有调用栈
    up //跳上层堆栈
    down
    frame select 12 跳指定下标堆栈
    frame variable 当前函数参数,只能修改当前函数参数
    thread return 代码回滚,直接返回,不执行后面的代码。提前返回,可以通过这种方式绕过hook

    内存断点

    Person *person = [Person new];
    person.name = "FY";
    下内存断点:
    watchpoint set variable person->_name
    watchpoint set expression 0x456 &person->_name
    当进行修改的时候就会触发内存断点


    然后我们bt一下

    可以看到方法的调用栈
    break command add 1
    在断点中添加一些指令

    让你在每次stop的时候去执行一些命令,只对breadpoint,watchpoint
    target stop-hook add -o "frame variable"
    target stop-hook add -o "po self.view"
    target stop-hook list

    这些指令也可以放到家目录下的.lldbinit中,只要lldb一启动就会执行里面的命令,一般的lldb插件就是在这个目录配置的

    cd ~ 进入家目录
    .lldbinit




    转自链接:https://www.jianshu.com/p/59123ee28503
    收起阅读 »

    Swift中的闭包

    一、简介闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 OC 中的代码块(blocks)以及其他一些编程语言中的 匿名函数 ...
    继续阅读 »

    一、简介

    闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。
    Swift 中的闭包与 C 和 OC 中的代码块(blocks)以及其他一些编程语言中的 匿名函数 比较相似。全局函数和嵌套函数其实就是特殊的闭包。

    由于之前对 Swift 中的闭包不太熟悉,所以在此归纳总结一下闭包的语法。

    二、语法

    Swift 中的闭包有很多优化的地方:

    根据上下文推断参数和返回值类型
    从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略 return)
    可以使用简化参数名,如$0, $1(从 0 开始,表示第 i 个参数...)
    提供了尾随闭包语法(Trailing closure syntax)
    闭包表达式 提供了一些语法优化,使得撰写闭包变得简单明了。下面 闭包表达式 的例子通过使用几次迭代展示了 sorted(by:) 方法定义和语法优化的方式。下面的每一次迭代都用更简洁的方式描述了相同的功能。🐳->🐘->🦛->🐷->🐔->🐹->🦟。

    sorted(by:) 函数介绍:
    Swift 标准库提供了名为 sorted(by:) 的方法,它会根据你所提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

    下面的闭包表达式示例使用 sorted(by:)方法对一个 String 类型的数组进行字母逆序排序。以下是初始数组:

    let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

    sorted(by:)方法接受一个 闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

    该例子对一个 String 类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool

    原始实现方式:

    提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 sorted(by:)方法的参数传入:

    func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
    }
    var reversedNames = names.sorted(by: backward)
    // 打印可得 reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

    然而,以这种方式来编写一个实际上很简单的表达式(a > b),确实太过繁琐了。对于这个例子来说,利用 闭包表达式语法 可以更好地构造一个 内联排序闭包

    闭包表达式语法:

    闭包表达式语法有如下的一般形式:

    { (parameters) -> return type in
    statements
    }

    闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。

    第一次精简——闭包语法:

    下面的例子展示了之前 backward(_:_:) 函数对应的闭包表达式版本的代码:

    reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
    })
    需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:) 函数类型声明相同。在这两种方式中,都写成了 (s1: String, s2: String) -> Bool。然而在内联闭包表达式中,函数和返回值类型都写在大括号 ,而不是大括号 
    关键字 in:闭包的函数体部分由关键字 in 引入。该关键字表示 “闭包的参数和返回值类型定义已经完成,闭包函数体即将开始”。

    该例中 sorted(by:) 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了 内联闭包

    第二次精简——根据上下文推断类型:

    因为排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。sorted(by:)方法被一个字符串数组调用,因此其参数必须是 (String, String) -> Bool 类型的函数。这意味着 (String, String)和 Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
    reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

    实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。

    第三次精简——单表达式闭包隐式返回:

    单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:

    reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

    在这个例子中,sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。因为闭包函数体只包含了一个单一表达式(s1 > s2),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略。

    第四次精简——参数名称缩写:

    Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0,$1,$2 来顺序调用闭包的参数,以此类推。

    如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
    reversedNames = names.sorted(by: { $0 > $1 } )

    在这个例子中,$0 和 $1 表示闭包中第一个和第二个 String 类型的参数。

    第五次精简——运算符方法:

    实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断出你想使用大于号的字符串函数实现:
    reversedNames = names.sorted(by: >)

    第六次精简——尾随闭包:

    什么是尾随闭包?
    如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用 尾随闭包 来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
    func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
    }

    // 以下是不使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure(closure: {
    // 闭包主体部分
    })

    // 以下是使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure() {
    // 闭包主体部分
    }

    在上面 sorted(by:) 方法参数的字符串排序闭包可以改写为:

    reversedNames = names.sorted() { $0 > $1 }

    如果闭包表达式是函数或方法的唯一参数,则当使用尾随闭包时,甚至可以把 () 省略掉:

    reversedNames = names.sorted { $0 > $1 }

    以上只是列举了闭包的一些基本语法与用法,还有一些其他概念需要继续学习,如 自动闭包、逃逸闭包,后续我会慢慢补齐总结的,谢谢!


    转自链接:https://www.jianshu.com/p/7043ffaac2f2
    收起阅读 »

    YTKNetwork的基本使用

    YTKNetwork是一个对AFNetworking封装的一个框架,虽然二者底层原理相同,但使用方法和使用效果是大不相同的。YTKNetwork 提供了以下更高级的功能:1.支持按时间缓存网络请求内容2.支持按版本号缓存网络请求内容3.支持统一设置服务器和 C...
    继续阅读 »

    YTKNetwork是一个对AFNetworking封装的一个框架,虽然二者底层原理相同,但使用方法和使用效果是大不相同的。YTKNetwork 提供了以下更高级的功能:

    1.支持按时间缓存网络请求内容
    2.支持按版本号缓存网络请求内容
    3.支持统一设置服务器和 CDN 的地址
    4.支持检查返回 JSON 内容的合法性
    5.支持文件的断点续传
    6.支持 block 和 delegate 两种模式的回调方式
    7.支持批量的网络请求发送,并统一设置它们的回调(实现在 YTKBatchRequest 类中)
    支持方便地设置有相互依赖的网络请求的发送,例如:发送请求 A,根据请求 A 的结果,选择性的发送请求 B
    和 C,再根据 B 和 C 的结果,选择性的发送请求 D。(实现在 YTKChainRequest 类中)
    支持网络请求 URL 的 filter,可以统一为网络请求加上一些参数,或者修改一些路径。


    YTKNetwork包含了这几个类:1、YTKNetworkConfig (设置域名) 2、YTKRequest (网络请求)3、YTKBatchRequest (请求多个类 )4、YTKChainRequest (依赖请求)5、YTKBaseRequest(YTKRequest的父类)

    YTKNetwork 的基本思想

    YTKNetwork 的基本的思想是把每一个网络请求封装成对象。所以使用 YTKNetwork,你的每一个请求都需要继承 YTKRequest 类,通过覆盖父类的一些方法来构造指定的网络请求。


    集约式和离散式API

    集约式API

    介绍:所有API的调用只有一个类,然后这个类接收API名字,API参数,以及回调着陆点,即项目中的每个请求都会走统一的入口,对外暴露了请求的 URL 和 Param 以及请求方式,入口一般都是通过单例 来实现,AFNetworking 的官方 demo 就是采用的集约式的方式对网络请求进行的封装,也是目前比较流行的网络请求方式。

    优点:使用便捷,能实现快速开发
    缺点:
    1.对每个请求的定制型不够强
    2.不方便后期业务拓展

    我们常用的AFNetworking框架就是集约式,在简单程序中AFNetworking 将请求逻辑写在 Controller 中比YTK更加方便,也不用一个个请求新建不同的request类。而YTKNetworking则是离散式的

    离散式API

    介绍:离散型API调用是这样的,一个API对应于一个APIManager,然后这个APIManager只需要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。即每个网络请求类都是一个对象,它的 URL 以及请求方式和响应方式 均不暴露给外部调用。只能内部通过 重载或实现协议 的方式来指定,外部调用只需要传 Param 即可,YTKNetwork就是采用的这种网络请求方式。


    优点:URL 以及请求和响应方式不暴露给外部,避免外部调用的时候写错
    业务方使用起来较简单,业务使用者不需要去关心它的内部实现
    可定制性强,可以为每个请求指定请求的超时时间以及缓存的周期
    缺点:
    网络层需要业务实现方去写,变相的增加了部分工作量
    文件增多,程序包会变大一些

    YTKNetworkConfig 类

    YTKNetworkConfig 类有两个作用:
    统一设置网络请求的服务器和 CDN 的地址。
    管理网络请求的 YTKUrlFilterProtocol 实例
    我们为什么需要统一设置服务器地址呢?因为:

    按照设计模式里的 Do Not Repeat Yourself 原则,我们应该把服务器地址统一写在一个地方。
    在实际业务中,我们的测试人员需要切换不同的服务器地址来测试。统一设置服务器地址到 YTKNetworkConfig 类中,也便于我们统一切换服务器地址。
    具体的用法是,在程序刚启动的回调中,设置好 YTKNetworkConfig 的信息,如下所示:

    - (BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig];
    config.baseUrl = @"http://yuantiku.com";
    config.cdnUrl = @"http://fen.bi";
    }

    设置好之后,所有的网络请求都会默认使用 YTKNetworkConfig 中 baseUrl 参数指定的地址。

    大部分企业应用都需要对一些静态资源(例如图片、js、css)使用 CDN。YTKNetworkConfig 的 cdnUrl 参数用于统一设置这一部分网络请求的地址。

    当我们需要切换服务器地址时,只需要修改 YTKNetworkConfig 中的 baseUrl 和 cdnUrl 参数即可。

    YTKRequest

    YTK把每个请求实例化,管理它的生命周期,也可以管理多个请求,在github的基础教程里面我们可以看到YTK是把每个网络请求都封装成对象,每一种网络请求继承 YTKRequest 类后,需要用方法覆盖(overwrite)的方式,来指定网络请求的具体信息。如下是一个示例:

    假如我们要向网址 http://www.yuantiku.com/iphone/register 发送一个 POST 请求,请求参数是 username 和 password。那么,这个类应该如下所示:

    // RegisterApi.h
    #import "YTKRequest.h"

    @interface RegisterApi : YTKRequest

    - (id)initWithUsername:(NSString *)username password:(NSString *)password;

    @end


    // RegisterApi.m

    #import "RegisterApi.h"

    @implementation RegisterApi {
    NSString *_username;
    NSString *_password;
    }

    - (id)initWithUsername:(NSString *)username password:(NSString *)password {
    self = [super init];
    if (self) {
    _username = username;
    _password = password;
    }
    return self;
    }

    - (NSString *)requestUrl {
    // “ http://www.yuantiku.com ” 在 YTKNetworkConfig 中设置,这里只填除去域名剩余的网址信息
    return @"/iphone/register";
    }

    - (YTKRequestMethod)requestMethod {
    return YTKRequestMethodPOST;
    }

    - (id)requestArgument {
    return @{
    @"username": _username,
    @"password": _password
    };
    }

    @end

    在上面这个示例中,我们可以看到:

    • 我们通过覆盖 YTKRequest 类的 requestUrl 方法,实现了指定网址信息。并且我们只需要指定除去域名剩余的网址信息,因为域名信息在 YTKNetworkConfig 中已经设置过了。
    • 我们通过覆盖 YTKRequest 类的 requestMethod 方法,实现了指定 POST 方法来传递参数。
    • 我们通过覆盖 YTKRequest 类的 requestArgument 方法,提供了 POST 的信息。这里面的参数 username 和 password 如果有一些特殊字符(如中文或空格),也会被自动编码。

    调用 RegisterApi

    在构造完成 RegisterApi 之后,具体如何使用呢?我们可以在登录的 ViewController 中,调用 RegisterApi,并用 block 的方式来取得网络请求结果:
    - (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
    RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
    [api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
    // 你可以直接在这里使用 self
    NSLog(@"succeed");
    } failure:^(YTKBaseRequest *request) {
    // 你可以直接在这里使用 self
    NSLog(@"failed");
    }];
    }
    }

    注意:你可以直接在 block 回调中使用 self,不用担心循环引用。因为 YTKRequest 会在执行完 block 回调之后,将相应的 block 设置成 nil。从而打破循环引用。

    除了 block 的回调方式外,YTKRequest 也支持 delegate 方式的回调:

    - (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
    RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
    api.delegate = self;
    [api start];
    }
    }

    - (void)requestFinished:(YTKBaseRequest *)request {
    NSLog(@"succeed");
    }

    - (void)requestFailed:(YTKBaseRequest *)request {
    NSLog(@"failed");
    }

    验证服务器返回内容

    有些时候,由于服务器的 Bug,会造成服务器返回一些不合法的数据,如果盲目地信任这些数据,可能会造成客户端 Crash。如果加入大量的验证代码,又使得编程体力活增加,费时费力。

    使用 YTKRequest 的验证服务器返回值功能,可以很大程度上节省验证代码的编写时间。

    例如,我们要向网址 http://www.yuantiku.com/iphone/users 发送一个 GET 请求,请求参数是 userId 。我们想获得某一个用户的信息,包括他的昵称和等级,我们需要服务器必须返回昵称(字符串类型)和等级信息(数值类型),则可以覆盖 jsonValidator 方法,实现简单的验证。

    - (id)jsonValidator {
    return @{
    @"nick": [NSString class],
    @"level": [NSNumber class]
    };
    }

    断点续传

    要启动断点续传功能,只需要覆盖 resumableDownloadPath 方法,指定断点续传时文件的存储路径即可,文件会被自动保存到此路径。如下代码将刚刚的取图片的接口改造成了支持断点续传:

    @implementation GetImageApi {
    NSString *_imageId;
    }

    - (id)initWithImageId:(NSString *)imageId {
    self = [super init];
    if (self) {
    _imageId = imageId;
    }
    return self;
    }

    - (NSString *)requestUrl {
    return [NSString stringWithFormat:@"/iphone/images/%@", _imageId];
    }

    - (BOOL)useCDN {
    return YES;
    }

    - (NSString *)resumableDownloadPath {
    NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *cachePath = [libPath stringByAppendingPathComponent:@"Caches"];
    NSString *filePath = [cachePath stringByAppendingPathComponent:_imageId];
    return filePath;
    }

    @end

    按时间缓存内容

    刚刚我们写了一个 GetUserInfoApi ,这个网络请求是获得用户的一些资料。

    我们想像这样一个场景,假设你在完成一个类似微博的客户端,GetUserInfoApi 用于获得你的某一个好友的资料,因为好友并不会那么频繁地更改昵称,那么短时间内频繁地调用这个接口很可能每次都返回同样的内容,所以我们可以给这个接口加一个缓存。

    在如下示例中,我们通过覆盖 cacheTimeInSeconds 方法,给 GetUserInfoApi 增加了一个 3 分钟的缓存,3 分钟内调用调 Api 的 start 方法,实际上并不会发送真正的请求。

    @implementation GetUserInfoApi {
    NSString *_userId;
    }

    - (id)initWithUserId:(NSString *)userId {
    self = [super init];
    if (self) {
    _userId = userId;
    }
    return self;
    }

    - (NSString *)requestUrl {
    return @"/iphone/users";
    }

    - (id)requestArgument {
    return @{ @"id": _userId };
    }

    - (id)jsonValidator {
    return @{
    @"nick": [NSString class],
    @"level": [NSNumber class]
    };
    }

    - (NSInteger)cacheTimeInSeconds {
    // 3 分钟 = 180 秒
    return 60 * 3;
    }

    @end

    该缓存逻辑对上层是透明的,所以上层可以不用考虑缓存逻辑,每次调用 GetUserInfoApi 的 start 方法即可。GetUserInfoApi 只有在缓存过期时,才会真正地发送网络请求。



    转自链接:https://www.jianshu.com/p/8213f3e3b0ea
    收起阅读 »

    objc_msgSend cache查找

    分析objc_msgSend中缓存的查找逻辑以及汇编代码是如何进入c/c++代码的。一、CacheLookup 查找缓存1.1 CacheLookup源码分析传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncache...
    继续阅读 »

    分析objc_msgSend中缓存的查找逻辑以及汇编代码是如何进入c/c++代码的。

    一、CacheLookup 查找缓存

    1.1 CacheLookup源码分析

    传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncached


    //NORMAL, _objc_msgSend, __objc_msgSend_uncached

    .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    // requirements:
    // //缓存不存在返回NULL,x0设置为0
    // GETIMP:
    // The cache-miss is just returning NULL (setting x0 to 0)
    // 参数说明
    // NORMAL and LOOKUP:
    // - x0 contains the receiver
    // - x1 contains the selector
    // - x16 contains the isa
    // - other registers are set as per calling conventions
    //
    //调用过来的p16存储的是cls,将cls存储在x15.
    mov x15, x16 // stash the original isa
    //_objc_msgSend
    LLookupStart\Function:
    // p1 = SEL, p16 = isa
    //arm64 64 OSX/SIMULATOR
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //isa->cache,首地址也就是_bucketsAndMaybeMask
    ldr p10, [x16, #CACHE] // p10 = mask|buckets
    //lsr逻辑右移 p11 = _bucketsAndMaybeMask >> 48 也就是 mask
    lsr p11, p10, #48 // p11 = mask
    //p10 = _bucketsAndMaybeMask & 0xffffffffffff = buckets(保留后48位)
    and p10, p10, #0xffffffffffff // p10 = buckets
    //x12 = cmd & mask w1为第二个参数cmd(self,cmd...),w11也就是p11 也就是执行cache_hash。这里没有>>7位的操作
    and w12, w1, w11 // x12 = _cmd & mask
    //arm64 64 真机这里p11计算后是_bucketsAndMaybeMask
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
    //arm64 + iOS + !模拟器 + 非mac应用
    #if CONFIG_USE_PREOPT_CACHES
    //iphone 12以后指针验证
    #if __has_feature(ptrauth_calls)
    //tbnz 测试位不为0则跳转。与tbz对应。 p11 第0位不为0则跳转 LLookupPreopt\Function。
    tbnz p11, #0, LLookupPreopt\Function
    //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff // p10 = buckets
    #else
    //p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
    and p10, p11, #0x0000fffffffffffe // p10 = buckets
    //p11 第0位不为0则跳转 LLookupPreopt\Function。
    tbnz p11, #0, LLookupPreopt\Function
    #endif
    //eor 逻辑异或(^) 格式为:EOR{S}{cond} Rd, Rn, Operand2
    //p12 = selector ^ (selector >> 7) select 右移7位&自己给到p12
    eor p12, p1, p1, LSR #7
    //p12 = p12 & (_bucketsAndMaybeMask >> 48) = index & mask值 = buckets中的下标
    and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else
    //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff // p10 = buckets
    //p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下标
    and p12, p1, p11, LSR #48 // x12 = _cmd & mask
    #endif // CONFIG_USE_PREOPT_CACHES
    //arm64 32
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //后4位为mask前置0的个数的case
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
    and p10, p11, #~0xf // p10 = buckets 相当于后4位置为0,取前32位
    and p11, p11, #0xf // p11 = maskShift 取的是后4位,为mask前置位的0的个数
    mov p12, #0xffff
    lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
    and p12, p1, p11 // x12 = _cmd & mask
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    //通过上面的计算 p10 = buckets,p11 = mask(arm64真机是_bucketsAndMaybeMask), p12 = index
    // p13(bucket_t) = buckets + 下标 << 4 PTRSHIFT arm64 为3. <<4 位为16字节 buckets + 下标 *16 = buckets + index *16 也就是直接平移到了第几个元素的地址。
    add p13, p10, p12, LSL #(1+PTRSHIFT)
    // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    //这里就直接遍历查找了,因为arm64下cache_next相当于遍历(这里只扫描了前面)
    // do {
    //p17 = imp, p9 = sel
    1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
    //sel - _cmd != 0 则跳转 3:,也就意味着没有找到就跳转到__objc_msgSend_uncached
    cmp p9, p1 // if (sel != _cmd) {
    b.ne 3f // scan more
    // } else {
    //找到则调用或者返回imp,Mode为 NORMAL
    2: CacheHit \Mode // hit: call or return imp 命中
    // }
    //__objc_msgSend_uncached
    //缓存中找不到方法就走__objc_msgSend_uncached逻辑了。
    //cbz 为0跳转 sel == nil 跳转 \MissLabelDynamic
    3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; 有空位没有找到说明没有缓存
    //bucket_t - buckets 由于是递减操作
    cmp p13, p10 // } while (bucket >= buckets) //⚠️ 这里一直是往前找,后面的元素在后面还有一次循环。
    //无符号大于等于 则跳转1:f b 分别代表front与back
    b.hs 1b

    //没有命中cache 查找 p13 = mask对应的元素,也就是倒数第二个
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //p13 = buckets + (mask << 4) 平移找到对应mask的bucket_t。UXTW 将w11扩展为64位后左移4
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
    // p13 = buckets + (mask << 1+PTRSHIFT)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //p13 = buckets + (mask >> 44) 这里右移44位,少移动4位就不用再左移了。因为maskZeroBits的存在 就找到了mask对应元素的地址
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
    // p13 = buckets + (mask << 1+PTRSHIFT)
    // see comment about maskZeroBits
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //p13 = buckets + (mask << 4) 找到对应mask的bucket_t。
    add p13, p10, p11, LSL #(1+PTRSHIFT)
    // p13 = buckets + (mask << 1+PTRSHIFT)
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    //p12 = buckets + (p12<<4) index对应的bucket_t
    add p12, p10, p12, LSL #(1+PTRSHIFT)
    // p12 = first probed bucket

    //之前已经往前查找过了,这里从后往index查找
    // do {
    //p17 = imp p9 = sel
    4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
    //sel - _cmd
    cmp p9, p1 // if (sel == _cmd)
    //sel == _cmd跳转CacheHit
    b.eq 2b // goto hit
    //sel != nil
    cmp p9, #0 // } while (sel != 0 &&
    //
    ccmp p13, p12, #0, ne // bucket > first_probed)
    //有值跳转4:
    b.hi 4b

    LLookupEnd\Function:
    LLookupRecover\Function:
    //仍然没有找到缓存,缓存彻底不存在 __objc_msgSend_uncached()
    b \MissLabelDynamic

    核心逻辑:

    • 根据不同架构找到bucketssel对应的indexp10 = buckets,p11 = mask / _bucketsAndMaybeMask(arm64_64 是 _bucketsAndMaybeMask),p12 = index
      • arm64_64的情况下如果_bucketsAndMaybeMask0位为1则执行LLookupPreopt\Function
    • p13 = buckets + index << 4找到cls对应的buckets地址,地址平移找到对应bucket_t
    • do-while循环扫描buckets[index]的前半部分(后半部分逻辑不在这里)。
      • 如果存在sel为空,则说明是没有缓存的,就直接__objc_msgSend_uncached()
      • 命中直接CacheHit \Mode,这里ModeNORMAL
    • 平移获得p13 = buckets[mask]对应的元素,也就是倒数第二个(倒数第一个为buckets地址)。
    • p13 = buckets + mask << 4找到mask对应的buckets地址,地址平移找到对应bucket_t
    • do-while循环扫描buckets[mask]的前面元素,直到index(不包含index)。
      • 命中CacheHit \Mode
      • 如果存在sel为空,则说明是没有缓存的,就直接结束循环。
    • 最终仍然没有找到则执行__objc_msgSend_uncached()
    1. CACHEcache_t相对isa的偏移。 #define CACHE (2 * __SIZEOF_POINTER__)
    2. maskZeroBits始终是40p13 = buckets + (_bucketsAndMaybeMask >> 44)右移44位后就不用再<<4找到对应bucket_t的地址了。这也是maskZeroBitsarm64_64下存在的意义。
    3. f b 分别代表frontback,往下往上的意思。

    1.2 CacheLookup 伪代码实现


    //NORMAL, _objc_msgSend, __objc_msgSend_uncached

    void CacheLookup(Mode,Function,MissLabelDynamic,MissLabelConstant) {
    //1. 根据架构不同集算sel在buckets中的index
    if (arm64_64 && OSX/SIMULATOR) {
    p10 = isa->cache //_bucketsAndMaybeMask
    p11 = _bucketsAndMaybeMask >> 48//mask
    p10 = _bucketsAndMaybeMask & 0xffffffffffff//buckets
    x12 = sel & mask //index 也就是执行cache_hash
    } else if (arm64_64) {//真机 //这个分支下没有计算mask
    p11 = isa->cache //_bucketsAndMaybeMask
    if (arm64 + iOS + !模拟器 + 非mac应用) {
    if (开启指针验证 ) {
    if (_bucketsAndMaybeMask 第0位 != 0) {
    goto LLookupPreopt\Function
    } else
    {
    p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff//buckets
    }
    } else {
    p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe //buckets
    if (_bucketsAndMaybeMask 第0位 != 0) {
    goto LLookupPreopt\Function
    }
    }
    //计算index
    p12 = selector ^ (selector >> 7)
    p12 = p12 & (_bucketsAndMaybeMask & 48) = p12 & mask//index
    } else
    {
    p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff //buckets
    p12 = selector & (_bucketsAndMaybeMask >>48) //index
    }
    } else if (arm64_32) {
    p11 = _bucketsAndMaybeMask
    p10 = _bucketsAndMaybeMask &(~0xf//buckets 相当于后4位置为0,取前32位
    p11 = _bucketsAndMaybeMask & 0xf //mask前置位0的个数
    p11 = 0xffff >> p11 //获取到mask的值
    x12 = selector & mask //index
    } else {
    #error Unsupported cache mask storage for ARM64.
    }

    //通过上面的计算 p10 = buckets,p11 = mask/_bucketsAndMaybeMask, p12 = index
    p13 = buckets + index << 4 //找到cls对应的buckets地址。地址平移找到对应bucket_t。

    //2.找缓存(这里只扫描了前面)
    do {
    p13 = *bucket-- //赋值后指向前一个bucket
    p17 = bucket.imp
    p9 = bucket.sel
    if (p9 != selector) {
    if (p9 == 0) {//说明没有缓存
    __objc_msgSend_uncached()
    }
    } else {//缓存命中,走命中逻辑 call or return imp
    CacheHit \Mode
    }
    } while(bucket >= buckets) //buckets是首地址,bucket是index对应的buckct往前移动

    //查找完后还没有缓存?
    //查找 p13 = mask对应的元素,也就是倒数第二个
    if (arm64_64 && OSX/SIMULATOR) {
    p13 = buckets + (mask << 4)
    } else if (arm64_64) {//真机
    p13 = buckets + (_bucketsAndMaybeMask >> 44)//这里右移44位,少移动4位就不用再左移了。这里就找到了对应index的bucket_t。
    } else if (arm64_32) {
    p13 = buckets + (mask << 4)
    } else {
    #error Unsupported cache mask storage for ARM64.
    }

    //index的bucket_t 从mask对应的buckets开始再往前找
    p12 = buckets + (index<<4)
    do {
    p17 = imp;
    p9 = sel;
    *p13--;
    if (p9 == selector) {//命中
    CacheHit \Mode
    }
    } while (p9 != nil && bucket > p12)//从后往前 p9位nil则证明没有存,也就不存在缓存了。

    //仍然没有找到缓存,缓存彻底不存在。
    __objc_msgSend_uncached()
    }

    二、LLookupPreopt\Function

    arm64_64真机的情况下,如果_bucketsAndMaybeMask的第0位为1则会执行LLookupPreopt\Function的逻辑。简单看了下汇编发现与cache_t 中的_originalPreoptCache有关。

    2.1 LLookupPreopt\Function 源码分析

    LLookupPreopt\Function:

    #if __has_feature(ptrauth_calls)
    //p10 = _bucketsAndMaybeMask & 0x007ffffffffffffe = buckets
    and p10, p11, #0x007ffffffffffffe // p10 = x
    //buckets x16为cls 验证
    autdb x10, x16 // auth as early as possible
    #endif

    // x12 = (_cmd - first_shared_cache_sel)
    //(_cmd >> 12 + PAGE) << 12 + PAGEOFF 第一个sel
    adrp x9, _MagicSelRef@PAGE
    ldr p9, [x9, _MagicSelRef@PAGEOFF]
    //差值index
    sub p12, p1, p9

    // w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
    #if __has_feature(ptrauth_calls)
    // bits 63..60 of x11 are the number of bits in hash_mask
    // bits 59..55 of x11 is hash_shift

    // 取到 hash_shift...
    lsr x17, x11, #55 // w17 = (hash_shift, ...)
    //w9 = index >> hash_shift
    lsr w9, w12, w17 // >>= shift
    //x17 = _bucketsAndMaybeMask >>60 //mask_bits
    lsr x17, x11, #60 // w17 = mask_bits
    mov x11, #0x7fff
    //x11 = 0x7fff >> mask_bits //mask
    lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
    //x9 = x9 & mask
    and x9, x9, x11 // &= mask
    #else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
    lsr w9, w12, w17 // >>= shift
    and x9, x9, x11, LSR #53 // &= mask
    #endif
    //x17 = el_offs | (imp_offs << 32)
    ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
    // cmp x12 x17 是否找到sel
    cmp x12, w17, uxtw

    .if \Mode == GETIMP
    b.ne \MissLabelConstant // cache miss
    //imp = isa - (sel_offs >> 32)
    sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
    //注册imp
    SignAsImp x0
    ret
    .else
    b.ne 5f // cache miss
    //imp(x17) = (isa - sel_offs>> 32)
    sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
    .if \Mode == NORMAL
    //跳转imp
    br x17
    .elseif \Mode == LOOKUP
    //x16 = isa | 3 //这里为或的意思
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    //注册imp
    SignAsImp x17
    ret
    .else
    .abort unhandled mode \Mode
    .endif
    //x9 = buckets-1
    5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
    //计算回调isa x16 = x16 + x9
    add x16, x16, x9 // compute the fallback isa
    //使用新isa重新查找缓存
    b LLookupStart\Function // lookup again with a new isa
    .endif
    • 找到imp就跳转/返回。
    • 没有找到返回下一个isa重新CacheLookup

    ⚠️@TODO 真机调试的时候进不到这块流程,这块分析的还不是很透彻,后面再补充。

    三、CacheHit

    在查找缓存命中后会执行CacheHit

    3.1 CacheHit源码分析

    #define NORMAL 0

    #define GETIMP 1
    #define LOOKUP 2

    // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
    .macro CacheHit
    //这里传入的为NORMAL
    .if $0 == NORMAL
    //调用imp TailCallCachedImp(imp,buckets,sel,isa)
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
    .elseif $0 == GETIMP
    //返回imp
    mov p0, p17
    //imp == nil跳转9:
    cbz p0, 9f // don't ptrauth a nil imp
    //有imp执行AuthAndResignAsIMP(imp,buckets,sel,isa)最后给到x0返回。
    AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
    9: ret // return IMP
    .elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    //找imp(imp,buckets,sel,isa)
    AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
    //isa与x15比较
    cmp x16, x15
    //cinc如果相等 就将x16+1,否则就设成0.
    cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
    ret // return imp via x17
    .else
    .abort oops
    .endif
    .endmacro
    • 这里其实走的是NORMAL逻辑,NORMALcase直接验证并且跳转imp
    • TailCallCachedImp内部执行的是imp^cls,对imp进行了解码。
    • GETIMP返回imp
    • LOOKUP查找注册imp并返回。

    3.1 CacheHit伪代码实现

    //x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa

    void CacheHit(Mode) {
    if (Mode == NORMAL) {
    //imp = imp^cls 解码
    TailCallCachedImp x17, x10, x1, x16 // 解码跳转imp
    } else if (Mode == GETIMP) {
    p0 = IMP
    if (p0 == nil) {
    return
    } else {
    AuthAndResignAsIMP(imp,buckets,sel,isa)//resign cached imp as IMP
    }
    } else if (Mode == LOOKUP) {
    AuthAndResignAsIMP(x17, buckets, sel, isa)//resign cached imp as IMP
    if (isa == x15) {
    x16 += 1
    } else {
    x16 = 0
    }
    } else {
    .abort oops//报错
    }
    }

    四、__objc_msgSend_uncached

    在缓存没有命中的情况下会走到__objc_msgSend_uncached()的逻辑:

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    //查找imp
    MethodTableLookup
    //跳转imp
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
    • MethodTableLookup查找imp
    • TailCallFunctionPointer跳转imp

    MethodTableLookup

    .macro MethodTableLookup

        
    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    //x2 = cls
    mov x2, x16
    //x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver
    //_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    mov x3, #3
    bl _lookUpImpOrForward

    // IMP in x0
    mov x17, x0

    RESTORE_REGS MSGSEND

    .endmacro
    • 调用_lookUpImpOrForward查找imp。这里就调用到了c/c++的代码了:
    • IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

    最终会调用_lookUpImpOrForward进入c/c++环境逻辑。

    对于架构的一些理解
    LP64 //64位
    x86_64 // interl 64位
    i386 // intel 32位
    arm // arm指令 32 位
    arm64 //arm64指令
    arm64 && LP64 //arm64 64位
    arm64 && !LP64 //arm64 32 位

    五、 objc_msgSend流程图

    objc_msgSend流程图

    总结

    • 判断receiver是否存在。
    • 通过isa获取cls
    • cls内存平移0x10获取cache也就是_bucketsAndMaybeMask
    • 通过buckets & bucketsMask获取buckets`地址。
    • 通过bucketsMask >> maskShift获取mask
    • 通过sel & mask获取第一次查找的index
    • buckets + index << 4找到index对应的地址。
    • do-while循环判断找缓存,这次从[index~0]查找imp
    • 取到buckets[mask]继续do-while循环,从[mask~index)查找imp。两次查找过程中如果有sel为空则会结束查找。走__objc_msgSend_uncached的逻辑。
    • 找到imp就解码跳转imp


    作者:HotPotCat
    链接:https://www.jianshu.com/p/c29c07a1e93d

    收起阅读 »

    iOS开发笔记(十一)— UITableView、ARC、xcconfig、Push

    前言分享iOS开发中遇到的问题,和相关的一些思考,本次内容包括:UITableView滚动问题、ARC、xcconfig、Push证书。正文UITableViewUITableView在reloadData 的时候,如果height的高度发生较大变化,cont...
    继续阅读 »

    前言

    分享iOS开发中遇到的问题,和相关的一些思考,本次内容包括:UITableView滚动问题、ARC、xcconfig、Push证书。

    正文

    UITableView

    UITableView在reloadData 的时候,如果height的高度发生较大变化,contentOffset无法保持原来的大小时,会发生滚动的效果。如果直接reloadData再setContentOffset:设置位置,仍会出现滚动的效果。
    如果需要去除该滚动效果,可以在reloadData之后,调用scrollToRowAtIndexPath并设置animated:NO,最后再用setContentOffset:微调位置。
    同理,如果需要在reloadData后,手动scroll到header时,可用同上的解决方案。

    UITableView还有类似的问题,如果列表项过多时,scrollToRowAtIndexPath有时并不准确,比如有1000行时滚动到第500行,此时可能会出现滚到501或者499行的情况。
    究其原因,是因为UITableView不会调用1~499行所有的heightFor和cellFor方法,所以无法准确计算出来位置。
    从这里去分析,如果需要滚动到准确的位置,可以用estimatedRowHeight的属性,设置和行高一样的高度;在行高各不相同的场景,可以设置estimatedRowHeight为大致的数字,在scrollToRowAtIndexPath之后通过setContentOffset:微调位置。

    // 解决部分 UITableView不滚动的问题,实现的效果是某个cell在点击后就扩展高度
    - (void)onMoreContentClick:(SSBookDetailContentCell *)cell {
    ++self.showNums;
    CGPoint offset = self.contentTableView.contentOffset;
    [self.contentTableView beginUpdates];
    [self.contentTableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
    [self.contentTableView endUpdates];
    [self.contentTableView.layer removeAllAnimations];
    [self.contentTableView setContentOffset:offset animated:NO];
    }

    ARC

    Automatic Reference Counting(ARC)是编译器特性,由编译器插入对象内存管理代码,实现内存管理。
    如果仅仅是retain/release的管理,非常容易理解,但是插入的代码如何实现weak、strong这些运行时特性?
    最近同事遇到一个问题,以下代码会crash:
    他实现了一个editingButton的getter,同时在dealloc的时候将其移除;
    如果editingButton在整个生命周期都没有初始化时,则在dealloc使用getter会触发初始化,然后在下面的weakify(self);这一行crash。

    - (void)dealloc
    {
    [self.editingView removeFromSuperview];
    [self.editingButton removeFromSuperview]; // crash
    }

    - (UIButton *)editingButton
    {
    if (!_editingButton)
    {
    _editingButton = [UIButton buttonWithType:UIButtonTypeCustom];
    ......
    weakify(self); // CRASH
    [_editingButton ss_addEventHandler:^(id _Nonnull sender) {
    ......
    } forControlEvents:UIControlEventTouchDown];
    }
    return _editingButton;
    }

    闪退的堆栈如下


    在ARC的文档中找到闪退的方法,其中有一段描述如下:


    当dealloc开始的时候,weakSelf的指针应该都已经被重置为nil;如果在dealloc的函数中再次初始化weakSelf指针会出现异常。

    另外,在dealloc方法执行属性的getter方法也是不合理,因为属性的getter方法大都包括如果未创建就创建并初始化的逻辑。
    ARC的文档 这份文档也是非常好的ARC学习资料。

    xcconfig

    xcconfig是用来保存build setting键值对的文件,里面是一些纯文本;

    //:configuration = Debug
    PRODUCT_BUNDLE_IDENTIFIER = com.loyinglin.dev
    DISPLAY_NAME = 测试标题
    PRODUCT_NAME = Learning
    GCC_TREAT_WARNINGS_AS_ERRORS = YES

    //:configuration = Debug
    GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 SSDEBUG=1

    比如这里配置是一份debug的xcconfig,其中PRODUCT_BUNDLE_IDENTIFIER = com.loyinglin.dev的键值会在编译的时候生效。
    xcconfig有什么用?
    一个Xcode工程,一定会有Debug的开发环境和Release的发布环境,可能会有Testflight的灰度环境、DailyBuild的持续集成环境、XXLanguage的多语言环境、TestCoverage的覆盖率测试环境、IAP的内购测试环境等;每个环境所用的证书不同,APP安装后显示的名字不同,provision file也不同等等。
    一种方案是使用Target来解决,公用的部分设置在project,每个环境根据各自特点自定义某些设置;这样带来的后果是target数量增多明显,而target增多带来的后果是当需要新增extension的时候会工作量巨大,并且多环境的管理难度加剧。
    另外一种方案是使用Configuration来区分环境,而xcconfig就是用来管理Configuration的文件。

    如何创建和使用xcconfig?

    1、在Xcode中新建文件,输入config,选择configuration settings file;这一步是创建xcconfig的文件。


    2、在Xcode中选中工程,在configurations中选择需要配置的选项,这里以debug为例,点击后选择刚刚已经创建的xcconfig,则可以把xcconfig和debug的编译选项绑定在一起。


    如果你用了cocoaPod,你会发现这一项已经有了CocoaPod创建xcconfig,如果选择了自己新建的xcconfig,则会编译失败;
    此时可以在自己新建的xcconfig头文件中加入以下代码:

    #include "Pods/Target Support Files/Pods-YourName/Pods-YourName.debug.xcconfig"

    注意需要修改成自己的工程名。

    3、在build setting选中某个配置项,cmd+c复制然后到xcconfig的文件中,cmd+v就可以复制配置项到xcconfig中。
    注意如果这个配置项在build setting已经有自定义值,需要将其删除,原因下面解释。


    xcconifg的配置和工程默认配置、手动在build setting配置有什么区别?

    配置的结果优先级不同,我的理解是:
    a、project默认配置是最低优先级,因为是最基础的配置;
    b、target配置基于project,但target默认会添加一些配置,优先级比上面高;
    c、xcconfig的配置是target某个config的配置,优先级比上面高;
    d、target的build setting中直接添加的配置项,优先级比上面高;


    知道上面的关系后,我们可以解决使用xcconifg时,CI 打包xcconifg配置项不生效的问题:
    检查是否对应配置项是否在target的build setting中直接添加;

    如果需要新增某个configuration,可以直接duplicate已有的configuration,但是如果使用Pods需要重新pod install,以生成对应的pod工程的配置项,否则会出现下图的报错:


    Push 证书

    .p12是连接苹果APNs服务器的证书(公钥+私钥);
    .cer 是苹果的证书文件(公钥);
    .pem是OpenSSL的证书文件(公钥+私钥);
    当我们生成push证书时,其实就是将我们本地的p12通过脚本,导出对应的pem文件;
    下面是一段常用的脚本:

    P12_CERT=AppStorePush.p12 # p12证书文件
    PASSWD=loying # p12密码

    EXPORT_CERT=AppStorePush.pem # 导出pem证书
    EXPORT_KEY=AppStorePushWithKey.pem # 导出的pem私钥,有密码
    EXPORT_KEY_UNENCRY=AppStorePushWithoutKey.pem # 导出的pem私钥,无密码
    EXPORT_KEY_AND_CERT=AppStore_ck.pem # 含有证书和私钥的pem

    openssl pkcs12 -clcerts -nokeys -out ${EXPORT_CERT} -in ${P12_CERT} -passin pass:${PASSWD} # 导出证书

    openssl pkcs12 -nocerts -passout pass:${PASSWD} -out ${EXPORT_KEY} -in ${P12_CERT} -passin pass:${PASSWD} #导出私钥,有密码

    openssl rsa -in ${EXPORT_KEY} -passin pass:${PASSWD} -out ${EXPORT_KEY_UNENCRY} # 导出私钥,无密码

    cat ${EXPORT_CERT} ${EXPORT_KEY_UNENCRY} > ${EXPORT_KEY_AND_CERT} # 证书和私钥合起来

    openssl s_client -connect gateway.push.apple.com:2195 -cert ${EXPORT_CERT} -key ${EXPORT_KEY_UNENCRY} # 测试 push证书

    # gateway.push.apple.com
    # gateway.sandbox.push.apple.com

    在调试Push的时候,以下这个软件(App Store可以下载)非常便捷:


    使用时配置好证书(可以点击connect验证是否连接APNs成功),再从iPhone获取到deviceToken添加到设备列表,便可以使用推送。

    总结

    这些都是在项目中遇到的一些问题,UITableView这个是老生常谈,ARC那篇文档是很好的学习资料,xcconfig需要多研究,未来随着版本和渠道增多会越来越复杂,Push在Easy APNs Provider这个软件出来后就很好测试,再也不用登录信鸽去手动配置Push。
    新的一年,继续搬砖和学习。

    链接:https://www.jianshu.com/p/0093cb8c5a35

    收起阅读 »

    iOS股票K线图、分时图绘制

    介绍:1、这是以雪球APP为原型,基于 iOS的K线开源项目。2、该项目整体设计思路已经经过某成熟证券APP的商业认证。3、本项目将K线业务代码尽可能缩减,保留核心功能,可流畅、高效实现手势交互。4、K线难点在于手势交互和数据动态刷新上,功能并不复杂,关键在于...
    继续阅读 »

    介绍:

    1、这是以雪球APP为原型,基于 iOS的K线开源项目。
    2、该项目整体设计思路已经经过某成熟证券APP的商业认证。
    3、本项目将K线业务代码尽可能缩减,保留核心功能,可流畅、高效实现手势交互。
    4、K线难点在于手势交互和数据动态刷新上,功能并不复杂,关键在于设计思路。

    演示:


    建议:

    如果搭建K线为公司业务,不建议采用集成度高的开源代码。庞大臃肿,纵然短期匆忙上线,难以应付后期灵活需求变更。
    Objective-C版请移步 https://github.com/cclion/CCLKLineChartView
    Swift版请移步 https://github.com/cclion/KLineView

    设计思路&&难点:

    K线难点在于手势的处理上,捏合、长按、拖拽都需要展示不同效果。以下是Z君当时做K线时遇到的问题的解决方案;

    1. 捏合手势需要动态改变K线柱的宽度,对应的增加或减少当前界面K线柱的展示数量,并且根据当前展示的数据计算出当前展示数据的极值。
    采用UITableView类实现,将K线柱封装在cell中,在tableview中监听捏合手势,对应改变cell的高度,同时刷新cell中K线柱的布局来实现动态改变K线柱的宽度。

    采用UITableView还有一个好处就是可以采用cell的重用机制降低内存。

    注意:因为UITableView默认是上下滑动,而K线柱是左右滑动,Z君这里将UITableView做了一个顺时针90°的旋转。


    2. K线柱绘制

    K线柱采用CAShapeLayer配合UIBezierPath绘制,内存低,效率高,棒棒哒!

    关于CAShapeLayer的使用大家可以看这篇 https://zsisme.gitbooks.io/ios-/content/chapter6/cashapelayer.html
    (现在的google、baidu,好文章都搜不到,一搜全是简单调用两个方法就发的博客,还是翻了两年前的收藏才找到这个网站,强烈推荐大家)

    3. 捏合时保证捏合中心点不变,两边以捏合中间点为中心进行收缩或扩散

    因为UITableView在改变cell的高度时,默认时不会改变偏移量,所以不能保证捏合的中心点不变,这里我们的小学知识就会用上了。


    我们可以通过变量定义控件间距离。


    保证捏合中心点的中K线柱的中心点还在捏合前,就需要c1 = c2 ,计算出O2,在捏合完,设置偏移量为O2即可。


    4. K线其他线性指标如何绘制

    在K线中除了K线柱之外,还有其他均线指标,连贯整个数据显示区。


    由图可以看出均线指标由每个cell中心点的数据连接相邻的cell中心点的数据。我们依旧将绘制放在cell中,将相连两个cell的线分割成两段,分别在各自所属的cell中绘制。

    需要我们做的就是就是在cell中传入相邻的cell的soureData,计算出相邻中点的位置,分为两段绘制。


    大家针对K线有什么问题都可以在下面留言,会第一时间解答。
    未完待续

    转自:https://www.jianshu.com/p/104857287bc4

    收起阅读 »

    WebKit的使用

    Web view 用于加载和显示丰富的网络内容。例如,嵌入 HTML 和网站。Mail app 使用 web view 显示邮件中的 HTML 内容。iOS 8 和 macOS 10.10 中引入了WebKit framework,用以取代UIWebView和...
    继续阅读 »

    Web view 用于加载和显示丰富的网络内容。例如,嵌入 HTML 和网站。Mail app 使用 web view 显示邮件中的 HTML 内容。


    iOS 8 和 macOS 10.10 中引入了WebKit framework,用以取代UIWebView和WebView。同时,在两个平台上提供同一API。与UIWebView相比,WebKit 有以下优势:使用了与 Safari 一样的 JavaScript engine,在运行脚本前,将脚本编译为机器代码,速度更快;支持多进程架构,Web 内容在单独的线程中运行,WKWebView崩溃不会影响 app运行;能够以60fps滑动。另外,在 iOS 12 和 macOS 10.14中,UIWebView和WebView已经被正式弃用。

    WebKit framework 提供了多个类和协议,用于在窗口中显示网络内容,并实现类似浏览器功能。例如,点击链接时显示链接内容,维护前进、后退列表,维护最近访问列表。加载网页内容时,异步从 HTTP 服务器请求内容,其响应 (response) 可能以增量、随机顺序到达,也可能因网络原因部分到达,而 WebKit 极大简化了这些过程。WebKit 框架还简化了显示各种 MIME 类型内容过程,以及管理视图中各元素滚动条。

    WebKit 框架中的方法、函数只能在主线程或主队列中调用。

    1. WKWebView
    WKWebView是 WebKit framework 的核心。在 app 内使用WKWebView插入网络内容步骤如下:

    1、创建WKWebView对象。
    2、将WKWebView设置为要显示的视图。
    3、向WKWebView发送加载 Web 内容的请求。

    使用initWithFrame:configuration:方法创建WKWebView,使用loadHTMLString:baseURL:加载本地HTML文件,或使用loadRequest:方法加载网络内容。如下:

    // Local HTMLs
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.webConfiguration];
    self.view = webView;
    NSURL *htmlURL = [[NSBundle mainBundle] URLForResource:@"WKWebView - NSHipster" withExtension:@"htm"];
    NSURL *baseURL = [htmlURL URLByDeletingLastPathComponent];
    NSString *htmlString = [NSString stringWithContentsOfURL:htmlURL
    encoding:NSUTF8StringEncoding
    error:NULL];
    [webView loadHTMLString:htmlString baseURL:baseURL];

    // Web Content
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.webConfiguration];
    self.view = webView;
    NSURL *myURL = [NSURL URLWithString:@"https://github.com/pro648/tips/wiki"];
    NSURLRequest *request = [NSURLRequest requestWithURL:myURL
    cachePolicy:NSURLRequestUseProtocolCachePolicy
    timeoutInterval:30];
    [webView loadRequest:request];

    本地HTML文件可以在demo源码中获取:https://github.com/pro648/BasicDemos-iOS/tree/master/WebKit

    如图所示:



    设置allowsBackForwardNavigationGestures属性可以开启、关闭横向滑动触发前进、后退导航功能:

    self.webView.allowsBackForwardNavigationGestures = YES;

    1.1 KVO

    WKWebView中的title、URL、estimatedProgress、hasOnlySecureContent和loading

    属性支持键值观察,可以通过添加观察者,获得当前网页标题、加载进度等。

    根据文档serverTrust属性也支持KVO,但截至目前,在iOS 12.1 (16B91)中使用观察者观察该属性,运行时会抛出this class is not key value coding-compliant for the key serverTrust的异常。

    将网页标题显示出来可以帮助用户了解当前所在位置,显示当前导航进度能够能够让用户感受到加载速度,另外,还可以观察hasOnlySecureContent查看当前网页所有资源是否均通过加密连接传输。在viewDidLoad中添加以下代码:

    [self.webView addObserver:self forKeyPath:@"hasOnlySecureContent" options:NSKeyValueObservingOptionNew context:webViewContext];
    [self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:webViewContext];
    [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:webViewContext];

    实现observerValueForKeyPath:ofObject:change:context:方法,在观察到值变化时进行对应操作:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"hasOnlySecureContent"]) {
    BOOL onlySecureContent = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
    NSLog(@"onlySecureContent:%@",onlySecureContent ? @"YES" : @"NO");
    } else if ([keyPath isEqualToString:@"title"]) {
    self.navigationItem.title = change[NSKeyValueChangeNewKey];
    } else if ([keyPath isEqualToString:@"estimatedProgress"]) {
    self.progressView.hidden = [change[NSKeyValueChangeNewKey] isEqualToNumber:@1];

    CGFloat progress = [change[NSKeyValueChangeNewKey] floatValue];
    self.progressView.progress = progress;
    } else {
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    }

    如果你对键值观察、键值编码还不熟悉,可以查看我的另一篇文章:KVC和KVO学习笔记

    运行demo,如下所示:


    控制台会输出如下内容:

    onlySecureContent:YES

    WKWebView调用reload、stopLoading、goBack、goForward可以实现刷新、返回、前进等功能:

    - (void)refreshButtonTapped:(id)sender {
    [self.webView reload];
    }

    - (void)stopLoadingButtonTapped:(id)sender {
    [self.webView stopLoading];
    }

    - (IBAction)backButtonTapped:(id)sender {
    [self.webView goBack];
    }

    - (IBAction)forwardButtonTapped:(id)sender {
    [self.webView goForward];
    }

    还可以通过观察loading属性,在视图加载完成时,更新后退、前进按钮状态:

    - (void)viewDidLoad {
    ...

    [self.webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:webViewContext];
    }

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    ...
    if (context == webViewContext && [keyPath isEqualToString:@"loading"]) {
    BOOL loading = [change[NSKeyValueChangeNewKey] boolValue];
    // 加载完成后,右侧为刷新按钮;加载过程中,右侧为暂停按钮。
    self.navigationItem.rightBarButtonItem = loading ? self.stopLoadingButton : self.refreshButton;

    self.backButton.enabled = self.webView.canGoBack;
    self.forwardButton.enabled = self.webView.canGoForward;
    } else {
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    }

    1.2 截取网页视图

    在iOS 11和macOS High Sierra中,WebKit framework增加了takeSnapshotWithConfiguration:completionHandler: API用于截取网页视图。截取网页可见部分视图方法如下:

    - (IBAction)takeSnapShot:(UIBarButtonItem *)sender {
    WKSnapshotConfiguration *shotConfiguration = [[WKSnapshotConfiguration alloc] init];
    shotConfiguration.rect = CGRectMake(0, 0, self.webView.bounds.size.width, self.webView.bounds.size.height);

    [self.webView takeSnapshotWithConfiguration:shotConfiguration
    completionHandler:^(UIImage * _Nullable snapshotImage, NSError * _Nullable error) {
    // 保存截图至相册,需要在info.plist中添加NSPhotoLibraryAddUsageDescription key和描述。
    UIImageWriteToSavedPhotosAlbum(snapshotImage, NULL, NULL, NULL);
    }];
    }

    此前,截取网页视图需要结合图层和graphics context。现在,只需要调用单一API。

    1.3 执行JavaScript

    可以使用evaluateJavaScript:completionHandler:方法触发web view JavaScript。下面方法触发输出web view userAgent:

    [self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable userAgent, NSError * _Nullable error) {
    NSLog(@"%@",userAgent);
    }];

    在iOS 12.1.2 (16C101) 中,输出如下:

    Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16C101

    2. WKWebViewConfiguration

    WKWebViewConfiguration是用于初始化Web视图属性的集合。通过WKWebViewConfiguration类,可以设置网页渲染速度,视频是否自动播放,HTML5 视频是否一帧一帧播放,如何与本地代码通信等。

    WKWebViewConfiguration属性有偏好设置preference、线程池processPool和用户内容控制器userContentController等。

    Web view 初始化时才需要WKWebViewConfiguration对象,WKWebView创建后无法修改其configuration。多个WKWebView可以使用同一个configuration。


    例如,设置网页中最小字体为30,自动检测电话号码:

    - (WKWebViewConfiguration *)webConfiguration {
    if (!_webConfiguration) {
    _webConfiguration = [[WKWebViewConfiguration alloc] init];

    // 偏好设置 设置最小字体
    WKPreferences *preferences = [[WKPreferences alloc] init];
    preferences.minimumFontSize = 30;
    _webConfiguration.preferences = preferences;

    // 识别网页中的电话号码
    _webConfiguration.dataDetectorTypes = WKDataDetectorTypePhoneNumber;

    // Web视图内容完全加载到内存之前,禁止呈现。
    _webConfiguration.suppressesIncrementalRendering = YES;
    }
    return _webConfiguration;
    }

    suppressesIncrementalRendering属性是布尔值,决定Web视图内容在完全加载到内存前是否显示,默认为NO,即边加载边显示。例如,Web视图中有文字和图片,会先显示文字后显示图片。

    3. Scripts

    用户脚本 (User Scripts) 是文档开始加载或加载完成后注入 Web 页面的 JS。User Scripts非常强大,其能够通过客户端设置网页,允许注入事件监听器,甚至可以注入脚本,这些脚本又可以回调 native app 。

    3.1 WKUserScript

    WKUserScript对象表示可以注入网页的脚本。initWithSource:injectionTime:forMainFrameOnly:方法返回可以添加到userContentController控制器的脚本。其中,source 参数为 script 源码;injectionTime为WKUserScriptInjectionTimeAtDocumentStart、WKUserScriptInjectionTimeAtDocumentEnd,

    其参数如下:

    source: script 源码。
    injectionTime: user script注入网页时间,为WKUserScriptInjectionTime枚举常量。WKUserScriptInjectionTimeAtDocumentStart在创建文档元素之后,加载任何其他内容之前注入。WKUserScriptInjectionTimeAtDocumentEnd在加载文档后,但在加载其他子资源之前注入。
    forMainFrameOnly: 布尔值,YES时只注入main frame,NO时注入所有 frame。
    下面代码将隐藏 Wikipedia toc、mw-panel 脚本注入网页,同时使用 JS 提取网页 toc 表格内容:

    // 隐藏wikipedia左边缘和contents表格
    NSURL *hideTableOfContentsScriptURL = [[NSBundle mainBundle] URLForResource:@"hide" withExtension:@"js"];
    NSString *hideTableOfContentsScriptString = [NSString stringWithContentsOfURL:hideTableOfContentsScriptURL
    encoding:NSUTF8StringEncoding error:NULL];
    WKUserScript *hideTableOfContentsScript = [[WKUserScript alloc] initWithSource:hideTableOfContentsScriptString
    injectionTime:WKUserScriptInjectionTimeAtDocumentStart
    forMainFrameOnly:YES];

    // 获取contents表格内容
    NSString *fetchTableOfContentsScriptString = [NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"fetch" withExtension:@"js"] encoding:NSUTF8StringEncoding error:NULL];
    WKUserScript *fetchTableOfContentsScript = [[WKUserScript alloc] initWithSource:fetchTableOfContentsScriptString
    injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
    forMainFrameOnly:YES];

    本文中的 js 和 HTML 均可通过文章底部源码链接获取。要使用 JavaScript 提取内容的网页为:https://en.wikipedia.org/w/index.php?title=San_Francisco&mobileaction=toggle_view_desktop

    3.2 WKUserContentController
    WKUserContentController对象为 JavaScript 提供了发送消息至 native app,将 user scripts 注入 Web 视图方法。

    将 user script 添加到userContentController才可以注入网页中:

    WKUserContentController *userContentController = [[WKUserContentController alloc] init];
    [userContentController addUserScript:hideTableOfContentsScript];
    [userContentController addUserScript:fetchTableOfContentsScript];

    要监听 JavaScript 消息,需要先注册要监听消息名称。添加监听事件方法为addScriptMessageHandler:name:,参数如下:

    scriptMessageHandler: 处理监听消息,该类需要遵守WKMessageHandler协议。
    name: 要监听消息名称。
    使用该方法添加监听事件后,JavaScript 的 window.webkit.messageHandlers.name.postMessage(messageBody) 函数将被定义在使用了该userContentController网页视图的所有frame。

    监听fetch.js中 didFetchTableOfContents 消息:

    [userContentController addScriptMessageHandler:self name:@"didFetchTableOfContents"];

    // 最后,将userContentController添加到WKWebViewConfiguration
    _webConfiguration.userContentController = userContentController;

    3.3 WKScriptMessageHandler

    监听 script message 的类必须遵守WKMessageHandler协议,实现该协议唯一且必须实现的userContentController:didReceiveScriptMessage:方法。Webpage 接收到脚本消息时会调用该方法。

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"didFetchTableOfContents"]) {
    id body = message.body;
    if ([body isKindOfClass:NSArray.class]) {
    NSLog(@"messageBody:%@",body);
    }
    }
    }

    如下所示:


    JavaScript 消息是WKScriptMessage对象,该对象属性如下:

    body:消息内容,可以是NSNumber、NSString、NSDate、NSArray、NSDictionary、NSNull类型。
    frameInfo:发送该消息的frame。
    name:接收消息对象名称。
    webView:发送该消息的网页视图。
    最终,我们成功的将事件从 iOS 转发到 JavaScript,并将 JavaScript 转发回 iOS。

    4. WKNavigationDelegate

    用户点击链接,使用前进、后退手势,JavaScript 代码(例如,window.location = ' https://github.com/pro648 '),使用代码调用loadRequest:等均会让网页加载内容,即action引起网页加载;随后,web view 会向服务器发送request,接收response,可能会是positive response,也可能请求失败;之后接收数据。我们的应用可以在action后、request前,或者response后、data前自定义网页加载,决定继续加载,或取消加载。


    WKNavigationDelegate协议内方法可以自定义Web视图接收、加载和完成导航请求过程的行为。

    首先,声明遵守WKNavigationDelegate协议:

    @interface ViewController () <WKNavigationDelegate>

    其次,指定遵守WKNavigationDelegate协议的类为 web view 代理:

    self.webView.navigationDelegate = self;

    最后,根据需要实现所需WKNavigationDelegate方法。

    webView:decidePolicyForNavigationAction:decisionHandler:方法在action后响应,webView:decidePolicyForNavigationResponse:decisionHandler:方法在response后响应。

    根据前面的配置,WKWebView会自动识别网页中电话号码。截至目前,电话号码只能被识别,无法点击。可以通过实现webView:decidePolicyForNavigationAction:decisionHandler:方法,调用系统Phone app拨打电话

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (navigationAction.navigationType == WKNavigationTypeLinkActivated && [navigationAction.request.URL.scheme isEqualToString:@"tel"]) {
    [UIApplication.sharedApplication openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) {
    NSLog(@"Successfully open url:%@",navigationAction.request.URL);
    }];
    decisionHandler(WKNavigationActionPolicyCancel);
    } else {
    decisionHandler(WKNavigationActionPolicyAllow);
    }
    }

    实现了该方法后,必须调用decisionHandler块。该块参数为WKNavigationAction枚举常量。WKNavigationActionPolicyCancel取消导航,WKNavigationActionPolicyAllow继续导航。

    WKNavigationAction对象包含引起本次导航的信息,用于决定是否允许本次导航。其属性如下:

    request:本次导航的request。
    sourceFrame:WKFrameInfo类型,请求本次导航frame信息。
    targetFrame:目标frame。如果导航至新窗口,则targetFrame为nil。
    navigationType:WKNavigationType枚举类型,为以下常量:
    WKNavigationTypeLinkActivated:用户点击href链接。
    WKNavigationTypeFormSubmitted:提交表格。
    WKNavigationTypeBackForward:请求前进、后退列表中item。
    WKNavigationTypeReload:刷新网页。
    WKNavigationTypeFormResubmitted:因后退、前进、刷新等重新提交表格。
    WKNavigationTypeOther:其他原因。
    WKFrameInfo对象包含了一个网页中的frame信息,其只是一个描述瞬时状态 (transient) 的纯数据 (data-only) 对象,不能在多次消息调用中唯一标志某个frame。

    如果需要在response后操作导航,需要实现webView:decidePolicyForNavigationResponse:decisionHandler:方法。WKNavigationResponse对象包含navigation response信息,用于决定是否接收response。其属性如下:

    canShowMIMEType:布尔类型值,指示WebKit是否显示MIME类型内容。
    forMainFrame:布尔类型值,指示即将导航至的frame是否为main frame。
    response:NSURLResponse类型。
    实现了该方法后,必须调用decisionHandler块,否则会在运行时抛出异常。decisionHandler块参数为WKNavigationResponsePolicy枚举类型。WKNavigationResponseCancel取消导航,WKNavigationResponseAllow继续导航。

    Navigation action 和 navigation response 既可以在处理完毕后立即调用decisionHandler,也可以异步调用。

    5. WKUIDelegate

    WKWebView与 Safari 类似,尽管前者在一个窗口显示内容。如果需要打开多个窗口、监控打开、关闭窗口,修改用户点击元素时显示哪些选项,需要使用WKUIDelegate协议。

    首先,声明遵守WKUIDelegate协议:

    @interface ViewController () <WKUIDelegate>

    其次,指定遵守WKUIDelegate协议的类为 web view 代理:

    self.webView.uiDelegate = self;

    最后,根据需要实现WKUIDelegate协议内方法。

    5.1 新窗口打开

    如何响应 JavaScript 的打开新窗口函数、或target="_blank"标签?有以下三种方法:

    创建新的WKWebView,并在新的页面打开。
    在 Safari 浏览器打开。
    捕捉 JS ,在同一个WKWebView加载。
    当 URL 为 mail、tel、sms 和 iTunes链接时交由系统处理。此时,系统会交由对应 app 处理。其他情况在当前 web view 加载。

    5.2 响应 JavaScript 弹窗
    在响应 JavaScript 时,可以通过WKUIDelegate协议使用 native UI呈现,有以下三种方法:

    1、webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler::为了用户安全,在该方法的实现中需要需要标志出提供当前内容的网址,最为简便的方法便是frame.request.URL.host,响应面板应只包含一个OK按钮。当 alert panel 消失后,调用completionHandler。
    2、webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler::为了用户安全,在该方法的实现中需要需要标志出提供当前内容的网址,最为简便的方法便是frame.request.URL.host,响应面板包括两个按钮,一般为OK和Cancel。当 alert panel 消失后,调用completionHandler。如果用户点击的是OK按钮,为completionHandler传入YES;如果用户点击的是Cancel按钮,为completionHandler传入NO。
    3、webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler::为了用户安全,在该方法的实现中需要需要标志出提供当前内容的网址,最为简便的方法便是frame.request.URL.host,响应面板包括两个按钮(一个OK按钮,一个Cancel按钮)和一个输入框。当面板消失时调用completionHandler,如果用户点击的是OK按钮,传入文本框文本;否则,传入nil。

    例如,输入账号、密码前点击登陆按钮,大部分网页会弹出警告框。在 JavaScript 中,会弹出 alert 或 confirm box。

    JavaScript alert 会堵塞当前进程,调用completionHandler后 JavaScript 才会继续执行。

    6. WKURLSchemeHandler

    UIWebView支持自定义NSURLProtocol协议。如果想要加载自定义 URL 内容,可以通过创建、注册NSURLProtocol子类实现。此后,任何调用自定义 scheme (例如,hello world://) 的方法,都会调用NSURLProtocol子类,在NSURLProtocol子类处理自定义 scheme ,这将非常实用。例如,在 book 中加载图片、视频等。

    WKWebView不支持NSURLProtocol协议,因此不能加载自定义 URL Scheme。在 iOS 11 中,Apple 为 WebKit framework 增加了WKURLSchemeHandler协议,用于加载自定义 URL Scheme。

    WebKit遇到无法识别的 URL时,会调用WKURLSchemeHandler协议。该协议包括以下两个必须实现的方法:

    webView:startURLSchemeTask::加载资源时调用。
    webView:stopURLSchemeTask::WebKit 调用该方法以终止 (stop) 任务。调用该方法后,不得调用WKURLSchemeTask协议的任何方法,否则会抛出异常。
    使用WKURLSchemeHandler协议处理完毕任务后,调用WKURLSchemeTask协议内方法加载资源。WKURLSchemeTask协议包括request属性,该属性为NSURLRequest类型对象。还包含以下方法:

    didReceiveResponse::设置当前任务的 response。每个 task 至少调用一次该方法。如果尝试在任务终止或完成后调用该方法,则会抛出异常。
    didReceiveData::设置接收到的数据。当接收到任务最后的 response 后,使用该方法发送数据。每次调用该方法时,新数据会拼接到先前收到的数据中。如果尝试在发送 response 前,或任务完成、终止后调用该方法,则会引发异常。
    didFinish:将任务标记为成功完成。如果尝试在发送 response 前,或将已完成、终止的任务标记为完成,则会引发异常。
    didFailWithError::将任务标记为失败。如果尝试将已完成、失败,终止的任务标记为失败,则会引发异常。
    在WKURLSchemeHandler协议方法内,可以获取到请求的request。因此,可以提取 URL 中任何内容,并将数据转发给WKWebView进行加载。

    下面的方法分别使用 url 、custom URL Scheme加载网络图片和相册图片:

    - (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
    NSURL *url = urlSchemeTask.request.URL;
    if ([url.absoluteString containsString:@"custom-scheme"]) {
    NSArray<NSURLQueryItem *> *queryItems = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES].queryItems;
    for (NSURLQueryItem *item in queryItems) {

    // example: custom-scheme://path?type=remote&url=https://placehold.it/120x120&text=image1
    if ([item.name isEqualToString:@"type"] && [item.value isEqualToString:@"remote"]) {
    for (NSURLQueryItem *queryParams in queryItems) {
    if ([queryParams.name isEqualToString:@"url"]) {
    NSString *path = queryParams.value;
    path = [path stringByReplacingOccurrencesOfString:@"\\" withString:@""];

    // 获取图片
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:path] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    [urlSchemeTask didReceiveResponse:response];
    [urlSchemeTask didReceiveData:data];
    [urlSchemeTask didFinish];
    }];
    [task resume];
    }
    }
    } else if ([item.name isEqualToString:@"type"] && [item.value isEqualToString:@"photos"]) { // example: custom-scheme://path?type=photos
    dispatch_async(dispatch_get_main_queue(), ^{
    self.imagePicker = [[ImagePicker alloc] init];
    [self.imagePicker showGallery:^(BOOL flag, NSURLResponse * _Nonnull response, NSData * _Nonnull data) {
    if (flag) {
    [urlSchemeTask didReceiveResponse:response];
    [urlSchemeTask didReceiveData:data];
    [urlSchemeTask didFinish];
    } else {
    NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL];
    [urlSchemeTask didFailWithError:error];
    }
    }];
    });
    }
    }
    }
    });
    }

    - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL];
    [urlSchemeTask didFailWithError:error];
    }

    实现上述方法的类必须遵守WKURLSchemeHandler协议。另外,必须在WKWebView的配置中注册所支持的 URL Scheme:

    // 添加要自定义的url scheme
    [_webConfiguration setURLSchemeHandler:self forURLScheme:@"custom-scheme"];

    运行如下:


    总结

    WebKit为 iOS 、macOS 开发人员提供了一套强大的开发工具,可以直接在 app 网页视图中操作 JavaScript,使用 user script 将 JavaScript 注入网页,使用WKScriptMessageHandler协议接收 JavaScript 消息。使用WKNavigationDelegate协议自定义网页导航,使用WKUIDelegate在网页上呈现 native UI,使用WKURLSchemeHandler加载自定义 URL Scheme 内容。

    如果只是简单呈现网页视图,推荐使用 iOS 9 推出的SFSafariViewController,几行代码就可实现与 Safari 一样的体验。SFSafariViewController还提供了自动填充、欺诈网站监测等功能。

    Demo名称:WebKit
    源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/WebKit

    链接:https://www.jianshu.com/p/65c66f924d56

    收起阅读 »

    pthread多线程(C语言) + Socket

    pthread多线程(C语言) + Socketpthread是使用使用C语言编写的多线程的API, 简称Pthreads ,是线程的POSIX标准,可以在Unix / Linux / Windows 等系统跨平台使用。在类Unix操作系统(Unix、Linu...
    继续阅读 »

    pthread多线程(C语言) + Socket

    pthread是使用使用C语言编写的多线程的API, 简称Pthreads ,是线程的POSIX标准,可以在Unix / Linux / Windows 等系统跨平台使用。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。

    GitHub项目FanSocket(纯C语言socket+线程队列)+其他demo客户端

    1.线程创建

    //子线程1
    void test1(int *a){
    printf("线程test1");
    //修改自己的子线程系统释放,注释打开后,线程不能用pthread_join方法
    //pthread_detach(pthread_self());
    }
    //子线程2
    void test2(int *a){
    printf("线程test2");
    }

    /*
    int pthread_create(pthread_t * thread, //新线程标识符
    pthread_attr_t * attr, //新线程的运行属性
    void * (*start_routine)(void *), //线程将会执行的函数
    void * arg);//执行函数的传入参数,可以为结构体
    */

    //创建线程方法一 (手动释放线程)
    int a=10;
    pthread_t pid;
    pthread_create(&pid, NULL, (void *)test1, (void *)&a);


    //线程退出或返回时,才执行回调,可以释放线程占用的堆栈资源(有串行的作用)
    if(pthread_join(pid, NULL)==0){
    //线程执行完成
    printf("线程执行完成:%d\n",threadIndex);
    if (message!=NULL) {
    printf("线程执行完成了\n");
    }
    }


    //创建线程方法二 (自动释放线程)
    //设置线程属性
    pthread_attr_t attr;
    pthread_attr_init (&attr);
    //线程默认是PTHREAD_CREATE_JOINABLE,需要pthread_join来释放线程的
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    //线程并发
    int rc=pthread_create(&pid, &attr, (void *)test2, (void *)a);
    pthread_attr_destroy (&attr);
    if (rc!=0) {
    printf("创建线程失败\n");
    return;
    }

    2.线程退出和其他

    pthread_exit (tes1) //退出当前线程
    pthread_main_np () // 获取主线程

    //主线程和子线程
    if(pthread_main_np()){
    //main thread
    }else{
    //others thread
    }

    int pthread_cancel(pthread_t thread);//发送终止信号给thread线程,如果成功则返回0
    int pthread_setcancelstate(int state, int *oldstate);//设置本线程对Cancel信号的反应
    int pthread_setcanceltype(int type, int *oldtype);//设置本线程取消动作的执行时机
    void pthread_testcancel(void);//检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回

    3 线程互斥锁(量)与条件变量

    3.1 互斥锁(量)

    //静态创建
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    //动态创建
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
    //注销互斥锁
    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    //lock 和unlock要成对出现,不然会出现死锁
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    //判断是否可以加锁,如果可以加锁并返回0,否则返回非0
    int pthread_mutex_trylock(pthread_mutex_t *mutex);

    3.2 条件变量

    1、条件变量是利用线程间共享的全局变量进行同步的一种机制,
    2、一个线程等待”条件变量的条件成立”而挂起;
    3、另一个线程使”条件成立”(给出条件成立信号)。
    4、为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

    //静态创建
    pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
    //动态创建
    int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
    //注销条件变量
    int pthread_cond_destroy(pthread_cond_t *cond);
    //条件等待,和超时等待
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
    const struct timespec *abstime);
    //开启条件,启动所有等待线程
    int pthread_cond_broadcast(pthread_cond_t *cond);
    //开启一个等待信号量
    int pthread_cond_signal(pthread_cond_t *cond);

    4.线程同步:互斥锁(量)与条件变量(具体封装实现)

    /*全局的队列互斥条件*/
    extern pthread_cond_t fan_cond;
    extern pthread_cond_t fan_cond_wait;
    /*全局的队列互斥锁*/
    extern pthread_mutex_t fan_mutex;
    //extern pthread_mutex_t fan_mutex_wait;

    extern int fan_thread_status;//0=等待 1=执行 -1=清空所有
    extern int fan_thread_clean_status;//0=默认 1=清空所有

    //开启线程等待 return=-2一定要处理
    extern int fan_thread_start_wait(void);
    //正常的超时后继续打开下一个信号量 return=-2一定要处理
    int fan_thread_start_timedwait(int sec);
    //启动线程,启动信号量
    extern int fan_thread_start_signal(void);
    //启动等待信号量
    extern int fan_thread_start_signal_wait(void);
    //暂停线程
    extern int fan_thread_end_signal(void);
    //初始化互斥锁
    extern int fan_thread_queue_init(void);
    //释放互斥锁信号量
    extern int fan_thread_free(void);

    //让队列里面全部执行完毕,而不是关闭线程;
    extern int fan_thread_clean_queue(void);
    //每次关闭清空后,等待1-2秒,要恢复状态,不然线程添加
    extern int fan_thread_init_queue(void);
    //设置线程的优先级,必须在子线程
    extern int fan_thread_setpriority(int priority);

    线程队列互斥,并且按入队顺序,一个一个按照外部条件,触发信号量,主要是等待队列,

    /*全局的队列互斥条件*/
    pthread_cond_t fan_cond=PTHREAD_COND_INITIALIZER;
    pthread_cond_t fan_cond_wait=PTHREAD_COND_INITIALIZER;

    /*全局的队列互斥锁*/
    pthread_mutex_t fan_mutex = PTHREAD_MUTEX_INITIALIZER;
    //pthread_mutex_t fan_mutex_wait = PTHREAD_MUTEX_INITIALIZER;
    int fan_thread_status=1;//0=等待 1=执行
    int fan_thread_clean_status;//0=默认 1=清空所有

    //开启线程等待
    int fan_thread_start_wait(void){
    pthread_mutex_lock(&fan_mutex);
    fan_thread_clean_status=0;
    while (fan_thread_status==0) {
    pthread_cond_wait(&fan_cond, &fan_mutex);
    if (fan_thread_clean_status==1) {
    break;
    }
    }
    if (fan_thread_clean_status==1) {
    pthread_mutex_unlock(&fan_mutex);
    return -2;
    }
    if (fan_thread_status==1) {
    fan_thread_status=0;
    pthread_mutex_unlock(&fan_mutex);
    }else{
    pthread_mutex_unlock(&fan_mutex);
    }
    return 0;
    }
    //正常的超时后继续打开下一个信号量
    int fan_thread_start_timedwait(int sec){
    int rt=0;
    pthread_mutex_lock(&fan_mutex);
    struct timeval now;
    struct timespec outtime;
    gettimeofday(&now, NULL);
    outtime.tv_sec = now.tv_sec + sec;
    outtime.tv_nsec = now.tv_usec * 1000;

    int result = pthread_cond_timedwait(&fan_cond_wait, &fan_mutex, &outtime);
    if (result!=0) {
    //线程等待超时
    rt=-1;
    }
    if (fan_thread_clean_status==1) {
    rt = -2;
    }
    pthread_mutex_unlock(&fan_mutex);
    return rt;
    }
    //启动线程,启动信号量
    int fan_thread_start_signal(void){
    int rs=pthread_mutex_trylock(&fan_mutex);
    if(rs!=0){
    pthread_mutex_unlock(&fan_mutex);
    }
    fan_thread_status=1;
    pthread_cond_signal(&fan_cond);
    // pthread_cond_broadcast(&fan_cond);//全部线程
    pthread_mutex_unlock(&fan_mutex);
    return 0;
    }
    //开启等待时间的互斥信号量
    int fan_thread_start_signal_wait(void){
    int rs=pthread_mutex_trylock(&fan_mutex);
    if(rs!=0){
    pthread_mutex_unlock(&fan_mutex);
    }
    // fan_thread_status=1;
    pthread_cond_signal(&fan_cond_wait);
    // pthread_cond_broadcast(&fan_cond);//全部线程
    pthread_mutex_unlock(&fan_mutex);
    return 0;
    }
    //暂停下一个线程
    int fan_thread_end_signal(void){
    pthread_mutex_lock(&fan_mutex);
    fan_thread_status=0;
    pthread_cond_signal(&fan_cond);
    pthread_mutex_unlock(&fan_mutex);
    return 0;
    }
    //初始化互斥锁(动态创建)
    int fan_thread_queue_init(void){
    pthread_mutex_init(&fan_mutex, NULL);
    pthread_cond_init(&fan_cond, NULL);
    return 0;
    }
    //释放互斥锁和信号量
    int fan_thread_free(void)
    {
    pthread_mutex_destroy(&fan_mutex);
    pthread_cond_destroy(&fan_cond);
    return 0;
    }

    //清空所有的队列
    int fan_thread_clean_queue(void){
    pthread_mutex_lock(&fan_mutex);
    fan_thread_clean_status=1;
    pthread_cond_broadcast(&fan_cond);
    pthread_cond_broadcast(&fan_cond_wait);
    pthread_mutex_unlock(&fan_mutex);
    return 0;
    }
    //恢复队列
    int fan_thread_init_queue(void){
    pthread_mutex_lock(&fan_mutex);
    fan_thread_clean_status=0;
    fan_thread_status=1;
    pthread_cond_signal(&fan_cond);
    pthread_mutex_unlock(&fan_mutex);
    return 0;
    }
    //设置线程的优先级,必须在子线程
    int fan_thread_setpriority(int priority){
    struct sched_param sched;
    bzero((void*)&sched, sizeof(sched));
    // const int priority1 = (sched_get_priority_max(SCHED_RR) + sched_get_priority_min(SCHED_RR)) / 2;
    sched.sched_priority=priority;
    //SCHED_OTHER(正常,非实时)SCHED_FIFO(实时,先进先出)SCHED_RR(实时、轮转法)
    pthread_setschedparam(pthread_self(), SCHED_RR, &sched);
    return 0;
    }

    5 其他线程方法

    //return=0:线程存活。ESRCH:线程不存在。EINVAL:信号不合法。
    int kill_ret=pthread_kill(pid, 0);//测试线程是否存在
    printf("线程状态:%d\n",kill_ret);
    if(kill_ret==0){
    //关闭线程
    pthread_cancel(pid);
    }


    pthread_equal(pid, pid1);//比较两个线程ID是否相同


    //函数执行一次
    pthread_once_t once = PTHREAD_ONCE_INIT;
    pthread_once(&once, test1);

    转自:https://www.jianshu.com/p/6fcd478635e2

    收起阅读 »