0%

常见的内存泄漏示例

ViewController的子视图对self的持有

问题代码:

class WSViewChildLeakTestVC: UIViewController {
var customView: WSCustomView?
deinit {
print("WSViewChildLeakTestVC - deinit()")
}
override func viewDidLoad() {
super.viewDidLoad()
customView = WSCustomView(vc: self)
self.view.addSubview(customView!)
}
}
class WSCustomView: UIView {
var myVC: WSViewChildLeakTestVC?
convenience init(vc: WSViewChildLeakTestVC) {
self.init()
self.myVC = vc
}
}

原因:

两个对象双向强引用,导致两者都不会被释放

解决方法:其中一个对象,改为weak即可

// var myVC: WSViewChildLeakTestVC? 添加weak
weak var myVC: WSViewChildLeakTestVC?

Delegate循环引用

问题代码:

class WSDelegateLeakTestVC: UIViewController,WSCustomDeProtocl {
var childView: WSCustomDeView?
deinit {
print("WSDelegateLeakTestVC - deinit()")
}
override func viewDidLoad() {
super.viewDidLoad()
self.childView = WSCustomDeView()
self.childView?.delegate = self
}
}
protocol WSCustomDeProtocl {
}
class WSCustomDeView: UIView {
var delegate: WSCustomDeProtocl?
}

造成循环引用的原因:

WSDelegateLeakTestVC => ChildView
ChildView.delegate => WSDelegateLeakTestVC

因此也造成了循环引用,导致不能被销毁

解决方法:

代理weak修饰

声明 delegate 为 weak 可能会避免这种情况,但是这样的话会引起编译错误,因为 structs 和 enums 不能引用 weak 变量。该如何解决呢?当声明 protocol 的时候,我们可以指定只有 class 类型的变量可以代理它,这样的话就可以使用 weak 来修饰了。

所以,代码修改如下:

protocol WSCustomDeProtocl: class {
}
class WSCustomDeView: UIView {
weak var delegate: WSCustomDeProtocl?
}

闭包

问题代码:

let someModalVC = WSClosuresLeakTestVC()
/// fix: weak
someModalVC.actionHandler = {
someModalVC.dismiss(animated: true, completion: nil)
}
self.present(someModalVC, animated: true, completion: nil)

原因:

someModalVC <=> actionHandler 互相强引用

解决方法,使用捕获列表:

someModalVC.actionHandler = { [weak self] in
someModalVC?.dismiss(animated: true, completion: nil)
}
// or
someModalVC.actionHandler = { [unowned self] in
someModalVC.dismiss(animated: true, completion: nil)
}

定时器

问题代码:

class WSTimerViewController: UIViewController {
var timer: Timer?
deinit {
print("WSTimerViewController - deinit()")
}
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(scheduledTask), userInfo: nil, repeats: true)
timer?.fire()
}
}

原因:

WSTimerViewController <=> timer 互相强引用

解决方法:

在适当的时候对timer销毁,解除引用

deinit {
timer?.invalidate()
timer = nil
print("WSTimerViewController - deinit()")
}

NSNotificationCenter,KVO 问题

问题代码:

class WSObservableViewController: UIViewController {
deinit {
print("WSObservableViewController - deinit()")
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: "SomethingToObserverNotification", object: nil, queue: .main, using: handleNotification)
}
private func handleNotification(_ notification: Notification) {
}
}

问题原因:

NSNotificationCenter <=> handleNotification 互相强引用

解决方法,使用捕获列表:

NotificationCenter.default.addObserver(forName: .SomethingToObserveNotification, object: nil, queue: .main) { [weak self] notification in
self?.handleNotification(notification)
}

三种排查内存泄漏方法

静态内存泄漏分析方法(Analyze)

  1. 通过Xcode打开项目,然后点击Product->Analyze,开始进入静态内存泄漏分析
  2. 等待分析结果。
  3. 根据分析的结果对可能造成内存泄漏的代码进行排查

动态内存泄漏分析方法(Leaks)

  1. 通过Xcode打开项目,然后点击Product->Profile,等待build成功之后,选择Leaks
  2. 这时项目开始启动了,由于Leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。橙色矩形框中所示绿色为正常,如果出现红色,则表示出现内存泄漏。
  3. 选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call TreeHide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。

    Debug Memory Graph

Xcode memory graph debugger可以帮助找到和修复循环引用与内存泄露。当被激活时,会暂停app运行,展现当前堆中的对象,对象的关系,对象间的引用。

使用方法:Debugger Navigator -> View Memory Graph Hierarchy

优点:可以轻松地找到一些简单的泄露,比如循环引用。例如一个对象在闭包中持有自己,通过闭包捕获列表可以轻易修复内存泄露。
缺点:可能找不到已经泄露的点。比如,创建一个UIButton对象并在上面添加一个UIToolBars items数组,我们只能看到这发生了内存泄露却看不到为什么泄露。

图片在计算机中如何存储和表示?

常见的图片格式

JPEG 是目前最常见的图片格式,它诞生于1992年,是一个很古老的格式。它只支持有损压缩,其压缩算法可以精确控制压缩比,以图像质量换得存储空间。由于它太过常见,以至于许多移动设备的 CPU 都支持针对它的硬编码与硬解码。

PNG 诞生在 1995 年,比 JPEG 晚几年。它本身的设计目的是替代 GIF 格式,所以它与 GIF 有更多相似的地方。PNG 只支持无损压缩,所以它的压缩比是有上限的。相对于 JPEG 和 GIF 来说,它最大的优势在于支持完整的透明通道

GIF 诞生于 1987 年,随着初代互联网流行开来。它有很多缺点,比如通常情况下只支持 256 种颜色、透明通道只有 1 bit、文件压缩比不高。它唯一的优势就是支持多帧动画,凭借这个特性,它得以从 Windows 1.0 时代流行至今,而且仍然大受欢迎。

格式 优点 缺点 用途
jpg 色彩丰富,文件小 有损压缩 颜色丰富的图
png 透明、无损压缩、简单图文件小 若颜色较多复杂,则图片生成后的文件很大 小图标、透明背景
gif 动态、透明、文件小 色域不广、只有256种颜色 动态图片

除了以上面常见的格式,也有一些新型的格式:

APNG 是 Mozilla 在 2008 年发布的一种图片格式,旨在替换掉画质低劣的 GIF 动画。它实际上只是相当于 PNG 格式的一个扩展,所以 Mozilla 一直想把它合并到 PNG 标准里面去。然而 PNG 开发组并没有接受 APNG 这个扩展,而是一直在推进它自己的 MNG 动图格式。MNG 格式过于复杂以至于并没有什么系统或浏览器支持,而 APNG 格式由于简单容易实现,目前已经渐渐流行开来。Mozilla 自己的 Firefox 首先支持了 APNG,随后苹果的 Safari 也开始有了支持, Chrome 目前也已经尝试开始支持 ,可以说未来前景很好

APNG 与 Gif 对比

WebP 是 Google 在 2010 年发布的图片格式,希望以更高的压缩比替代 JPEG。它用 VP8 视频帧内编码作为其算法基础,取得了不错的压缩效果。它支持有损和无损压缩、支持完整的透明通道、也支持多帧动画,并且没有版权问题,是一种非常理想的图片格式(美中不足的是,WebP格式图像的编码时间“比JPEG格式图像长8倍)。借由 Google 在网络世界的影响力,WebP 在几年的时间内已经得到了广泛的应用。看看你手机里的 App:微博、微信、QQ、淘宝、网易新闻等等,每个 App 里都有 WebP 的身影。Facebook 则更进一步,用 WebP 来显示聊天界面的贴纸动画。

关于以上几种图片格式在移动端的解码和性能对比参见:移动端图片格式调研

iOS中图片加载过程和性能瓶颈

