iOS 常见问题集锦
在开发过程中,遇到的问题和解决方案,当做备忘,以后会持续更新。
1. Attempted to dereference garbage pointer 0x20
访问了垃圾指针(野指针,对象的内存已经释放,但是指针还没置nil)。
调试技巧:打开“NSZombies”。这样做,就是每次你的应用程序试图释放一个对象时,Objective-C会用一个NSZombie对象替换它,当你当程序因“Attempted to dereference garbage pointer”崩溃的时候,它会提供一个更有用的错误信息。
2. iOS9以下使用苹方字体崩溃
iOS 8及以下是不支持系统苹方字体的,如果强行使用不但没有效果,而且还会引起崩溃。解决这个问题可以手动引入苹方字体文件,引用教程:https://www.jianshu.com/p/32ae87d4fe16
3. iOS处理图片的一些小Tip
参考博客:https://blog.ibireme.com/author/ibireme/
保存Gif到相册:iOS的相册是支持保存GIF和APNG动图的,只是不能直接播放。用[ALAssetsLibrary writeImageDataToSavedPhotosAlbum:metadata:completionBlock]可以直接把APNG/GIF的数据写入相册。如果想省事直接用UIImageWriteToSavedPhotosAlbum() 写相册,那么图像会被强制转成PNG。
把UIImage保存到磁盘的三种方式:
1.直接用NSKeyedArchiver把UIImage序列化保存。
2.用UIImagePNGRepresentation()把图片转为PNG保存。
3.用UIImageJPEGRepresentation()把图片压缩成JPEG保存。
比较:NSKeyedArchiver是调用了UIImagePNGRepresentation进行序列化的,用它来保存图片是消耗最大的。苹果对JPEG有硬编码和硬解码,保存成JPEG会大大缩减编解码的时间,也能减小文件体积。所以如果不包含透明像素时,UIImageJPEGRepresentation(0.9)是最佳的图片保存方式,其次是UIImagePNGRepresentation()。
UIImage 缓存:通过 imageNamed 创建 UIImage 时,系统实际上只是在 Bundle 内查找到文件名,然后把这个文件名放到 UIImage 里返回,并没有进行实际的文件读取和解码。当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。据我观察,在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。
我要是用 imageWithData 能不能避免缓存呢?
不能。通过数据创建 UIImage 时,UIImage 底层是调用 ImageIO 的 CGImageSourceCreateWithData() 方法。该方法有个参数叫 ShouldCache,在 64 位的设备上,这个参数是默认开启的。这个图片也是同样在第一次显示到屏幕时才会被解码,随后解码数据被缓存到 CGImage 内部。与 imageNamed 创建的图片不同,如果这个图片被释放掉,其内部的解码数据也会被立刻释放。
如何判断一个文件的图片类型?
通过读取文件或数据的头几个字节然后和对应图片格式标准进行比对。在这里有一个简单的函数,能很快速的判断图片格式
4. 适配iphoneX以及各种X
DEMO:https://github.com/shaozhe-chen/AdaptationTool
如果导航栏隐藏之后,可能很多同学适配iphoneX的都是这样的:
if(_IS_IPHONEX){//设置top}else{//设置top}
而且很多同学判断iPhone X的条件,应该很多都是通过尺寸判断的吧。这时候发布了iPhone XS Max,我们就不得不去对_IS_IPHONEX的判断条件做更改,然后发布,所以基于这种问题,我设计了一个思路:
iPhone X其实思路也很简单,就是获取到导航栏的子视图_UIBarBackground类的view,获取topSafeArea = view.height-44,这样就能获得安全区域的高度了。
5. iOS性能优化:
1.对象的创建会分配内存、调整属性(view的frame、bounds、transform等)、甚至还有读取文件等操作,比较消耗资源。
2.用轻量级的对象代替重量级的对象,比如如果不需要响应时间的话,可以使用CALayer代替UIView。
3.不涉及UI操作尽量放到后台线程去处理。
4.通过StoryBoard创建视图对象时,其资源消耗会比直接通过代码创建视图对象要大得多
5.如果对象可以复用,尽量加到复用池中。
6.视图的布局计算是最为常见的消耗CPU的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这块基本就不会产生性能问题(性能最好的布局是直接使用frame布局,如果不想手动调整frame属性,可以使用第三方工具方法替代:top、bottom、left、right、width、height)
7.对于复杂的视图,使用autolayout会随着视图数量的增长,CPU的消耗也会呈指数增长。
8.文本计算宽高会占用很大一部分资源,并且不可避免(也可以使用[NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高)。有这方面优化需求的,可以使用CoreText绘制文本,自己计算宽高。
9.图片的解码,当使用UIImage或CGImageSource的那几个方法创建图片的时候,图片数据并不会立刻解码,只有当图片设置到UIImageView或者CALayer.contents中去,并且CALayer被提交到GPU前,CGImage中的数据才会得到解码,这一步是发生在主线程的,并且不可避免。如果想绕开这个机制,常见的做法是在后台线程先把图片绘制到CGBitMapContext中,然后从Bitmap直接创建图片。
10.图片的绘制通常是指那些以CG开头的方法,把图像绘制到画布中,如何从画布创建图片并显示这样的一个过程。最常见的地方就是[UIView drawRect:]。由于CoreGraphic方法通常都是线程安全的,所以图像的绘制可以很容易放到后台线程中进行。
6. XCode 9.4.1 Could not build module and redefinition of module - Can't Build
7. 几个内存泄漏的常见的情景
1.block循环引用.
2.delegate循环引用
3.定时器没有销毁
4.非OC对象内存处理,没有手动释放
5.循环体中多次创建对象,造成内存暴涨
详情请见我另一边文章https://www.jianshu.com/p/d6821fd028f2
8.EXC_BAD_ACCESS(code=EXC_I386_GPFLT)
出现野指针,查看属性是否有使用assign修饰OC对象的。block是否在多线程中执行,但是没有使用weak-strong-dance的。
__weak typeof (self) weakSelf = self;
self.animations= ^{
__strong typeof(self) strongSelf = weakSelf;
weakSelf.str=@"ooo";
};
9. 在@protocol 和 category 中如何使用 @property
在@protocol中使用@property只会声明Getter和Setter方法,并没有实现Getter和Setter方法。我们在@protocol中使用@property是希望遵守我协议的对象能实现该属性的Getter和Setter方法,如果没有实现代理,直接调用 _delegate.__delegate.appleName = @"abc";会直接crash,报:unrecognized selector sent to instance。正确使用如下:
声明代理 代理实现在category中使用@property也只会声明Getter和Setter方法,并没有实现Getter和Setter方法。如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:objc_setAssociatedObject、objc_getAssociatedObject,代码如下:
10. atomic不是真正意义的线程安全
atomic修饰的属性,只保证这个属性的getter、setter方法是线程安全的,并无法保证这个对象线程安全。
ARC下原子性声明了一个nonatomic的array,重写getter、setter方法,使用@synchronize同步锁保证readWrite线程安全。但是却无法保证这个对象的线程安全,原因是:如果线程A在对array进行release操作,同时线程B在访问array就会导致crash。
11. setNeedsLayout和layoutIfNeeded的使用
继承于UIView的子类重写,进行布局更新,刷新视图。如果某个视图自身的bounds或者子视图的bounds发生改变,那么这个方法会在当前runloop一个周期结束的时候被调用。为什么不是立即调用呢?因为渲染毕竟比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个周期,所有UI控件在布局上的变动统一生效并且在视图上更新,苹果通过这种高性能的机制保障了视图渲染的流畅性。
· setNeedsLayout
标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用。
· layoutIfNeeded
如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。
总结:如果需要马上更新视图,可以这么调用
[self setNeedsLayout];
[self layoutIfNeeded];
12. layoutSubviews和drawRect的调用时机
· layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews(还没添加到视图,不会调用layoutSubviews)。
2、第一次addSubview会触发layoutSubviews和一次drawRect。
3、改变一个UIView的bounds会触发layoutSubviews(注意:是改变bounds而不是frame,因为改变x,y是不会调用layoutSubviews的),当然前提是bounds的值设置前后发生了变化。
4、滚动一个UIScrollView引发UIView的重新布局会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、直接调用setNeedsLayout 或者 layoutIfNeeded。
· drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 调用是在Controller->loadView, Controller->viewDidLoad 两方法之后调用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改bounds的时候自动调用drawRect:。
3、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
13. iPhone5机型以下(包含)获取时间戳异常问题
在一次获取时间戳时,发现iPhone5获取到的时间戳是9位的,导致时间戳异常。
// 64位架构,使用NSInteger 1508461200000 10月20日09点
// 32位架构,使用NSInteger 927679104 01月12日01点
经过研究才发现,原来iPhone5机型以下都是使用32位的CPU,而我使用NSInteger来修饰时间戳,导致溢出。
如图所示,我们可以看出,在64位的CPU下,NSInteger是long,所以获取13位的时间戳是没有问题的。而在32位CPU下,NSInteger是int,所以就导致了溢出。
问题解决:
使用时间戳时,我们有个专门用于时间戳的类:NSTimeInterval,使用它来代替NSInteger能保证万无一失。
14. 自修复unrecognized selector sent to instance造成的crash
具体内容看我的demo,里面有注释,应该很容易看懂。