iOS开发琐碎知识点

作为一个半路出家的小白、一个为了生活不断积累技术知识的俗人,本着急功近利的的精神把工作中遇到的小问题、琐碎知识点总结下来留给我闺女。

1. bringSubViewToFront 不起作用的问题

bringSubViewToFront方法只对该view的childView起作用,而对grandView不起作用,可以用上面的方法,把grandView前置。

最近项目中遇到一个坑,有一个父view,该父view中添加了第三方SDK中的view,所以没有准确判断view的层级关系,导致我添加的一个label无论怎样调用bringSubViewToFront都无法显示在最前面。后来发现原因是,我的label添加后,调用bringSubViewToFront过早,因为这个时候第三方view还没加载上去,所以调用bringSubViewToFront也不会将label显示在第三方veiw的前面。要在第三方view加载完成后,才会起作用。

总结:bringSubViewToFront方法只会将某个view放在其父view的所有已存在的子view的前面。后来添加的view依然有可能遮挡住这个view.

2. 强制某个 viewController 横屏竖屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*方法*/
- (void)interfaceOrientation:(UIInterfaceOrientation)orientation
{
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {

SEL selector = NSSelectorFromString(@"setOrientation:");

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];

[invocation setSelector:selector];

[invocation setTarget:[UIDevice currentDevice]];

int val = orientation;

[invocation setArgument:&val atIndex:2];

[invocation invoke];
}
}

举个栗子:

1
2
3
4
5
6
7
8
9
// 横屏
- (IBAction)landscapAction:(id)sender {
[self interfaceOrientation:UIInterfaceOrientationLandscapeRight];
}

// 竖屏
- (IBAction)portraitAction:(id)sender {
[self interfaceOrientation:UIInterfaceOrientationPortrait];
}

3. nib 文件加载过程

Outlets are set after -init and before -awakeFromNib. If you want to access outlets, you need to do that in -awakeFromNib or another method that’s executed after the outlets are set (e.g. -[NSWindowController windowDidLoad]).

When a nib file is loaded:

  1. Objects in the nib file are allocated/initialised, receiving either -init, -initWithFrame:, or -initWithCoder:
  2. All connections are reestablished. This includes actions, outlets, and bindings.
  3. -awakeFromNib is sent to interface objects, file’s owner, and proxy objects.

4. table view 优化

1
2
3
4
5
6
7
8
9
10
1. cell 复用
2. view 的opaque属性尽可能设置为 YES
3. 避免渐变、图像伸缩、离屏渲染
4. 如果cell高度不变,缓存计算出的高度
5. 如果cell展示的内容包含网络请求的内容,缓存并且异步加载
6. 阴影用shadow path
7. 尽可能减少cell的subviews数量
8. 在cellForRowAtIndexPath回调中尽可能少做事,如果必须,尽可能缓存耗时操作的结果
9. 抽象出合理的数据结构来展示信息
10.直接设置rowHeight、sectionFooterHeight、sectionHeaderHeight的值,不要在相应的代理中设置

5. xib 中使用 autolayout 布局

关于 xib 中使用 autolayout 布局的问题,下面两张图应该说明的很明白了。

6. 使用 setValue:forKey:设置对象属性值

在使用 setValue:forKey: 设置对象属性值时,不管该属性是否为只读的、不管在 .h 或者 .m 文件中,都能够成功设置! 果如你重写了这个属性的 setter 方法,那么也会走该属性的 setter 方法。

7. Class / id / objc_object

Objective-C类是由 Class 类型来表示的,它实际上是一个指向
objc_class 结构体的指针。Class 的定义如下:

1
typedef struct objc_class *Class;

查看 objc/runtime.h 中 objc_class 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

由 objc_class 结构体可知,在 objective-C 中,所有的类自身也是一个对象,里面有一个 Class 类型的 isa 指针,指向 metaClass(元类)。

super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。

cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,提高了调用效率。

objc_object 是表示一个类的实例的结构体,它的定义如下(objc/objc.h):

1
2
3
4
5
6
7
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

id 类型是一个指向 objc_object 结构体类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

在 Objective-C,一个对象的类由它的 isa 指针决定。isa 指针指向这个对象的 Class。在 Objective-C 中,对象的一个重要的特性是,你可以向它们发送消息:

  1. 当你向一个对象发送消息,就在那个对象的方法列表中查找那个消息。
  2. 当你想一个类发送消息,就再那个类的 meta-class 中查找那个消息。