如上文所说,大部分格式的图片都是被压缩的,都需要被首先解码为bitmap(未压缩的位图),然后才能渲染到UI上。
UIImageView 显示图片,也有类似的过程。实际上,一张图片从在文件系统中,到被显示到 UIImageView,会经历以下几个步骤:

  1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  2. 然后将生成的 UIImage 赋值给 UIImageView
  3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  4. 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
    1. 分配内存缓冲区用于管理文件 IO 和解压缩操作;
    2. 将文件数据从磁盘读到内存中;
    3. 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
    4. 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。这就是 UIImageView 的一个性能瓶颈。

实际上,当我们调用[UIImage imageNamed:@"xxx"]后,UIImage 中存储的是未解码的图片,而调用 [UIImageView setImage:image]后,会在主线程进行图片的解码工作并且将图片显示到 UI 上,这时候,UIImage 中存储的是解码后的 bitmap 数据。

为什么需要解压缩

既然图片的解压缩需要消耗大量的 CPU 时间,那么我们为什么还要对图片进行解压缩呢?是否可以不经过解压缩,而直接将图片显示到屏幕上呢?答案是否定的。要想弄明白这个问题,我们首先需要知道什么是位图

bitmap:bitmap 又叫位图文件,它是一种非压缩的图片格式,所以体积非常大。所谓的非压缩,就是图片每个像素的原始信息在存储器中依次排列,一张典型的1920*1080像素的 bitmap 图片,每个像素由 RGBA 四个字节表示颜色,那么它的体积就是 1920 * 1080 * 4 = 1012.5kb。

由于 bitmap 简单顺序存储图片的像素信息,它可以不经过解码就直接被渲染到 UI 上。实际上,其它格式的图片都需要先被首先解码为 bitmap,然后才能渲染到界面上

不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。值得一提的是,在苹果的 SDK 中专门提供了两个函数用来生成 PNG 和 JPEG 图片:

// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);

// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);

因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因。

图片解压缩的过程其实就是将图片的二进制数据转换成像素数据的过程

图片的编码和解码
iOS 底层是用 ImageIO.framework 实现的图片编解码。目前 iOS 原生支持的格式有:JPEG、JPEG2000、PNG、GIF、BMP、ICO、TIFF、PICT,自 iOS 8.0 起,ImageIO 又加入了 APNG、SVG、RAW 格式的支持。在上层,开发者可以直接调用 ImageIO 对上面这些图片格式进行编码和解码。对于动图来说,开发者可以解码动画 GIF 和 APNG、可以编码动画 GIF。

注意:图片所占内存的大小与图片的尺寸有关,而不是图片的文件大小

色彩空间和像素格式

计算图片解码后每行需要的比特数,由两个参数相乘得到:每行的像素数 width,和存储一个像素需要的比特数4

这里的4,其实是由每张图片的像素格式和像素组合来决定的,下表是苹果平台支持的像素组合方式

2021-02-20-i3lZl7

表中的bpp,表示每个像素需要多少位;bpc表示颜色的每个分量,需要多少位。具体的解释方式,可以看下面这张图:

2021-02-20-1X3zJ5

我们解码后的图片,默认采用 kCGImageAlphaNoneSkipLast RGB 的像素组合,没有 alpha 通道,每个像素32位4个字节,前三个字节代表红绿蓝三个通道, 但是有时候,如果我们只是绘制一个蒙版,是不需要这么字节表示的比如 Alpha 8 format,每个像素只需要占用 1 个字节,这之间的差距就造成了内存浪费。

UIGraphicsImageRenderer 和 UIGraphicsBeginImageContextWithOptions

当我们为了离屏渲染,要创建 image buffer 时,我们通常会使用 UIGraphicsBeginImageContext,但是最好还是用 UIGraphicsImageRenderer,它的性能更好、更高效,并且支持广色域。这里有一个中间地带,如果你主要将图像渲染到图形图像渲染器(graphic image render)中,该图像可能使用超出 SRGB 色域的色彩空间值,但实际上并不需要更大的元素来存储这些信息。所以 UIImage 有一个可以用来获取预构建的 UIGraphicsImageRendererFormat 对象的 image renderer format 属性,该对象用于重新渲染图像时进行最优化存储。

所以苹果官方建议使用 UIGraphicsImageRenderer,这个方法是从 iOS 10 引入,在 iOS 12 上会自动选择最佳的图像格式,可以减少很多内存。系统可以根据图片分辨率选择创建解码图片的格式,如选用SRGB format 格式,每个像素占用 4 字节,而Alpha 8 format,每像素只占用 1 字节,可以减少大量的解码内存占用。

扩展阅读色彩空间与像素格式

imageWithContentsOfFile 和 imageNamed 对比

imgeNamed

用这个方法加载图片分为两种情况:

  1. 系统缓存有这个图片,直接从缓存中取得
  2. 系统缓存没有这个图片
    通过传入的文件名对整个工程进行遍历 (在application bundle的顶层文件夹寻找名字的图象 ), 如果如果找到对应的图片,iOS 系统首先要做的是将这个图片放到系统缓存中去,以备下次使用的时候直接从系统缓存中取, 接下来重复第一步,即直接从缓存中取

由于系统会缓存图片,所以如果要加载的这个图片的文件量很多,文件大小很大,内存不足,内存泄露,甚至是程序的崩溃都是很容易发生的事.

imageWithContentsOfFile

用这个方法只有一种情况,那就是仅仅加载图片, 图像数据不会被缓存. 因此在加载较大图片的时候, 以及图片使用情况很少的时候可以使用这两个方法 , 降低内存消耗.

加载本地图片,要比从Assets Catalogs耗时要长,具体见Assets Catalogs 与 I/O 优化

图片内存优化

到此我们可知,图片经过解压之后,在内存中实际是根据图片的分辨率和图片渲染所用的像素格式而定的

一、对不常用的大图片,使用 imageWithContentsOfFile 代替 imageNamed 方法,避免内存缓存(相应的使用imageNamed要避免载入大量的图片造成内存暴增)

二、使用 ImageIO 方法,对大图片进行缩放,减少图片解码占用内存大小。

UIImage 在设置和调整大小的时候,需要将原始图像加压到内存中,然后对内部坐标空间做一系列转换,整个过程会消耗很多资源。我们可以使用 ImageIO,它可以直接读取图像大小和元数据信息,不会带来额外的内存开销。

三、绘制图片,用 UIGraphicsImageRenderer 代替 UIGraphicsBeginImageContextWithOptions,自动管理颜色格式

四、超大图片处理

  1. 加载使用苹果推荐的DownSampling方案(缩略图方式)
    // DownSampling(降低采样)
    // 在视图比较小,图片比较大的场景下,直接展示原图片会造成不必要的内存和CPU消耗,这里就可以使用ImageIO的接口,DownSampling,也就是生成缩略图
    func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage
    {
    let sourceOpt = [kCGImageSourceShouldCache : false] as CFDictionary
    /**<
    这里有两个注意事项

    设置kCGImageSourceShouldCache为false,避免缓存解码后的数据,64位设置上默认是开启缓存的,(很好理解,因为下次使用该图片的时候,可能场景不同,需要生成的缩略图大小是不同的,显然不能做缓存处理)
    设置kCGImageSourceShouldCacheImmediately为true,避免在需要渲染的时候才做解码,默认选项是false
    */
    // 其他场景可以用createwithdata (data并未decode,所占内存没那么大),
    let source = CGImageSourceCreateWithURL(imageURL as CFURL, sourceOpt)!

    let maxDimension = max(pointSize.width, pointSize.height) * scale
    let downsampleOpt = [kCGImageSourceCreateThumbnailFromImageAlways : true,
    kCGImageSourceShouldCacheImmediately : true ,
    kCGImageSourceCreateThumbnailWithTransform : true,
    kCGImageSourceThumbnailMaxPixelSize : maxDimension] as CFDictionary
    let downsampleImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOpt)!
    return UIImage(cgImage: downsampleImage)
    }
  2. 使用苹果的CATiledLayer去加载。原理是分片渲染,滑动时通过指定目标位置,通过映射原图指定位置的部分图片数据解码渲染。这里不再累述,有兴趣的小伙伴可以自行了解下官方API。

    五、网络图片加载方式:使用SDwebImage等三方库

解决UIImageView的性能瓶颈

我们在讨论UIImageView的性能瓶颈中发现,问题在于主线程进行图片解压缩占用了大量的CPU,解决问题的办法就是:在子线程提前对图片进行强制解压缩

而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate :

/* Create a bitmap context. The context draws into a bitmap which is `width'
pixels wide and `height' pixels high. The number of components for each
pixel is specified by `space', which may also specify a destination color
profile. The number of bits for each component of a pixel is specified by
`bitsPerComponent'. The number of bytes per pixel is equal to
`(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap
consists of `bytesPerRow' bytes, which must be at least `width * bytes
per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
of the number of bytes per pixel. `data', if non-NULL, points to a block
of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
data for context is allocated automatically and freed when the context is
deallocated. `bitmapInfo' specifies whether the bitmap should contain an
alpha channel and how it's to be generated, along with whether the
components are floating-point or integer. */
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

如果 UIImage 中存储的是已经解码后的数据,速度就会快很多,所以优化的思路就是:在子线程中对图片原始数据进行强制解码,再将解码后的图片抛回主线程继续使用,从而提高主线程的响应速度。
我们需要使用的工具是 Core Graphics 框架的 CGBitmapContextCreate 方法和相关的绘制函数。总体的步骤是:

  1. 创建一个指定大小和格式的 bitmap context
  2. 将未解码图片写入到这个 context 中,这个过程包含了强制解码。
  3. 从这个 context 中创建新的 UIImage 对象,返回。

SDWebImage 实现

下面是SDWebImage的核心代码:

// 1. 从 UIImage 对象中获取 CGImageRef 的引用。这两个结构是苹果在不同层级上对图片的表示方式,UIImage 属于 UIKit,是 UI 层级图片的抽象,用于图片的展示;CGImageRef 是 QuartzCore 中的一个结构体指针,用C语言编写,用来创建像素位图,可以通过操作存储的像素位来编辑图片。这两种结构可以方便的互转:
CGImageRef imageRef = image.CGImage;

// 2. 调用 UIImage 的 +colorSpaceForImageRef: 方法来获取原始图片的颜色空间参数。
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);

// 3. 计算图片解码后每行需要的比特数,由两个参数相乘得到:每行的像素数 width,和存储一个像素需要的比特数4(这里的4,其实是由每张图片的像素格式和像素组合来决定的)
size_t bytesPerRow = 4 * width;

// 4. 最关键的函数:调用 CGBitmapContextCreate() 方法,生成一个空白的图片绘制上下文,我们传入了上述的一些参数,指定了图片的大小、颜色空间、像素排列等等属性。
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}

// 5. 调用 CGContextDrawImage() 方法,将未解码的 imageRef 指针内容,写入到我们创建的上下文中,这个步骤,完成了隐式的解码工作。
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);

// 6. 从 context 上下文中创建一个新的 imageRef,这是解码后的图片了。
CGImageRef newImageRef = CGBitmapContextCreateImage(context);

// 7. 从 imageRef 生成供UI层使用的 UIImage 对象,同时指定图片的 scale 和 orientation 两个参数。
UIImage *newImage = [UIImage imageWithCGImage:newImageRef
scale:image.scale
orientation:image.imageOrientation];

CGContextRelease(context);
CGImageRelease(newImageRef);

return newImage;

通过以上的步骤,我们成功在子线程中对图片进行了强制转码,回调给主线程使用,从而大大提高了图片的渲染效率。这也是现在主流 App 和大量三方库的最佳实践。

SDWebImage配置优化,减小CG-raster-data内存占用

在使用SDWebImage的时候,会默认保存图片解码后的内存,以便提高页面的渲染速度,但是这会导致内存的急速增加,所以可以在不影响体验的情况下,选择机型和系统,进行优化,避免大量的内存占用,引起OOM问题。关闭解码内存缓存的方法如下:

[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];

附录

WWDC2018 图像和图形的最佳实践
WWDC2018 深入iOS内存
移动端图片格式调研
谈谈 iOS 中图片的解压缩
iOS开发:图片格式与性能优化

Texture 控件

节点容器(Node Containers)

节点容器 等价于 UIKit
ASCollectionNode 代替 UICollectionView
ASPagerNode 代替UIPageViewController
ASTableNode 代替UITableView
ASViewController 代替UIViewController
ASNavigationController 代替UINavigationController,实现 ASVisibility 协议。
ASTabBarController 代替UITabBarController,实现 ASVisibility 协议。

为什么使用节点容器,不使用UIViewController

  • Node默认是异步布局/渲染,只有在需要将frame/contents等同步到UIView上才会回到主线程,使其空出更多的时间处理其他事件
  • Node自动管理其子节点实现智能预加载,这意味着节点的所有布局计算,数据读取,解码和渲染都将会异步完成,这就是为什么我们建议将节点放进节点容器中使用的原因。

节点(Node Subclasses)

节点 等价于 UIKit
ASDisplayNode 代替 UIView,所有的 Node 都继承自 ASDisplayNode。
ASCellNode 代替 UITableViewCell&UICollectionViewCell,需要和 ASTableNode,ASCollectionNode 和 ASPagerNode 共同使用。
ASScrollNode 代替 UIScrollView,这个节点对于创建自定义的,包含其他节点的可滚动区域非常有用。
ASEditableTextNode 代替 UITextView。
ASTextNode 代替 UILabel。
ASImageNode 代替 UIImage。
ASNetworkImageNode 代替 UIImage。
ASMultiplexImageNode 代替 UIImage。
ASVideoNode 代替 AVPlayerLayer。
ASVideoPlayerNode 代替 UIMoviePlayer。
ASControlNode 代替 UIControl。
ASButtonNode 代替 UIButton。
ASMapNode 代替 MKMapView。

继承关系

ASDisplayNode

  • ASCellNode
    • ASTextCellNode
  • ASCollectionNode
    • ASPagerNode
  • ASControlNode
    • ASButtonNode
    • ASImageNode
      • ASMapNode
      • ASMultiplexImageNode
      • ASNetworkImageNode
        • ASVideoNode
    • ASTextNode
  • ASEditableTextNode
  • ASScrollNode
  • ASTableNode
  • ASVideoPlayerNode

ASDisplayNode

异步绘制步骤

  1. 在ASDisplayNode.h中有相当多的注释,其中displaysAsynchronously属性大致描述了异步渲染的步骤:
/** Asynchronous rendering proceeds as follows:
* When the view is initially added to the hierarchy, it has -needsDisplay true.
* After layout, Core Animation will call -display on the _ASDisplayLayer
* -display enqueues a rendering operation on the displayQueue
* When the render block executes, it calls the delegate display method
(-drawRect:… or -display)
* The delegate provides contents via this method and an operation is added to
the asyncdisplaykit_async_transaction
* Once all rendering is complete for the current
asyncdisplaykit_async_transaction,
* the completion for the block sets the contents on all of the layers in the
same frame
*/
  1. ASDisplayNode还有一个属性shouldRasterizeDescendants。
    /**
    * @abstract Whether to draw all descendant nodes’ layers/views into this node’s
    > layer/view’s backing store.
    * @discussion
    * When set to YES, causes all descendant nodes’ layers/views to be drawn
    > directly into this node’s layer/view’s backing
    * store. Defaults to NO.
    * If a node’s descendants are static (never animated or never change attributes
    > after creation) then that node is a
    * good candidate for rasterization. Rasterizing descendants has two main
    > benefits:
    * 1) Backing stores for descendant layers are not created. Instead the layers
    > are drawn directly into the rasterized
    * container. This can save a great deal of memory.
    * 2) Since the entire subtree is drawn into one backing store, compositing and
    > blending are eliminated in that subtree
    * which can help improve animation/scrolling/etc performance.
    * Rasterization does not currently support descendants with transform,
    > sublayerTransform, or alpha. Those properties
    * will be ignored when rasterizing descendants.
    * Note: this has nothing to do with -[CALayer shouldRasterize], which doesn’t
    > work with ASDisplayNode’s asynchronous
    * rendering model.
    */
    当我们不需要分别关注单个CALayer,也不需要对他们进行操作时,就可以将所有的子node都合并到父node的backing store一并绘制,从而达到节省内存和提高性能的目的。