meta-class 是一个类对象的类

当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

meta-class 之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的 meta-class ,因为每个类的类方法基本不可能完全相同。

meta-class 是必须的,因为它为一个 Class 存储类方法。每个类都必须有一个唯一的 meta-class,因为每个 Class 都有一个可能不一样的类方法。

meta-class,如之前的 Class,同样是个对象。这就意味着你也可以在它上面调用方法。自然的,这就意味着它也必须有一个类

所有的 meta-class 使用它们基类的 meta-class (继承层次中最顶层的 Class 的 meta-class)作为它们自己的类。这就是说所有继承自 NSObject 的类(大部分的类),以 NSObject 的 meta-class 作为自己的 meta-class 的类。

遵循这个规则,所有的 meta-class 使用基类的 meta-class 作为他们的类,任何基类的 meta-class 将会是他们自己(它们的 isa 指向他们自己)。这就是说 NSObject 的 meta-class 的 isa 指针指向它们自己(是自己的一个实例)。

8. nullable、__nullable、_Nullable 究竟有什么区别

  1. 对于属性、方法返回值、方法参数的修饰,使用: nonnull/nullable

  2. 对于 C 函数的参数、Block 的参数、Block 返回值的修饰,使用: _Nonnull/_Nullable , 建议弃用 nonnull/nullable

如果需要每个属性或每个方法都去指定 nonnull 和 nullable ,将是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 。在这两个宏之间的代码,所有简单指针对象都被假定为 nonnull ,因此我们只需要去指定那些 nullable 指针对象即可。

疑问:为什么已经有了 nonnull/nullable ,为什么还要增加 _Nonnull/_Nullable ?

9. 使用pathForResource获取不到 bundle 里的资源,返回nil的问题

通过 右键->add files to 的方式将 Bundle 添加到工程里面,但是使用[[NSBundle mainBundle] pathForResource:@”name” ofType:@”type”]时,无论如何都找不到文件,经过了重启工程 、clear工程以及重启电脑等方式都无法解决问题。经过思考和测试,感觉这可能是xcode的一个bug。

解决办法及原理是这样的,[NSBundle mainBundle]其获取的路径是你程序的安装路径下的资源文件位置。 在xcode中采用add file to 方式添加文件时,一般情况下xcode会自动将文件添加到你的资源文件,而且,这些文件在你工程的 build Phases 中的 copy Bundle Resources 中可以查看到。但是有时候,由于xcode的问题,采用add files to 不能自动添加到你的资源文件中,这时,可以采用copy Bundle Resources下面的“+”号,手动将文件添加到你的资源文件中,这样就可以解决问题了。

10. 新建 window 并设置它的 rootViewController 遇到的状态栏问题

新建 window 并设置它的 rootViewController ,在 rootViewController 中想要自定义状态栏样式或者隐藏状态栏,遇到的问题是,如果该 window 的 frame 不等于 [UIScreen mainScreen].bounds ,那么在 rootViewController 中想要使用下面两个方法设置状态栏时,是无效的,因为此时这两个方法不会调用:

1
2
- (UIStatusBarStyle)preferredStatusBarStyle;
- (BOOL)prefersStatusBarHidden

Apple 这样做是有理由的,比如新建 window 不能覆盖整个屏幕,只是一个很小的悬浮框,此时的 rootViewController 不需要控制状态栏样式。

当不设置 window 的 frame 或者设置 window 的 frame 为 [UIScreen mainScreen].bounds 时,在 rootViewController 中才能够设置状态栏样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
//能够正确设置状态栏的代码:

UIWindow *delegateWindow = [UIApplication sharedApplication].delegate.window;
CGRect rect = [UIScreen mainScreen].bounds;
self.window = [[UIWindow alloc] init];
self.window.rootViewController = self.rootVC;
[self.window makeKeyAndVisible];
self.window.frame = CGRectMake(0, -rect.size.height, rect.size.width, rect.size.height);
// 保持原先的keyWindow,避免一些奇怪的bug
[delegateWindow makeKeyWindow];
[UIView animateWithDuration:0.3 animations:^{
self.window.frame = rect;
} completion:NULL];

11. 当 present 一个viewController并且设置这个viewController背景透明度时,背景色变黑的问题

想模态展示一个VC窗口,设置它的背景透明度为0.5,却发觉prsent后的背景色变为黑色。

原因是:

NavigationController and the View Controllers are designed in such a way that only one view controller may show at a time. When a new view controller is pushed/presented the previous view controller will be hidden by the system. So when you reduce the modal view’s alpha you will possibly see the window’s backgroundColor (the black color you see now).

If you want a translucent view to slide-in over the main view, you can add the view as the subView of main view and animate it using UIView Animations.

解决方法可以是 :直接动画添加view
或者 设置模态VC的属性 modalPresentationStyle 为
UIModalPresentationCustom

12. App支持从4种来源去打开一个VC页面

  • Push推送
  • App外部网页打开
  • App内部网页打开
  • 应用内点击打开

这四种方式均跳转到 DetailViewController 界面。普通的跳转依然可以满足该场景, 最简单的解决方案是在四个不同的地方都写一个独立的界面打开逻辑。作为一名有追求的开发者, 这么冗余的四份入口代码显然不合适。

一种解决方案是采用 URL 协议统一跳转。每个 viewController 页面定义与之对应的 URL,在各个入口只需要调用打开该URL的方法即可完成页面的创建以及跳转。

基于URL的路由方案:

SNMediator 是用于 iOS 应用进行模块化拆分的中间件框架,它不依赖任何第三方库,基于 URL 协议实现三端(iOS, Android, H5)统一的页面跳转方式。

例如:你的 APP DetailViewController 界面对应URL定义为:
myapp://businessModule/goodsdetails/?id=100
其中,scheme为 myapp,host为 businessModule,path为 goodsdetails,携带参数 id=100.

使用 SNMediator 跳转页面方法如下:

1
2
3
+ (BOOL)routeURL:(nonnull NSURL *)URL withParams:(nullable NSDictionary *)params completion:(void(^ _Nullable)(id _Nullable result))completion;

[SNMediator routeURL:@"myapp://businessModule/goodsdetails/?id=100" params:nil completion:NULL];

SNMediator 支持通过字典传递额外的自定义复杂对象,也支持URL自身携带参数。所以4种入口都可以通过这一句代码调用完成页面跳转,保持了不同入口跳转同一界面的代码一致性。

13. iOS 中一个viewController只能 present 出来唯一一个其他viewController

如果你要在同一个 viewController 中上同时 present 两个viewController,比如:

1
2
3
4
5
SNViewControllerOne *oneVC = [[SNViewControllerOne alloc] init];
[self presentViewController:oneVC animated:YES completion:^{
SNViewControllerTwo *twoVC = [[SNViewControllerTwo alloc] init];
[self presentViewController:twoVC animated:YES completion:NULL];
}];

此时界面上只会显示 oneVC 的视图,不会显示 twoVC 并且 twoVC 也不存在于视图栈中。这是因为当一个新的 viewController 被 push/present 时,先前的那个 viewController 就会被系统隐藏,所以不会出现在视图栈中。

When a new view controller is pushed/presented the previous view controller will be hidden by the system.

并且在控制台还会给出警告:

Warning: Attempt to present < SNViewControllerTwo: 0x7ff813c5c760> on <SNRootViewController: 0x7ff813e307c0> whose view is not in the window hierarchy!

告诉你当 SNRootViewController present 出来 SNViewControllerOne 后,再试图 present SNViewControllerTwo 时,SNRootViewController 已经被隐藏,不再存在于 window 的视图层级中,所以也就无法在 SNRootViewController 基础上继续 present 另一个视图。

14. UIGestureRecognizerState 各个状态的变化

UIGestureRecognizerState的定义如下:

1
2
3
4
5
6
7
8
9
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible,
UIGestureRecognizerStateBegan,
UIGestureRecognizerStateChanged,
UIGestureRecognizerStateEnded,
UIGestureRecognizerStateCancelled,
UIGestureRecognizerStateFailed,
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

Remarks:
This describes the state of a UIGestureRecognizer. All of UIGestureRecognizers start in the Possible state. Once one or more touches has been received, the recognizers transition to the Began state. For one-shot patterns (like Tap), this will either transition into the Recognized state or the Failed state. For continuous gestures (like panning, pinching, rotating) the recognizer will transition to the Changed state and emit multiple calls back to the action and finally transition to either the Ended or Cancelled states.

15. NULL / nil / Nil / NSNull

C 用 0 来作为不存在的原始值,而NULL作为指针,这在指针环境中相当于 0。

Objective-C 在 C 的表达不存在的基础上增加了nil。nil 是一个指向不存在的对象指针。虽然它在语义上与NULL 不同,但它们在技术上是相等的。