ASDisplayNode 对象方法

  • init 任何线程调用
    • 不应该在节点初始化方法中初始化任何 UIKit 对象,以及调用 node.layer node.view.x等与view 或 layer 有关的操作
    • touch事件或者手势绑定不要在这里执行
    • 这些事件应该在 didLoad 方法中进行。
  • didLoad 主线程调用
    • 当后台视图初始化完成时,它会被调用一次
    • 可以进行任何UI相关操作和初始化
    • 需要注意的是,这里是获取不到正确的frame的,获取frame要去layout方法
  • layoutSpecThatFits
    • 该方法定义了节点的布局,并在后台线程上进行了大量的计算。此方法是你声明、创建和修改 ASLayoutSpec 布局描述对象的地方,该对象描述了节点的 size,以及其子节点的 size 和 position,是你放置大部分布局代码的地方。
    • ASLayoutSpec 对象直到在此方法中返回前是可变的。 在这之后,这个对象将不可改变
    • 由于它在后台线程上运行,因此你不能在这个方法中调用 node.view 或 node.layer 以及它们的属性。
    • 此外,除非你明确知道自己在做什么,否则不要在此方法中创建其他节点
  • layout 主线程调用
    • 比较类似viewWillLayoutSubviews,在此方法中调用 super 将会使用 layoutSpec 对象计算布局,所有子节点都将计算其 size 和 position。
    • 适合进行hidden或者背景色等UI基本信息设置,但不包含布局设置
    • 你可以在 -layoutspec: 方法中设定背景颜色,但这可能会存在时序问题。
    • 如果要设置UIView相关对象,可以在layout中设置frame,尽量使用initWithViewBlock方法,放到后台进程
    • 官方举了个使用例子,colletionNode设置全屏适合在layout方法中调用

一些特有的东西

  • ASCellNode

    • 作用同等于 UITableViewCell 或 UICollectionViewCell,自带 indexPath 属性,不用注册cell
  • ASButtonNode

    @property (nonatomic, assign) CGFloat contentSpacing; // 设置图片和文字的间距
    @property (nonatomic, assign) ASButtonNodeImageAlignment imageAlignment;// 图片和文字的排列方式,
    /**
    ASButtonNodeImageAlignmentBeginning, // 图片在前,文字在后
    ASButtonNodeImageAlignmentEnd// 文字在前,图片在后
    */
  • ASNetworkImageNode

ASViewController

ASViewController 是一个常规的 UIViewController 子类,它具有管理节点的特殊功能。因为它是一个 UIViewController 子类,所以所有的方法都在主线程上被调用,并且你应该在主线程上创建至少一个 ASViewController。

ASViewController 对象方法

  • init
    • 这个方法在 ASViewController 的生命周期开始时被调用一次
    • 不要访问self.view或者self.node.view,这样会强迫视图被过早初始化,以免引发问题。
    • 自带initWithNode方法初始化,其自身管理node的方法和view类似
  • loadView
    • 官方不建议使用这个方法
  • viewDidLoad
    • 跟 UIViewController一样,这个方法在 -loadView 之后被执行
    • 可以访问 node.view 最早的方法,你可以在这份方法中任意修改 view 和 layer 或添加手势,这个方法在其所属的生命周期中,只会执行一次。
    • 布局代码不应该放在这个方法中,因为当界面重绘时,这里的代码不会被再次调用。UIViewController 中这个方法也是同样的,在这种方法中放置布局代码是一种不太好的做法,即使你的布局不会因为交互发生变化。
  • viewWillLayoutSubviews
    • 这个方法会与Node的 -layout 同时调用,它可能在 ASViewController 的生命周期中被多次调用
    • 当 ASViewController 的节点的边界发生改变,如旋转、分割屏幕、键盘弹出等行为,或者当视图的层次结构发生变化,如子节点添加、删除或改变大小时,这个方法将被调用。
    • 官方建议把布局逻辑写在这里(不强依赖于size的代码)
  • viewWillAppear / viewDidDisappear
    • 适合统计用户行为log
    • 适合进行Controller的动画操作

Texture 布局

ASLayout

@interface ASLayout : NSObject

@property (nonatomic, weak, readonly) id<ASLayoutable> layoutableObject; // 它所代表的布局元素
@property (nonatomic, readonly) CGSize size; // 元素的尺寸
@property (nonatomic, readwrite) CGPoint position; // 元素的位置
@property (nonatomic, readonly) NSArray<ASLayout *> *sublayouts; // 它所包含的sublayouts
@property (nonatomic, readonly) CGRect frame;

...

@end

可以看出,当一个node具备了确定的ASLayout对象时,它自身的布局也就随之确定了
只要Node能够计算出自己的ASLayout,父元素就可以完成对其的布局。这种方法(measureWithSizeRange:)将sizeThatFits和layoutSubviews结合在一起,一定程度上避免了相似代码的尴尬,但是计算上仍然是手动布局,不够简便。

ASLayoutSpec

  • 存储着ASDK可以“理解”的布局信息,供布局系统进行布局,这个类一般不会直接接触到(除非你需要自己实现一个LayoutSpec),基本可以理解为,这个类存储了以下三个重要信息:layout信息所作用的对象(layoutableObject)、layout的位置(position)、layout的大小(size)。
  • ASLayout类存储着布局的信息,但若所有布局都要靠我们自己创建ASLayout对象,那几乎也就和纯手算然后setFrame没有太大区别了,ASLayoutSpec及其子类可以看做是一个上层接口,让开发者不必考虑复杂的构建ASLayout对象的过程,而通过这些LayoutSpec类来构造自己的布局,这个类及其子类将会是我们在开发的时候见到最多的。
  • ASLayoutSpec只负责指定布局规则,而不关心其布局的具体是Node还是其他ASLayoutSpec
  • ASLayoutSpec 的作用更像是一个抽象类,在真正使用 ASDK 的布局引擎时,都不会直接使用这个类,而是会用类似 ASStackLayoutSpec、ASRelativeLayoutSpec、ASOverlayLayoutSpec 以及 ASRatioLayoutSpec 等子类。

Layout Types

2021-02-20-15167749195271

Layout继承关系

2021-02-20-15167751656197

ASLayoutSpec 与下面的所有的 Spec 类都是继承关系,在视图需要布局时,会调用 ASLayoutSpec 或者它的子类的 - measureWithSizeRange: 方法返回一个用于布局的对象 ASLayout。

几种布局规则

规则 描述
ASWrapperLayoutSpec 填充布局
ASStackLayoutSpec 盒子布局
ASInsetLayoutSpec 插入布局
ASOverlayLayoutSpec 覆盖布局
ASBackgroundLayoutSpec 背景布局
ASCenterLayoutSpec 中心布局
ASRatioLayoutSpec 比例布局
ASRelativeLayoutSpec 顶点布局
ASAbsoluteLayoutSpec 绝对布局

以上几种布局就不一一介绍了,具体可以看官方的DEMO

Layout Element 布局元素属性

  • ASStackLayoutElement Properties:只会在盒子布局中的的 subnodelayoutSpec 中生效;
  • ASAbsoluteLayoutElement Properties:只会在绝对布局中的的 subnodelayoutSpec 中生效;
  • ASLayoutElement Properties:适用于所有 NodelayoutSpec

ASStackLayoutElement Properties

请注意,以下属性只有在 ASStackLayoutsubnode上设置才会生效。

.style.spacingBefore

CGFloat 类型,direction 上与前一个 node 的间隔。

.style.spacingAfter

CGFloat 类型,direction 上与后一个 node 的间隔。

.style.flexGrow

Bool 类型,子节点尺寸总和小于 minimum ,即存在剩余空间时,是否放大。

.style.flexShrink

Bool 类型,子节点总和大于 maximum,即空间不足时,是否缩小。

.style.flexBasis

ASDimension 类型,描述在剩余空间是均分的情况下,应用 flexGrowflexShrink 属性之前,该对象在盒子中垂直或水平方向的初始 size

.style.alignSelf

ASStackLayoutAlignSelf 类型,描述对象在十字轴的方向,此属性会覆盖 alignItems,可选值有:

  • ASStackLayoutAlignSelfAuto
  • ASStackLayoutAlignSelfStart
  • ASStackLayoutAlignSelfEnd
  • ASStackLayoutAlignSelfCenter
  • ASStackLayoutAlignSelfStretch

.style.ascender

CGFloat 类型,用于基线对齐,描述对象从顶部到其基线的距离。

.style.descender

CGFloat 类型,用于基线对齐,描述对象从基线到其底部的距离。

ASAbsoluteLayoutElement Properties

请注意,以下属性只有在 AbsoluteLayoutsubnode上设置才会生效。

.style.layoutPosition

CGPoint 类型,描述该对象在 ASAbsoluteLayoutSpec 父规则中的位置。

ASLayoutElement Properties

请注意,以下属性适用于所有布局元素。

.style.width

ASDimension 类型,width 属性描述了 ASLayoutElement 内容区域的宽度。 minWidthmaxWidth 属性会覆盖 width, 默认值为 ASDimensionAuto

.style.height

ASDimension 类型,height 属性描述了 ASLayoutElement 内容区域的高度。 minHeightmaxHeight 属性会覆盖 height,默认值为 ASDimensionAuto

.style.minWidth

ASDimension 类型,minWidth 属性用于设置一个特定布局元素的最小宽度。 它可以防止 width 属性的使用值小于 minWidth 指定的值,minWidth 的值会覆盖 maxWidthwidth。 默认值为 ASDimensionAuto

.style.maxWidth

ASDimension 类型,maxWidth 属性用于设置一个特定布局元素的最大宽度。 它可以防止 width 属性的使用值大于 maxWidth 指定的值,maxWidth 的值会覆盖 widthminWidth 会覆盖 maxWidth。 默认值为 ASDimensionAuto

.style.minHeight

ASDimension 类型,minHeight 属性用于设置一个特定布局元素的最小高度。 它可以防止 height 属性的使用值小于 minHeight 指定的值。 minHeight 的值会覆盖 maxHeightheight。 默认值为 ASDimensionAuto

.style.maxHeight

ASDimension 类型,maxHeight 属性用于设置一个特定布局元素的最大高度,它可以防止 height 属性的使用值大于 maxHeight 指定的值。 maxHeight 的值会覆盖 heightminHeight 会覆盖 maxHeight。 默认值为 ASDimensionAuto

.style.preferredSize

CGSize 类型, 建议布局元素的 size 应该是多少。 如果提供了 minSizemaxSize ,并且 preferredSize 超过了这些值,则强制使用 minSizemaxSize。 如果未提供 preferredSize,则布局元素的 size 默认为 calculateSizeThatFits: 方法提供的固有大小。

此方法是可选的,但是对于没有固有大小或需要用与固有大小不同的的 size 进行布局的节点,则必须指定 preferredSizepreferredLayoutSize 中的一个,比如没这个属性可以在 ASImageNode 上设置,使这个节点的 size 和图片 size 不同。

警告:当 size 的宽度或高度是相对值时调用 getter 将进行断言。

.style.minSize

CGSize 类型,可选属性,为布局元素提供最小尺寸,如果提供,minSize 将会强制使用。 如果父级布局元素的 minSize 小于其子级的 minSize,则强制使用子级的 minSize,并且其大小将扩展到布局规则之外。

例如,如果给全屏容器中的某个元素设置 50% 的 preferredSize 相对宽度,和 200pt 的 minSize 宽度,preferredSize 会在 iPhone 屏幕上产生 160pt 的宽度,但由于 160pt 低于 200pt 的 minSize 宽度,因此最终该元素的宽度会是 200pt。

.style.maxSize

CGSize 类型,可选属性,为布局元素提供最大尺寸,如果提供,maxSize 将会强制使用。 如果子布局元素的 maxSize 小于其父级的 maxSize,则强制使用子级的 maxSize,并且其大小将扩展到布局规则之外。

例如,如果给全屏容器中的某个元素设置 50% 的 preferredSize 相对宽度,和 120pt 的 maxSize 宽度,preferredSize 会在 iPhone 屏幕上产生 160pt 的宽度,但由于 160pt 高于 120pt 的 maxSize 宽度,因此最终该元素的宽度会是 120pt。

.style.preferredLayoutSize

ASLayoutSize 类型,为布局元素提供建议的相对 sizeASLayoutSize 使用百分比而不是点来指定布局。 例如,子布局元素的宽度应该是父宽度的 50%。 如果提供了可选的 minLayoutSizemaxLayoutSize,并且 preferredLayoutSize 超过了这些值,则将使用 minLayoutSizemaxLayoutSize。 如果未提供此可选值,则布局元素的 size 将默认是 calculateSizeThatFits: 提供的固有大小。

.style.minLayoutSize

ASLayoutSize 类型, 可选属性,为布局元素提供最小的相对尺寸, 如果提供,minLayoutSize 将会强制使用。 如果父级布局元素的 minLayoutSize 小于其子级的 minLayoutSize,则会强制使用子级的 minLayoutSize,并且其大小将扩展到布局规则之外。

.style.maxLayoutSize

ASLayoutSize 类型, 可选属性,为布局元素提供最大的相对尺寸。 如果提供,maxLayoutSize 将会强制使用。 如果父级布局元素的 maxLayoutSize 小于其子级的 maxLayoutSize,那么将强制使用子级的 maxLayoutSize,并且其大小将扩展到布局规则之外。

一些布局小贴士

  • 更新布局,记得调用setNeedsLayout(经过本人测试,这个方法,同时会刷新相关的所有的Node)
  • automaticallyManagesSubnodes 方法可以自动管理子Node,不用手动addSubNode(需要注意的是,要实现对应的布局才会添加到父视图里)
  • inverted 可以翻转整个tableNode、CollectionNode的NSIndexPath,最下方的cellNode变成NSIndexPath(0, 0)
  • imageModificationBlock 是ImageNode的block,可以做一个后期处理,比如圆角裁剪等
  • shouldRasterizeDescendants 子树光栅化,意味着类似VVebo的效果,一个点内的所有node层级都会绘制到一个layer上
  • neverShowPlaceholders 同步绘制cellNode,主线程会锁死,直到界面完成绘制
  • hitTestSlop属性可以扩大点击响应区域(但是同时,也会影响frame)
    • enableHitTestDebug ADK提供很多debug功能,这个是测试相应区域的,可响应区域会变成绿色

一些官方的建议

  • 常见错误
    • 禁止在-init:方法访问node.view
    • 确保在node block外部访问data source
    • viewBlocks避免循环引用
  • 常见概念误区
    • ASCellNode不会复用
    • LayoutSpecs会在每次layout被调用时执行
    • AsyncDisplayKit有自己的强大布局API,用法上和系统的API又很大区别
  • 常见性能问题场景
    • 避免使用cornerRadius属性(以及shadowPath,border, mask),CALayer的cornerRadius是一个性能杀手,他是CALayer上效率最低,渲染密度最高的属性之一,这些属性会出发离屏渲染,也就是说滚动的同时实时渲染,具体可以查看官方的圆角指南
    • ASDK不支持Auto Layout
    • ASDisplayNode的指针会保持alive(据说系统中当layer被添加到UIView的super layer上后,也会一直保留,即使layer的delegate被置为weak)
  • Corner Rounding 关于圆角性能
    • 官网有一篇圆角的分析和优化文章,具体可以看文档