在框架层面,Foundation 定义了 NSNull,即一个类方法 +null,它返回一个单独的 NSNull 对象。NSNull 与 nil 以及 NULL 不同,因为它是一个实际的对象,而不是一个零值。、

在 Foundation/NSObjCRuntime.h 中,Nil 被定义为指向零的类指针。它并不常常出现,但至少值得注意。

16. CGGeometry

GRect 用于表示屏幕上绘制的所有视图的 frame,一个程序员操作矩形几何体的能力决定着他在图形编程上的成功。

变换

几何变换,这些函数返回在传入的矩形中做某些特定操作后的 CGRect

  • CGRectOffset: 返回一个原点在源矩形基础上进行了偏移的矩形
1
2
3
4
5
CGRect CGRectOffset(
CGRect rect,
CGFloat dx,
CGFloat dy
)

注意,用这个你只改变了矩形的原点。它不仅能让你在同时改变水平和垂直位置的时候减少一行代码,更重要的是,它所表示的平移比直接分开操作原点的值更具有几何意义。

  • CGRectInset: 返回一个与源矩形共中心点的,或大些或小些的新矩形
1
2
3
4
5
CGRect CGRectOffset(
CGRect rect,
CGFloat dx,
CGFloat dy
)

注意,用这个你只改变了矩形的原点。它不仅能让你在同时改变水平和垂直位置的时候减少一行代码,更重要的是,它所表示的平移比直接分开操作原点的值更具有几何意义。

  • CGRectInset: 返回一个与源矩形共中心点的,或大些或小些的新矩形
1
2
3
4
5
CGRect CGRectInset(
CGRect rect,
CGFloat dx,
CGFloat dy
)

想一个视图中的视图更好看吗?用CGRectInset给它设置一个 10pt 的边距吧。需要记住的是,矩形将围绕它的中心点进行缩放,左右分别增减dx(总共2 x dx),上下分别增减 dy(总共 2 x dy)。

如果你用 CGRectInset 作为缩放矩形的快捷方法,一般通用的做法是嵌套调用CGRectOffset,把CGRectInset的返回值作为CGRectOffset的参数。

  • CGRectIntegral: 返回包围源矩形的最小整数矩形
1
2
3
CGRect CGRectIntegral (
CGRect rect
)

将CGRect 取整到最近的完整点是非常重要的。小数值会让边框画在像素边界处。因为像素已经是最小单元(不能再细分),小数值会使绘制时取周围几个像素的平均值,这样看起来就模糊了。

CGRectIntegral 将表示原点的值向下取整,表示大小的值向上取整,这样就保证了你的绘制代码平整地对齐到像素边界。

作为一个经验性的原则,如果你在执行任何一个可能产生小数值的操作(例如除法,CGGetMid[X|Y],或是 CGRectDivide),在把一矩形作为视图的边框之前应该用CGRectIntegral正则化它。

从技术上讲,坐标系讲的是点,而视网膜屏一个点中有四个像素,所以它在奇数像素± 0.5f处绘制也不会产生模糊。

取值辅助函数

CGRectGet[Min|Mid|Max][X|Y]

1
2
3
4
5
6
CGRectGetMinX
CGRectGetMinY
CGRectGetMidX
CGRectGetMidY
CGRectGetMaxX
CGRectGetMaxY

这六个函数返回矩形x或y的最小、中间或最大值,原型如下:

1
2
3
CGFloat CGRectGet[Min|Mid|Max][X|Y] (
CGRect rect
)

用这些函数代替诸如frame.origin.x + frame.size.width之类的代码将更加清晰、语义上更为生动的(特别是用取中间和取最大函数)。

CGRectGet[Width|Height]

  • CGRectGetHeight: 返回矩形的高度。
1
2
3
CGFloat CGRectGetHeight (
CGRect rect
)
  • CGRectGetWidth: 返回矩形的宽度。
1
2
3
CGFloat CGRectGetWidth (
CGRect rect
)

跟之前的函数一样,用CGRectGetWidth 和 CGRectGetHeight返回CGRect的size成员更可取。这绝不只是节省了几个字符,语义上的清晰胜过简洁。

常量

这里列出了三个我们必须了解的特殊矩形值,它们都有一些独一无二的属性:

CGRectZero, CGRectNull,和 CGRectInfinite

  • const CGRect CGRectZero: 一个原点在(0, 0),且长宽均为 0 的常数矩形。这个零矩形与 CGRectMake(0.0f, 0.0f, 0.0f, 0.0f) 是等价的。

  • const CGRect CGRectNull: 空矩形。这个会在,比如说,求两个不相交的矩形的相交部分时返回。注意,空矩形不是零矩形。

  • const CGRect CGRectInfinite: 无穷大矩形。

CGRectZero 可能是所有这些特殊矩形中最有用的了。当初始化一个视图时,它们的边框通常设置为CGRectZero,把布局放到 -layoutSubviews中。

CGRectNull 跟 CGRectZero 是两回事,尽管它隐隐约约让你感觉到NULL == 0。这个值在概念上与NSNotFound相近,所以它表示预期值的缺失。请注意函数可能返回 CGRectNull,同时也应让它能正确处理传入的CGRectIsNull。

CGRectInfinite 是以上所有当中最有异国风情的,并且有一些最有趣的属性。它与所有的点或矩形相交,包含所有矩形,且它与任何矩形的并集等于它自身。用 CGRectIsInfinite 来检查一矩形是否为无限大。

最复杂、最容易误解、也最有用的CGGeometry 函数:CGRectDivide。
  • CGRectDivide: 将源矩形分为两个子矩形。
1
2
3
4
5
6
7
void CGRectDivide(
CGRect rect,
CGRect *slice,
CGRect *remainder,
CGFloat amount,
CGRectEdge edge
)

CGRectDivide 用以下方式将矩形分割为两部分:

(1). 传入一个矩形并选择一条edge(上,下,左,右)
(2). 平行那个边在矩形里量出amount的长度
(3). 从edge 到量出的amount区域都保存到slice 参数中
(4). 剩余的部分保存到remainder 参数中

其中 edge 参数是一个CGRectEdge 枚举类型:

1
2
3
4
5
6
enum CGRectEdge {
CGRectMinXEdge,
CGRectMinYEdge,
CGRectMaxXEdge,
CGRectMaxYEdge
}

CGRectDivide 用于在几个视图之间分割可用空间真是太完美了(把它在随后的remainder容纳多于两个的视图)。下次当你需要手机布局一个UITableViewCell时试试吧。CGRectDivide is perfect for dividing up available space among several views (call it on subsequent remainder amounts to accommodate more than two views). Give it a try next time you’re manually laying-out a UITableViewCell.

17. layoutSubViews 的使用

当我们熟练使用 autolayout 时,或许会忽略了 layoutSubViews 的存在,但是不管是使用 autoLayout 还是直接 frame 布局,只要重写了 layoutSubViews,视图都会在布局它的子视图时调用 layoutSubViews(使用 autolayout 布局的话,不能在 layoutSubViews 再更改 frame 的值了)。那么我们一般什么时候需要重写它?既然 layoutSubViews 不能直接调用,那么系统在什么时候会调用它?触发的条件是什么呢?

很显然,当你需要使用纯frame布局一个视图myView的子视图(view1 view2 view3等…)时,就需要重写 myViewlayoutSubViews 方法,计算并设置它的子视图的frame从而完成布局。

先看一段官方文档:

The default implementation of this method does nothing on iOS 5.1 and earlier. Otherwise, the default implementation uses any constraints you have set to determine the size and position of any subviews.Subclasses can override this method as needed to perform more precise layout of their subviews. You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.

主要意思是,对于子视图的布局,如果 autoresizing 以及基于约束的布局方式不能满足你实际的需求时,才应该重写该方法。不要直接调用此方法。如果你想强制更新布局,你可以调用setNeedsLayout方法;如果你想立即数显你的views,你需要调用layoutIfNeeded方法。

layoutSubviews 以下情况会被调用

苹果官方文档已经强调,不能直接调用layoutSubviews对子视图进行重新布局。以下几种情况layoutSubviews会被调用。

  • 直接调用setLayoutSubviews(这个在上面苹果官方文档里有说明)
  • addSubview的时候。
  • 当视图的的frame发生改变的时候。
  • 滑动UIScrollView的时候。
  • 旋转Screen会触发父UIView上的layoutSubviews事件。
  • 改变一个子视图大小的时候,也会触发父视图上的 layoutSubviews 事件。

注意:

当view的frame的值为0的时候,addSubview也不会调用layoutSubviews的。
layoutSubviews方法在对子视图进行布局的时候非常方便。