参考链接

为什么要使用代码混淆,下面来用一个 DEMO 简单的告诉大家如何获取 VIP

下面用一个实例来验证,没有代码混淆的程序我们可以看到什么,可以做什么?

首先我们创建一个简单的 命令行程序,EHUser里有一个 isVIP 方法判断用户是否是VIP:

2021-02-20-15104511577489

  1. 使用 class-dump 命令后工具,我们可以把程序的头文件导出来
class-dump -H ~/Downloads/HopperTest -o ~/Downloads/demo

2021-02-20-15104771683883

EHUser 的方法名一览无余

  1. 使用hopper工具,我们可以修改 isVIP 的方法逻辑
  • 查看 isVIP 方法
    2021-02-20-15104775647034
    甚至可以查看伪代码:
    2021-02-20-15104775986307

  • 这里可以把方法统一返回 YES,那么就算破解了这个isVIP方法了,添加mov rax,0x01始终返回yes

2021-02-20-15104775607304

  • 保存修改之后的程序,然后运行:
    2021-02-20-15104777215017

使用脚本混淆代码

原理:使用脚本,对项目里的方法名,进行宏替换

  1. 打开项目,选择target -> Build Phases,添加一个 Run Script
$PROJECT_DIR/gre3K/confuse.sh
#!/usr/bin/env bash

TABLENAME=symbols
SYMBOL_DB_FILE="symbols"
STRING_SYMBOL_FILE="$PROJECT_DIR/项目/func.list"

HEAD_FILE="$PROJECT_DIR/项目/codeObfuscation.h"

export LC_CTYPE=C
# #取以.m或.h结尾的文件以+号或-号开头的行 |去掉所有+号或-号|用空格代替符号|n个空格跟着<号 替换成 <号|开头不能是IBAction|用空格split字串取第二部分|排序|去重复|删除空行|删掉以init开头的行>写进func.list
# grep -h -r -I "^[-+]" $CONFUSE_FILE --include '*.[mh]' |sed "s/[+-]//g"|sed "s/[();,: *\^\/\{]/ /g"|sed "s/[ ]*</</"| sed "/^[ ]*IBAction/d"|awk '{split($0,b," "); print b[2]; }'| sort|uniq |sed "/^$/d"|sed -n "/^gre_/p" >$STRING_SYMBOL_FILE

#维护数据库方便日后作排重
createTable()
{
echo "create table $TABLENAME(src text, des text);" | sqlite3 $SYMBOL_DB_FILE
}

insertValue()
{
echo "insert into $TABLENAME values('$1' ,'$2');" | sqlite3 $SYMBOL_DB_FILE
}

query()
{
echo "select * from $TABLENAME where src='$1';" | sqlite3 $SYMBOL_DB_FILE
}

ramdomString()
{
openssl rand -base64 64 | tr -cd 'a-zA-Z' |head -c 16

}

rm -f $SYMBOL_DB_FILE
rm -f $HEAD_FILE
createTable

touch $HEAD_FILE
echo '#ifndef Demo_codeObfuscation_h
#define Demo_codeObfuscation_h' >> $HEAD_FILE
echo "//confuse string at `date`" >> $HEAD_FILE
cat "$STRING_SYMBOL_FILE" | while read -ra line; do
if [[ ! -z "$line" ]]; then
ramdom=`ramdomString`
echo $line $ramdom
insertValue $line $ramdom
echo "#define $line $ramdom" >> $HEAD_FILE
fi
done
echo "#endif" >> $HEAD_FILE


sqlite3 $SYMBOL_DB_FILE .dump

  1. 在项目里添加codeObfuscation.hfunc.list文件
  2. func.list 里填入需要替换的方法名,换行添加另一个
  3. 在项目里引用”codeObfuscation.h”,然后编译项目
  4. 正常情况下, 这个时候,填入的方法已经被替换成宏里,可以去codeObfuscation.h查看生成的宏

需要设置的地方

为了不影响开发测试

只在release下运行脚本,指定target运行脚本,在release下才导入宏头文件。

需要注意的问题

  • 注意:

    • 对于类方法,会出问题
    • 对象方法,某些情况下编译会报错,莫名其妙
    • 最好不要对大量使用的方法进行替换,会出现莫名其妙的问题,可能都启动不起来,编译一下就知道了
    • 不要引入到项目里,打包ipa里会存在这个文件
  • 建议:

    • 对关键方法使用
    • 对 target 进行判断,引用头文件
  • 还需要解决的问题

    1. 每次生成的宏都是随机的,这就对调试造成印象,自己查看崩溃日志都不知道是哪个方法出问题了。
      • 解决思路:使用加盐的哈希,这样自己可以通过key来配对真正的函数方法。
    2. 是否可以过审核?GRE3000貌似被查出来,发现脚本导入到项目里,会被打到ipa文件里,
    3. 每次生成的宏头文件,在git会有引起冲突的可能

链接

class dump使用方式和原理

一个数字的魔法——破解Mac上198元的Paw

破解InterfaceInspector的3种方法

打造自己的笔记系统-Atom

技术笔记应该的功能

  • 侧边目录,树叉分类,快速浏览
  • 代码高亮
  • 快速的查找:基于文件名的查找,基于关键字的全局查找
  • Markdown的支持
  • 可以运行代码片段
  • 支持Vim
  • 简单的IDE功能:代码错误提示

好吧,其实我就是想说,以上功能,Atom都可以搞定,当然你也可以用其他编辑器来实现,但是这里就不过多介绍了。

Atom 插件

必备插件:

  • highlight-selected 选择高亮
  • script 让你的代码在Atom里跑起来,cmd + i 直接运行
  • sync-setting 同步Atom配置文件
  • git-plus git必备
  • markdown-preview-plus md预览加强
  • markdown-writer md编辑加强
  • markdown-scroll-sync md文件同步滚动(这个插件,滚动貌似有些bug)

可选插件:

  • atom-beautify 格式化你的代码,让你的代码更加美观
  • minimap 在你右侧显示代码缩略图
  • vim-mode
  • pretty-json 格式化JSON插件
  • minimap 在你右侧显示代码缩略图
  • linter 代码错误提示,这个配置起来会麻烦一些,适合把编辑器当作IED用的人
  • file-icons 美化你的文件icon

快捷操作:

  • control + Shift + l 切换文件类型
  • cmd + t 快速搜索文件名
  • cmd + f 文件内搜索内容
  • shift + cmd + f 全局搜索内容
  • shift + cmd + \ 切换到目录操作,hjkl 移动

目录操作:

  • cmd-\ 或者 cmd-k cmd-b 显示(隐藏)目录树
  • ctrl-0 焦点切换到目录树(再按一次或者Esc退出目录树)
  • a 添加文件
  • shift a 添加文件夹
  • m 重命名
  • d 将当前文件另存为(duplicate)
  • i 显示(隐藏)版本控制忽略的文件

这些已经够日常使用了,其他的快捷键就不再罗列了。

一键打开笔记的思路

使用 Keyboard MaestroAlfredApple script
设置一个快捷键,指定使用Atom打开文件夹,当然,最好加一个判断:Atom是否在运行,否者隐藏,或者打开。

笔记同步

Dropbox + Git + sync-setting (Atom插件,同步Atom设置和插件)

效果展示

分析

如果直接打开系统的pagingEnabled分页, 它只会根据Cell的Bounds分页。如果cell不是屏幕宽度的话,就不能使用了,需要我们对滚动进行计算了。

方案一

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
*targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0
float pageWidth = (float)self.articlesCollectionView.bounds.size.width;
int minSpace = 10;

int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines
if (cellToSwipe < 0) {
cellToSwipe = 0;
} else if (cellToSwipe >= self.articles.count) {
cellToSwipe = self.articles.count - 1;
}
[self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}

在手指松开的时候,开始计算滚动的下一个位置。这个方法虽然也可以实现需求,但是效果比较生硬,不是很推荐。

具体可以看 :查看其中的「分页的几种实现方式」

方案二

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset
withScrollingVelocity:(CGPoint)velocity {

CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;

NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];

UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {

// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell) {
continue;
}

// == First time in the loop == //
if(!candidateAttributes) {
candidateAttributes = attributes;
continue;
}

if (fabs(attributes.center.x - proposedContentOffsetCenterX) <
fabs(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
candidateAttributes = attributes;
}
}

return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);

}

通过重写UICollectionViewLayout的方法,根据计算返回最终的悬停位置。

目前还存在的问题

滑动如果比较慢的话,它跳转到下一页的也会比较慢。

可以通过设置 decelerationRate 的值,来决定手指放开后的减速率,范围为0-1。然而经过测试,发现是可以加速,但是会出现莫名的卡顿。

2017年3月10号更新

使用UIScrollview 按照view宽度分页的方法:
详见DEMO: https://github.com/labmain/UIScrollViewCellWidthPage

解决了 UICollectionView 滑动比较慢的问题。

Demo 地址

github

LLDB的基础使用

help < command >

最简单命令是help,它会列举出所有的命令。
例如:

help print 或者 help thread。

print

缩写:prin or pri or p

例如:

(lldb) print count
(NSUInteger) $0 = 99
(lldb) print $0 + 7
(unsigned long) $1 = 106

说明:(其实 print 是 expression -- 的缩写)

打印变量

  • 默认是10进制

  • 十六进制

    (lldb) p/x 16
    0x10
  • 二进制

    (lldb) p/t 16
    0b00000000000000000000000000010000
    (lldb) p/t (char)16
    0b00010000

    你也可以使用 p/c 打印字符,或者 p/s 打印以空终止的字符串 (注:以 '\0' 结尾的字符串)。
    这里是格式的完整清单。

expression

缩写:e

例如:

(lldb) expression count = 42
(NSUInteger) $4 = 42
(lldb) print count
(NSUInteger) $5 = 42

打印对象

参数 e -O --(查看对象description 方法的结果)

(lldb) p @[ @"foo", @"bar" ]
(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"
(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)

参数 e -O --,缩写是:**po**

(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"

定义变量

(lldb) e int $a = 2
(lldb) p $a * 19
38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
2
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression

但是LLDB无法确定涉及的类型,需要指定类型:

(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
77

流程控制

Xcode上的流程控制按钮对应LLDB命令:

从左到右,四个按钮分别是:
continue
step over
step into
step out

  • process continue命令对应 :continue按钮
    (别名continue,或直接缩写为c

  • thread step-over命令对应:step over按钮
    (缩写:nextn

  • thread step in命令对应:step into
    (缩写:steps

Thread Return命令

它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。

(lldb) p i
(int) $0 = 99
(lldb) s
(lldb) thread return NO
(lldb) n
(lldb) p even0
(BOOL) $2 = NO
(lldb) frame info
frame #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17

LLDB中的断点

breakpoint list (或者 br li) 命令

管理断点

Xcode里你可以点击单个断点来开启或关闭:
LLDB 中使用 breakpoint enable <breakpointID> breakpoint disable <breakpointID>

(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1, resolved = 1, hit count = 1

1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, resolved, hit count = 1

(lldb) br dis 1
1 breakpoints disabled.
(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1 Options: disabled

1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, unresolved, hit count = 1

(lldb) br del 1
1 breakpoints deleted; 0 breakpoint locations disabled.
(lldb) br li
No breakpoints currently set.

breakpoint set 创建断点

(lldb) breakpoint set -f main.m -l 16
Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab

也可以使用缩写形式 br。虽然 b 是一个完全不同的命令 (_regexp-break 的缩写),但恰好也可以实现和上面同样的效果。

(lldb) b main.m:17
Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x000000010a3f6cc4
创建一个在函数开始处的断点:

C语言函数:

(lldb) b isEven
Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
(lldb) br s -F isEven
Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00

OC函数:

(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) b -[NSArray objectAtIndex:]
Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
(lldb) b +[NSSet setWithObject:]
Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820

条件断点(Action)

例子:
打印 i,然后大声念出那个句子,接着打印了自定义的表达式,
下面是在 LLDB 而不是 XcodeUI 中做这些的时候,看起来的样子:

(lldb) breakpoint set -F isEven
Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
(lldb) breakpoint modify -c 'i == 99' 1
(lldb) breakpoint command add 1
Enter your debugger command(s). Type 'DONE' to end.
> p i
> DONE
(lldb) br li 1
1: name = 'isEven', locations = 1, resolved = 1, hit count = 0
Breakpoint commands:
p i

Condition: i == 99

1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0

LLDB项目实用技巧

运行中更新UI属性

  1. 打印视图层级:

    (lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
    <UIWindow: 0x7f82b1fa8140; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x7f82b1fa92d0>; layer = <UIWindowLayer: 0x7f82b1fa8400>>
    | <UIView: 0x7f82b1d01fd0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7f82b1e2e0a0>>
  2. 有了上面的输出,我们可以获取这个 view:

    (lldb) e id $myView = (id)0x7f82b1d01fd0
  3. 然后在调试器中改变它的背景色:

    (lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
  4. 更新到渲染服务中:

    (lldb) e (void)[CATransaction flush] // caflush

    Push 一个 View Controller

  5. 获取rootVC:

    (lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]
  6. 然后 push 一个 child view controller:

    (lldb) e id $vc = [UIViewController new]
    (lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
    (lldb) e (void)[$vc setTitle:@"Yay!"]
    (lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
  7. 最后最新下面命令:

(lldb) caflush // e (void)[CATransaction flush]

查看按钮的 target

(lldb) po [$myButton allTargets]
{(
<MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)

整理自:https://objccn.io/issue-19-2/

扩展阅读:(Chisel-LLDB命令插件,让调试更Easy)
https://blog.cnbluebox.com/blog/2015/03/05/chisel/

问题

刚接手一个新项目,根据设计图来看,导航栏需要修改颜色,于是代码如下:

// 设置导航栏背景颜色
self.navigationController.navigationBar.barTintColor = HexRGB(0X000000);
// 关闭导航栏透明
self.navigationController.navigationBar.translucent = NO;

修改之后,界面显示出了问题,视图往下偏移了64,一个导航栏的高度

排查

查了一下原因,原来只要关闭导航栏透明,视图就会从导航栏的下方开始绘制。

如果直接修改子视图的frame虽然可以解决问题,但是在Storyborad显示却十分不爽:

解决方法

当然知道原因之后,就开始找解决方法,想办法把视图加载位置从(0,0)开始就可以了:

self.navigationController.navigationBar.translucent = NO;

完成效果:

扩展

当然如果想要从导航栏下面加载,有以下三种方法:

  1. 直接把translucent属性盖为YES

    self.navigationController.navigationBar.translucent = YES;
  2. 修改edgesForExtendedLayout成UIRectEdgeNone或者UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight

    self.edgesForExtendedLayout = UIRectEdgeNone;  
  3. 修改frame

    CGRectMake(0, 0, SCREENW, self.view.frame.size.height - 64)  

    最后

跟设计师沟通,根本不用修改颜色,给的颜色已经考虑到了透明度问题。。。。。


参考资料:http://blog.csdn.net/zyzxrj/article/details/47832337

循环引用的原因

循环引用问题的根源在于Block和obj可能会互相强引用,互相retain对方,这样就导致了retain cycle,最后这个Block和obj就变成了孤岛,谁也释放不了谁。比如以下几个例子:

几个示例 及 解决方法

@interface Class:()
{
NSString *strVar;//成员变量
}
@end

@implementation Class
@property (nonatomic, copy) NSString *strVVV;//成员属性


- (void)viewDidLoad {
// 强引用示例1
self.myBlock = ^{
[self doSomething];
};
// 强引用示例2
self.myBlock = ^{
strVar = @"haha";
};
// 强引用示例3
self.myBlock = ^{
_strVVV = @"vvvvv";
};
}
@end
+-----------+ +-----------+
| self | | Block |
---> | | --------> | |
| retain 2 | <-------- | retain 1 |
| | | |
+-----------+ +-----------+

说明:

  • 示例1:自己强引用自己
  • 示例2: strVar 是成员变量,属性本身为 strong,同样为强引用。
  • 示例3:这里在 Block 中虽然没直接使用 self,但使用了成员属性。在Block中使用成员属性,retain的不是这个属性,而会retain self,但是这个属性为 strong

解决方法:

 __weak typeof (self) weakSelf = self;

// 示例1
self.myBlock = ^{
[weakSelf doSomething];
};

// 示例2
// 成员变量的话,经过测试,不能使用 weakSelf 指向,也不能使用 self-> ,所以,最好还是改为成员属性

// 示例3
self.myBlock = ^{
weakSelf.strVVV = @"vvvvv";
};

+-----------+ +-----------+
| self | | Block |
--X->| | ----X---> | |
| retain 0 | < - - - - | retain 0 |
| | weak | |
+-----------+ +-----------+

另外一个示例:

// 以下代码会存在强引用
BlockTestVC *myController = [[BlockTestVC alloc] init];
myController.testBlock = ^(NSString *str) {
[myController dismissViewControllerAnimated:YES completion:nil];
};

// 解决上述强引用的方法,同样也是添加 __weak
BlockTestVC *myController1 = [[BlockTestVC alloc] init];
BlockTestVC * __weak weakMyViewController1 = myController1;
myController1.testBlock = ^(NSString *str) {
[weakMyViewController1 dismissViewControllerAnimated:YES completion:nil];
};

// 更好的方式是在使用__weak变量前,先用__strong变量把该值锁定
BlockTestVC *myController2 = [[BlockTestVC alloc] init];
BlockTestVC * __weak weakMyController2 = myController2;
myController2.testBlock = ^(NSString *str) {
BlockTestVC *strongMyController = weakMyController2;
if (strongMyController) {
[strongMyController dismissViewControllerAnimated:YES completion:nil];

} else {

}
};

多个对象之间引用问题

retain cycle不只发生在两个对象之间,也可能发生在多个对象之间,这样问题更复杂,更难发现

ClassA* objA = [[ClassA alloc] init];
objA.myBlock = ^{
[self doSomething];
};
self.objA = objA;

+-----------+ +-----------+ +-----------+
| self | | objA | | Block |
| | --------> | | --------> | |
| retain 1 | | retain 1 | | retain 1 |
| | | | | |
+-----------+ +-----------+ +-----------+
^ |
| |
+------------------------------------------------+

解决办法同样是用__weak打破循环引用

ClassA* objA = [[ClassA alloc] init];

__weak typeof(self)weakSelf = self;
objA.myBlock = ^{
[weakSelf doSomething];
};
self.objA = objA;

最后再说一下不会被循环引用的几种情况:

  • UIView 的 animations

  • GCD

  • NSOperation

  • 第三方框架:AFN、Masonry、等。。

  • block不是self的属性或者变量时,在block内使用self也不会循环引用:

    //block不是self的属性时,block内部使用self也不是循环引用
    Animal *animal = [[Animal alloc] init];
    animal.animalBlock = ^(void){
    NSLog(@"animal--> value:%@,address=%p,self=%p",self.person,self.person,self);
    };
  • 把block内部抽出一个作为self的方法,当使用weakSelf调用这个方法,并且这个方法里有self的属性,block不会造成内存泄露

    self.testBlock = ^()
    {
    [weakSelf test];
    };
    -(void)test
    {
    NSLog(@"%@",self.mapView);
    }
  • 当使用类方法有block作为参数使用时,block内部使用self也不会造成内存泄露

    [WDNetwork testBlock:^(id responsObject) {
    NSLog(@"%@",self.mapView);
    }];

    关于UIView和AFN不会强引用的原因:

    首先block循环引用的条件: block —>强引用(self) self —>强引用(block属性)

    UIView的动画block不会造成循环引用的原因就是,这是个类方法,当前控制器不可能强引用一个类,所以循环无法形成。

    而AFN无循环是因为绝大部分情况下,你的网络类对象是不会被当前控制器引用的,这时就不会形成引用环。当然我不知道AFN是否做了别的处理,按照这样来说的话,如果你的控制器强引用了这个网络类的对象,而且在block里面引用了当前控制器,也是会发生循环引用的。
    其他情况可以自己查询一下

参考内容:http://tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/

推荐阅读:理解 ARC 下的循环引用 http://ios.jobbole.com/82077/

Mac 相关

程序切换 cmd + ~
程序内切换 cmd + tap

偏好设置 cmd + ,

隐藏Xcode cmd + h
隐藏其他窗口 cmd + option + H

关闭窗口 cmd + W
关闭当前项目 cmd + option + W
关闭当前文件 cmd + control + W
关闭Xcode cmd + Q

保存文件 cmd + S
保存所有文件 cmd + option + S
另存为 cmd + shift + S
副本另存为 cmd + option + shift + S
还原到保存时状态 cmd + U

创建快照 cmd + control + S
显示快找 可设置快捷键(File -> Snapshots)

文件相关

新建项目 cmd + shift + N
新建文件 cmd + N
新建空文件 cmd + control + N

运行和编译

运行 cmd + R
编译 cmd + B
停止 cmd + .
单步调试 F6
跳入 F7
继续 F8

调试

在当前行断点标记和取消 cmd + \ /
清除工程缓存 cmd + shift + K
清楚控制台打印信息 cmd + K

视图

双编辑器

开启双编辑器 option + cmd + enter
关闭双编辑器 cmd + enter
打开某文件作为次编辑器 option + 点击

导航栏

左边的导航窗口开关 cmd + 0
右边的导航窗口开关 cmd + option + 0
切换使用工具选项卡 cmd + option 1-n

底部调试区显示隐藏开关 cmd + option + Y

查看相关

.m 与.h文件之间切换 cmd + control + 上/下
按照浏览文件的前后顺序切换 cmd + control + 左/右
快速显示文件位置 cmd + shift + J

跳转相关

快速打开匹配的文件 cmd + shift + O
属性方法跳转菜单 control + 6
魔法菜单 control + 1
跳转到指定行 cmd + L
跳转到光标出(视图切换) cmd + J

编辑

范围内重命名局部变量(不支持属性) control + cmd + E
查看属性或者变量的来源 control + cmd + J
隐藏和展开方法体 cmd + option + →/←

查找

项目中查找 cmd + shift + F
查找替换 cmd + control + F
查找上一个 cmd + G
查找下一个 cmd + shift + G

排版

注释 cmd + /

到行首 cmd + 左,control + A
到行尾 cmd + 右,control + E

删除一整行 到行尾 + cmd + delete

代码左缩进 cmd + [
代码右缩进 cmd + ]
代码上移一行 cmd + option + [
代码下移一行 cmd + option + ]

格式化代码 control + I

光标操作(Unix操作通用)

向右一个字符(forward) control + F
向左一个字符(backward) control + B
向前一行(previous) control + P
向后一行(next) control + N
去行首 control + A
去行尾(end) control + E
调换光标两边的字符(transpose) control + T
删除右侧的字符(delete) control + D
删除本行剩余的字符(kill) control + K