问题:view的bounds的x、y能更改吗,如果更改了会怎样?
答:先看到下面的代码
-(CGRect)frame{
return CGRectMake(self.frame.origin.x,self.frame.origin.y,self.frame.size.width,self.frame.size.height);
}
-(CGRect)bounds{
return CGRectMake(0,0,self.frame.size.width,self.frame.size.height);
}
很明显,bounds的原点是(0,0)点(就是view本身的坐标系统,默认永远都是0,0点,除非调用了setbounds函数),而frame的原点却是任意的(相对于父视图中的坐标位置)。
frame: 该view在父view坐标系统中的位置和大小。(参照点是,父亲的坐标系统)
bounds:该view在本地坐标系统中的位置和大小。(参照点是,本地坐标系统,就相当于ViewB自己的坐标系统,以0,0点为起点)
center:该view的中心点在父view坐标系统中的位置和大小。
其实本地坐标系统的关键就是要知道的它的原点(0,0)在什么位置(这个位置又是相对于上层的view的本地坐标系统而言的,最上层view就是 window它的本地坐标系统原点就是屏幕的左上角了)。
通过修改view的bounds属性可以修改本地坐标系统的原点位置。
所以,bounds的有这么一个特点:
它是参考自己坐标系,它可以修改自己坐标系的原点位置,进而影响到“子view”的显示位置。UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 200, 200)];
[view1 setBounds:CGRectMake(-30, -30, 200, 200)];
view1.backgroundColor = [UIColor redColor];
[self.view addSubview:view1];//添加到self.view
NSLog(@"view1 frame:%@========view1 bounds:%@",NSStringFromCGRect(view1.frame),NSStringFromCGRect(view1.bounds));
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view2.backgroundColor = [UIColor yellowColor];
[view1 addSubview:view2];//添加到view1上,[此时view1坐标系左上角起点为(-30,-30)]
核心点:v2的0,0相对于v1的负30就要往右下跑
问题:UIApplication相关
一、UIApplication
1.简单介绍
(1)UIApplication对象是应用程序的象征,一个UIApplication对象就代表一个应用程序。
(2)每一个应用都有自己的UIApplication对象,而且是单例的,如果试图在程序中新建一个UIApplication对象,那么将报错提示。
(3)通过[UIApplication sharedApplication]可以获得这个单例对象
(4) 一个iOS程序启动后创建的第一个对象就是UIApplication对象,且只有一个(通过代码获取两个UIApplication对象,打印地址可以看出地址是相同的)。
(5)利用UIApplication对象,能进行一些应用级别的操作
2.应用级别的操作示例:
(1)设置应用程序图标右上角的红色提醒数字(如QQ,微博等消息的时候,图标上面会显示1,2,3条新信息等。)
@property(nonatomic) NSInteger applicationIconBadgeNumber;
代码实现和效果:
- (void)viewDidLoad{
[super viewDidLoad];
//创建并添加一个按钮
UIButton *btn=[[UIButton alloc]initWithFrame:CGRectMake(100, 100, 60, 30)];
[btn setTitle:@"按钮" forState:UIControlStateNormal];
[btn setBackgroundColor:[UIColor brownColor]];
[btn addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
-(void)onClick{
NSLog(@"按钮点击事件");
//错误,只能有一个唯一的UIApplication对象,不能再进行创建
// UIApplication *app=[[UIApplication alloc]init];
//通过sharedApplication获取该程序的UIApplication对象
UIApplication *app=[UIApplication sharedApplication];
app.applicationIconBadgeNumber=123;
}
(2)设置联网指示器的可见性
@property(nonatomic,getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;
代码和效果:
//设置指示器的联网动画
app.networkActivityIndicatorVisible=YES;
(3)管理状态栏
- 从iOS7开始,系统提供了2种管理状态栏的方式
通过UIViewController管理(每一个UIViewController都可以拥有自己不同的状态栏).
在iOS7中,默认情况下,状态栏都是由UIViewController管理的,UIViewController实现下列方法就可以轻松管理状态栏的可见性和样式
状态栏的样式
- (UIStatusBarStyle)preferredStatusBarStyle;
状态栏的可见性
-(BOOL)prefersStatusBarHidden;
//pragma mark-设置状态栏的样式
-(UIStatusBarStyle)preferredStatusBarStyle{
//设置为白色
//return UIStatusBarStyleLightContent;
//默认为黑色
return UIStatusBarStyleDefault;
}
//pragma mark-设置状态栏是否隐藏(否)
-(BOOL)prefersStatusBarHidden{
return NO;
}
-
通过UIApplication管理(一个应用程序的状态栏都由它统一管理)
如果想利用UIApplication来管理状态栏,首先得修改Info.plist的设置
//通过sharedApplication获取该程序的UIApplication对象
UIApplication *app=[UIApplication sharedApplication];
app.applicationIconBadgeNumber=123;
//设置指示器的联网动画
app.networkActivityIndicatorVisible=YES;
//设置状态栏的样式
//app.statusBarStyle=UIStatusBarStyleDefault;//默认(黑色)
//设置为白色+动画效果
[app setStatusBarStyle:UIStatusBarStyleLightContent animated:YES];
//设置状态栏是否隐藏
app.statusBarHidden=YES;
//设置状态栏是否隐藏+动画效果
[app setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
- 补充
既然两种都可以对状态栏进行管理,那么什么时候该用什么呢?
如果状态栏的样式只设置一次,那就用UIApplication来进行管理;
如果状态栏是否隐藏,样式不一样那就用控制器进行管理。
UIApplication来进行管理有额外的好处,可以提供动画
(4)openURL:方法
UIApplication有个功能十分强大的openURL:方法
-(BOOL)openURL:(NSURL*)url;
openURL:方法的部分功能有
打电话
UIApplication *app = [UIApplicationsharedApplication]; [app openURL:[NSURLURLWithString:@"tel://10086"]];
发短信 [app openURL:[NSURLURLWithString:@"sms://10086"]];
发邮件 [app openURL:[NSURLURLWithString:@"mailto://12345@qq.com"]];
打开一个网页资源 [app openURL:[NSURLURLWithString:@"http://ios.itcast.cn"]];
打开其他app程序 openURL方法,可以打开其他APP。
URL补充:
URL:统一资源定位符,用来唯一的表示一个资源。
URL格式:协议头://主机地址/资源路径
网络资源:http/ ftp等 表示百度上一张图片的地址 http://www.baidu.com/images/20140603/abc.png
本地资源:file:///users/apple/desktop/abc.png(主机地址省略)
二、UIApplication Delegate
1.简单说明
所有的移动操作系统都有个致命的缺点:app很容易受到打扰。比如一个来电或者锁屏会导致app进入后台甚至被终止。
还有很多其它类似的情况会导致app受到干扰,在app受到干扰时,会产生一些系统事件,这时UIApplication会通知它的delegate对象,让delegate代理来处理这些系统事件。
作用:当被打断的时候,通知代理进入到后台。
每次新建完项目,都有个带有“AppDelegate”字眼的类,它就是UIApplication的代理,NJAppDelegate默认已经遵守了UIApplicationDelegate协议,已经是UIApplication的代理。
2.代理方法
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
// 当应用程序启动完毕的时候就会调用(系统自动调用)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}
//当应用程序程序失去焦点的时候调用(系统自动调用)
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
//当程序进入后台的时候调用
//一般在这里保存应用程序的数据和状态
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
//将要进入前台的是时候调用
//一般在该方法中恢复应用程序的数据,以及状态
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
//应用程序获得焦点
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
// 应用程序即将被销毁的时候会调用该方法
// 注意:如果应用程序处于挂起状态的时候无法调用该方法
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@end
应用程序一般有五个状态:官方文档app.states
三、程序启动原理
UIApplicationMain
main函数中执行了一个UIApplicationMain这个函数
intUIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);
*argc、argv:直接传递给UIApplicationMain进行相关处理即可
*principalClassName:指定应用程序类名(app的象征),该类必须是UIApplication(或子类)。如果为nil,则用UIApplication类作为默认值
1、delegateClassName:指定应用程序的代理类,该类必须遵守UIApplicationDelegate协议
2、UIApplicationMain函数会根据principalClassName创建UIApplication对象,根据delegateClassName创建一个delegate对象,并将该delegate对象赋值给UIApplication对象中的delegate属性
接着会建立应用程序的Main Runloop(事件循环),进行事件的处理(首先会在程序完毕后调用delegate对象的application:didFinishLaunchingWithOptions:方法)
程序正常退出时UIApplicationMain函数才返回
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
/*
argc: 系统或者用户传入的参数个数
argv: 系统或者用户传入的实际参数
1.根据传入的第三个参数创建UIApplication对象
2.根据传入的第四个产生创建UIApplication对象的代理
3.设置刚刚创建出来的代理对象为UIApplication的代理
4.开启一个事件循环
*/
}
}
系统入口的代码和参数说明:
argc:系统或者用户传入的参数
argv:系统或用户传入的实际参数
1.根据传入的第三个参数,创建UIApplication对象
2.根据传入的第四个产生创建UIApplication对象的代理
3.设置刚刚创建出来的代理对象为UIApplication的代理
4.开启一个事件循环(可以理解为里面是一个死循环)这个时间循环是一个队列(先进先出)先添加进去的先处理
ios程序启动原理
四、程序启动的完整过程
1.main函数
2.UIApplicationMain
- 创建UIApplication对象
- 创建UIApplication的delegate对象
3.delegate对象开始处理(监听)系统事件(没有storyboard) - 程序启动完毕的时候, 就会调用代理的application:didFinishLaunchingWithOptions:方法
- 在application:didFinishLaunchingWithOptions:中创建UIWindow
- 创建和设置UIWindow的rootViewController
- 显示窗口
3.根据Info.plist获得最主要storyboard的文件名,加载最主要的storyboard(有storyboard)
- 创建UIWindow
- 创建和设置UIWindow的rootViewController
- 显示窗口
链接:UIApplication相关转载自//www.greatytc.com/p/16b65b9c22b0
五、具体在main函数之前还有很多细则
链接: https://blog.csdn.net/Hello_Hwc/article/details/78317863
问题:响应者链,以及不能响应的控件什么时候事件被抛弃
响应者对象:
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”。
我们熟悉的UIApplication、UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。
UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件。
1. 一根或者多根手指开始触摸屏幕
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
2.一根或者多根手指在屏幕上移动(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
3.一根或者多根手指离开屏幕
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
4.触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
针对上面方法的touches,类型是UITouch,点进去查看头文件
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UITouch : NSObject
//时间戳记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
//触摸事件在屏幕上有一个周期,即触摸开始、触摸点移动、触摸结束,还有中途取消。通过phase可以查看当前触摸事件在一个周期中所处的状态。
@property(nonatomic,readonly) UITouchPhase phase;
//点按次数(点1次算1,再点一下算2)
@property(nonatomic,readonly) NSUInteger tapCount;
@property(nonatomic,readonly) UITouchType type API_AVAILABLE(ios(9.0));
// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius API_AVAILABLE(ios(8.0));
@property(nonatomic,readonly) CGFloat majorRadiusTolerance API_AVAILABLE(ios(8.0));
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
//用户点击的视图
@property(nullable,nonatomic,readonly,strong) UIView *view;
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers API_AVAILABLE(ios(3.2));
//用户点击的位置
- (CGPoint)locationInView:(nullable UIView *)view;
//用户前一次点击的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// Use these methods to gain additional precision that may be available from touches.
// Do not use precise locations for hit testing. A touch may hit test inside a view, yet have a precise location that lies just outside.
- (CGPoint)preciseLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
// Force of the touch, where 1.0 represents the force of an average touch
@property(nonatomic,readonly) CGFloat force API_AVAILABLE(ios(9.0));
// Maximum possible force with this input mechanism
@property(nonatomic,readonly) CGFloat maximumPossibleForce API_AVAILABLE(ios(9.0));
// Azimuth angle. Valid only for stylus touch types. Zero radians points along the positive X axis.
// Passing a nil for the view parameter will return the azimuth relative to the touch's window.
- (CGFloat)azimuthAngleInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
// A unit vector that points in the direction of the azimuth angle. Valid only for stylus touch types.
// Passing nil for the view parameter will return a unit vector relative to the touch's window.
- (CGVector)azimuthUnitVectorInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
// Altitude angle. Valid only for stylus touch types.
// Zero radians indicates that the stylus is parallel to the screen surface,
// while M_PI/2 radians indicates that it is normal to the screen surface.
@property(nonatomic,readonly) CGFloat altitudeAngle API_AVAILABLE(ios(9.1));
// An index which allows you to correlate updates with the original touch.
// Is only guaranteed non-nil if this UITouch expects or is an update.
@property(nonatomic,readonly) NSNumber * _Nullable estimationUpdateIndex API_AVAILABLE(ios(9.1));
// A set of properties that has estimated values
// Only denoting properties that are currently estimated
@property(nonatomic,readonly) UITouchProperties estimatedProperties API_AVAILABLE(ios(9.1));
// A set of properties that expect to have incoming updates in the future.
// If no updates are expected for an estimated property the current value is our final estimate.
// This happens e.g. for azimuth/altitude values when entering from the edges
@property(nonatomic,readonly) UITouchProperties estimatedPropertiesExpectingUpdates API_AVAILABLE(ios(9.1));
@end
下面是针对touch的四个方法的演练,效果就是myView会跟着手指做一些事情
//
// CPViewController.m
#import "CPViewController.h"
@interface CPViewController ()
@property (nonatomic, strong) UIImageView *myView;
@end
@implementation CPViewController
// 集合演练
- (void)demoSet
{
// NSSet : 集合,同样是保存一组数据,不过集合中的对象“没有顺序”
// 要访问NSSet中的对象,使用anyObject
// 集合的用处:例如可重用单元格,在缓冲区找一个就拿出来了
// NSArray : 存储有序的对象,对象的顺序是按照添加的先后次序来决定,通过下标来访问数组中的对象
NSSet *set = [NSSet setWithObjects:@1, @2, @3, @4, nil];
NSLog(@"%@", set.anyObject);
}
- (UIView *)myView
{
if (!_myView) {
_myView = [[UIImageView alloc] initWithFrame:CGRectMake(110, 100, 100, 100)];
_myView.image = [UIImage imageNamed:@"hero_fly_1"];
[self.view addSubview:_myView];
}
return _myView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self myView];
}
// 1. 手指按下
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 从集合中取出UITouch对象
UITouch *touch = touches.anyObject;
//打开这句,然后屏蔽touchesMoved里的代码,可实现myView跟着手指跑
//[self moveView1:touch];
NSLog(@"%d", touch.tapCount);
NSLog(@"%s", __func__);
}
// 2. 手指移动
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
// 随着手指移动,移动红色的视图
// 1. 取出触摸对象
UITouch *touch = touches.anyObject;
// 2. 手指当前的位置
CGPoint location = [touch locationInView:self.view];
// 3. 手指之前的位置
CGPoint pLocation = [touch previousLocationInView:self.view];
// 4. 计算两点之间的偏移
CGPoint offset = CGPointMake(location.x - pLocation.x, location.y - pLocation.y);
// 5. 设置视图位置
// self.myView.center = CGPointMake(self.myView.center.x + offset.x, self.myView.center.y + offset.y);
// 6. 使用transform设置位置,提示,在调整对象位置时,最好使用transform
self.myView.transform = CGAffineTransformTranslate(self.myView.transform, offset.x, 0);
}
- (void)moveView1:(UITouch *)touch
{
// 随着手指移动,移动红色的视图
// 1. 取出触摸对象
CGPoint location = [touch locationInView:self.view];
// 2. 设置红色视图的位置,在第一次移动的时候,会产生跳跃
self.myView.center = location;
}
// 3. 手指抬起
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
}
// 4. 触摸被取消(中断),例如打电话被中断
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
}
@end
多点触摸
//
// CPViewController.m
// 02-多点触摸
#import "CPViewController.h"
@interface CPViewController ()
/** 图片数组 */
@property (nonatomic, strong) NSArray *images;
@end
@implementation CPViewController
- (NSArray *)images
{
if (!_images) {
_images = @[[UIImage imageNamed:@"spark_blue"], [UIImage imageNamed:@"spark_red"]];
}
return _images;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// 支持多点
self.view.multipleTouchEnabled = YES;
}
// 手指按下
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// // 遍历集合中的所有触摸
// int i = 0;
// for (UITouch *touch in touches) {
// // 取出触摸点的位置
// CGPoint location = [touch locationInView:self.view];
//
// // 在触摸点的位置添加图片
// UIImageView *imageView = [[UIImageView alloc] initWithImage:self.images[i]];
//
// imageView.center = location;
//
// [self.view addSubview:imageView];
//
// i++;
// }
}
// 手指移动
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// 遍历集合中的所有触摸
int i = 0;
for (UITouch *touch in touches) {
// 取出触摸点的位置
CGPoint location = [touch locationInView:self.view];
// 在触摸点的位置添加图片
UIImageView *imageView = [[UIImageView alloc] initWithImage:self.images[i]];
imageView.center = location;
[self.view addSubview:imageView];
i++;
// 要将这些图像视图删除!延迟一段时间
[UIView animateWithDuration:2.0f animations:^{
imageView.alpha = 0.3;
} completion:^(BOOL finished) {
// 从界面上删除
[imageView removeFromSuperview];
}];
}
}
// 手指抬起
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"%d", self.view.subviews.count);
}
@end
UIView不接受触摸事件的四种情况
1.当前视图或父视图不接收用户交互:
userInteractionEnabled = NO
提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的
2.隐藏:
hidden = YES
3.透明:
alpha = 0.0 ~ 0.01
4.当前视图虽添加在父视图上,但是位置偏移出父视图即子视图的位置超出了父视图的有效位置
eg:黄色view添加在绿色view上,但是偏移出view范围,虽然黄色view可以展示,但是点击黄色view时,是后面白色的大view去响应了点击。
此处提示:如果设置了绿色view..clipsToBounds = YES;这句代码,含义就是裁剪超出绿色view的范围,那么黄色view就不会显示了。
响应者链条
响应者链条,是通过递归构成的一组UIResponder对象的链式序列!
上图简述:
1.运行循环监听到屏幕被点击时,首先会通知UIApplication去找找谁被点击了,然后UIApplication会去通知UIWindow去找找谁被点击了,然后UIWindow会去通知控制器去找找谁被点击了,然后控制器会去通知view去找找谁被点击了,然后view会去通知内部的button是不是它被点击了,button说是我被点击了,然后告诉了view,然后view告诉控制器是button被点击了,然后控制器告诉UIWindow是button被点击了,然后UIWindow告诉UIApplication是button被点击了,然后UIApplication告诉运行循环是button被点击了,之后运行循环会通知控制器执行点击方法。
2.如果查找时,控制器找到view,view找到button,button说我没有被点击,那么会告诉view,然后view告诉控制器button没有被点击,然后控制器告诉UIWindow这个button没有被点击,然后UIWindow告诉UIApplication这个button没有被点击,然后UIApplication告诉运行循环这个button没有被点击,然后这个事件此次会被抛弃。
hit-test
//1> 系统会自动递归调用hitTest方法来判断哪一个视图来响应点击事件
// 2> hitTest方法是系统"底层专门"用来"递归遍历"哪一个视图应该对点击做出响应的方法!
// 3> point参数是当前视图的坐标点,专门用来判断用户触摸点是否在视图的"有效范围"内!
// 4> 每一个控件都有hitTesthitTest方法,需要时重写即可。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [super hitTest:point withEvent:event];
}
首先看如上图层级关系,在控制器里实现touchesBegan方法时,拿到的touch.view就是当前点击的视图
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = touches.anyObject;
if (touch.view == self.redView) {
NSLog(@"red view");
} else if (touch.view == self.blueView) {
NSLog(@"blue view");
}
}
下面是红色view里的代码
#import "CPRedView.h"
#import "CPBlueView.h"
@implementation CPRedView
// 如果用storyboard,此方法不会被调用
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSLog(@"%s", __func__);
}
return self;
}
- (void)awakeFromNib
{
NSLog(@"%s", __func__);
self.backgroundColor = [UIColor redColor];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%s %@", __func__, NSStringFromCGPoint(point));
return [super hitTest:point withEvent:event];
// 如果写 return self,此时强行拦截所有的点击测试!
// return self;
}
@end
下面是蓝色view代码
#import "CPBlueView.h"
@implementation CPBlueView
- (void)awakeFromNib
{
self.backgroundColor = [UIColor blueColor];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%s %@", __func__, NSStringFromCGPoint(point));
return [super hitTest:point withEvent:event];
}
@end
总结
通过打印可以发现如果点击白色view,系统会轮循调用到红色view里的hitTest,然后touchesBegan里拿到的view是白色view;如果点击红色view,系统会轮循调用到蓝色view里的hitTest,然后touchesBegan里拿到的view是红色view。所以通过响应者链条,可以知道是找到响应者后,最多再查下子控件,之后不在深入去轮循。如上面代码,如果我们把红色view里把hitTest的返回写成 return self,那么,我们点击白色view时,会轮循到红色view里的hitTest,然后touchesBegan里拿到的view就变成了红色view了,因为红色view的hitTest强制返回self。如果我们点击红色view,touchesBegan里拿到的view是红色view,同时不再去蓝色view里调用hitTest;如果我们点击蓝色的view,那么touchesBegan返回的依旧是红色view,同时调用到红色view的hitTest就会停止调用蓝色view的hitTest。
如何让button点击范围变大,让view点击范围变大?
点击view头文件里看以查找到如下方法
//返回视图层级中能响应触控点的最深视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
//返回视图是否包含指定的某个点
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
hitTest已经知道了是系统轮循遍历响应者的。那么pointInside则可以达到改变点击范围。至于如何做到,且看下面。
最土的一个办法自然就是直接在button上盖一个大点的view添加响应事件,还有其他办法更简单实现。
第一种
继承于UIButton,然后重写方法 -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
如下实践,继承于UIButton,实现如下效果:
#import <UIKit/UIKit.h>
@interface MyBigButton : UIButton
@end
#import "MyBigButton.h"
@implementation MyBigButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event{
CGRect bounds = self.bounds;
//若原热区小于44x44,则放大热区,否则保持原大小不变
CGFloat widthDelta = MAX(44.0 - bounds.size.width, 0);
CGFloat heightDelta = MAX(44.0 - bounds.size.height, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
@end
/*
扩展
//该结构体的应用是以原rect为中心,再参考dx,dy,进行缩放或者放大。
CGRectInset CGRect CGRectInset (
CGRect rect,
CGFloat dx,
CGFloat dy
);
函数CGRectContainsPoint(CGRect rect, CGPoint point)是用于判断,参数2point是否包含在参数1rect中
*/
第二种
给UIButton新增分类
[button setHitTestEdgeInsets:UIEdgeInsetsMake(-50, - 50, -50, - 50)];
#import <UIKit/UIKit.h>
@interface UIButton (BigFream)
@property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
@end
#import "UIButton+BigFream.h"
#import <objc/runtime.h>
@implementation UIButton (BigFream)
@dynamic hitTestEdgeInsets;
static const NSString *KEY_HIT_TEST_EDGE_INSETS = @"HitTestEdgeInsets";
-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
NSValue *value = [NSValue value:&hitTestEdgeInsets withObjCType:@encode(UIEdgeInsets)];
objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(UIEdgeInsets)hitTestEdgeInsets {
NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS);
if(value) {
UIEdgeInsets edgeInsets;
[value getValue:&edgeInsets];
return edgeInsets;
}else {
return UIEdgeInsetsZero;
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
//如果 button 边界值无变化 失效 隐藏 或者透明 直接返回。
if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden || self.alpha == 0 ) {
return [super pointInside:point withEvent:event];
}
CGRect relativeFrame = self.bounds;
//UIEdgeInsetsInsetRect表示在原来的rect基础上根据边缘距离切一个rect出来
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
什么是离屏渲染等,为什么会触发
屏幕显示图像的原理
首先从过去的 CRT 显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。
垂直同步信号(VSync)
|--------------------> 水平同步信号(HSync)
|--------------------> 水平同步信号(HSync)
|--------------------> 水平同步信号(HSync)
|--------------------> 水平同步信号(HSync)
|--------------------> 水平同步信号(HSync)
|--------------------> 水平同步信号(HSync)
|--------------------> 水平同步信号(HSync)
\|/
CPU和GPU
在屏幕成像的过程中,CPU和GPU起着至关重要的作用
CPU(Central Processing Unit,中央处理器)
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
GPU(Graphics Processing Unit,图形处理器)
纹理的渲染
计算 渲染 读取 展示
CPU ----> GPU ---> 帧缓存 ---> 视频控制器 --->屏幕
(渲染后放到缓存区) (由视频控制器从缓存区读取展示)
通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。
双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:
为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。
那么目前主流的移动设备是什么情况呢?从网上查到的资料可以知道,iOS 设备会始终使用双缓存,并开启垂直同步。而安卓设备直到 4.1 版本,Google 才开始引入这种机制,目前安卓系统是三缓存+垂直同步。
卡顿产生的原因和解决方案
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。
CPU 资源消耗原因和解决方案
对象创建
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择。
尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。
对象调整
对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。
当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
对象销毁
对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
布局计算
视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。
不论通过何种技术对视图进行布局,其最终都会落到对 UIView.frame/bounds/center 等属性的调整上。上面也说过,对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。
Autolayout
Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。具体数据可以看这个文章:http://pilky.me/36/。 如果你不想手动调整 frame 等属性,你可以用一些工具方法替代(比如常见的 left/right/top/bottom/width/height 快捷属性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
文本计算
如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。
如果你用 CoreText 绘制文本,那就可以先生成 CoreText 排版对象,然后自己计算了,并且 CoreText 对象还能保留以供稍后绘制使用。
文本渲染
屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。
图片的解码
当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。
图像的绘制
图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致):
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
GPU 资源消耗原因和解决方案
相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。
纹理的渲染
所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。
当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。目前来说,iPhone 4S 以上机型,纹理尺寸上限都是 4096×4096,更详细的资料可以看这里:iosres.com。所以,尽量不要让图片和视图的大小超过这个值。
视图的混合 (Composing)
当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。
图形的生成
CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
卡顿检测
可以先用简易控件 YYFPSLabel看一下 CPU 消耗,发现帧数在出现明显卡顿时仍保持在55-60之间,可排除 CPU。
用 Instuments 的 GPU Driver 预设,能够实时查看到 CPU 和 GPU 的资源消耗。在这个预设内,你能查看到几乎所有与显示有关的数据,比如 Texture 数量、CA 提交的频率、GPU 消耗等,在定位界面卡顿的问题时,这是最好的工具。
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
离屏渲染
油画算法
计算机图层的叠加绘制大概遵循油画算法
,在这种算法下会按层绘制,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分,如下图。
这样就不会导致远的物体挡住近的物体,但是有个局限,就是无法在后面一层渲染完成后,再回去修改前面图层,因为前面的图层已经被覆盖了
离屏渲染
对于有前后依赖的图层(如全局剪切,阴影等),油画算法无法满足我们的需求,对于有前后依赖的图层,我们可以再另开辟一个空间,用于临时渲染,渲染完成后再渲染到当前的缓冲区上,这个临时渲染,就是离屏渲染,由于需要开辟一个新的内存空间,并且共享同一个上下文,所以还需要做上下文切换(状态切换),并且渲染完成后还要进行拷贝操作。
1.开辟临时缓存空间
2.上下文切换,上下文对象比较大,切换操作会带来一定的性能消耗
3.内存拷贝
4.额外的渲染
上面4项带来的开销会很大,并且每一帧渲染都需要执行,如果屏幕上触发离屏渲染的操作过多,会导致GPU渲染时间过长造成卡顿,应该避免触发离屏渲染。
离屏渲染检测
模拟器的工具栏选择debug -> 选取 color Offscreen-Rendered.
开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题
哪些操作会触发离屏渲染?
圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0
光栅化,layer.shouldRasterize = YES (光栅化含义:将图转化为一个个栅格组成的图象。 光栅化特点:每个元素对应帧缓冲区中的一像素。)
遮罩,layer.mask
阴影,layer.shadowXXX
如果设置了layer.shadowPath就不会产生离屏渲染
圆角触发离屏条件
1.设置layer.masksToBounds = YES
2.设置layer.cornerRadius大于0
3.同时设置上面俩条件后并且用于渲染的图层大于1,就会触发离屏渲染。(背景颜色相当于一个单独一个图层)
3.1> imageView单图层,同时设置背景和圆角可触发。
3.2> button多图层,设置圆角可触发。
3.3> 无绘制或无子控件的view是单图层,添加子控件并设置圆角可触发
3.4> label单图层,设置圆角背景还不能触发。
优化圆角触发离屏
1.避免使用裁切(masksToBounds)操作,如果我们能确保View里面的内容不会溢出,就可以不用masksToBounds
2.即使要用到裁切的操作,尽量放到子view里面,不要在上层view使用masksToBounds,因为裁切需要对所有的layer和subview所有图层都进行裁切,这样离屏渲染会需要更大的空间,裁切更多的图层,应该只对必要的view/layer进行裁切
3.提前切好需要的圆角,避免渲染的时候再切
4.使用1. 使用YYWebImage去处理
问题: 展开讲下利用RunLoop检测卡顿
source0 处理的是 app 内部事件,包括 UI 事件,每次处理的开始和结束的耗时决定了当前页面刷新是否正常,即 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 之间。因此创建一个子线程去监听主线程状态变化,通过dispatch_semaphore 在主线程进入上面两个状态时发送信号量,子线程设置超时时间循环等待信号量,若超过时间后还未接收到主线程发出的信号量则可判断为卡顿,此时可以保存当前调用栈信息作为后续分析依据,线上卡顿监控多采用这种方式
#pragma mark - 注册RunLoop观察者
//在主线程注册RunLoop观察者
- (void)registerMainRunLoopObserver
{
//监听每个步凑的回调
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
self.runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopObserver, kCFRunLoopCommonModes);
}
//观察者方法
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
self.runLoopActivity = activity;
//触发信号,说明开始执行下一个步骤。
if (self.semaphore != nil)
{
dispatch_semaphore_signal(self.semaphore);
}
}
#pragma mark - RunLoop状态监测
//创建一个子线程去监听主线程RunLoop状态
- (void)createRunLoopStatusMonitor
{
//创建信号
self.semaphore = dispatch_semaphore_create(0);
if (self.semaphore == nil)
{
return;
}
//创建一个子线程,监测Runloop状态时长
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
while (YES)
{
//如果观察者已经移除,则停止进行状态监测
if (self.runLoopObserver == nil)
{
self.runLoopActivity = 0;
self.semaphore = nil;
return;
}
//信号量等待。状态不等于0,说明状态等待超时
//方案一->设置单次超时时间为500毫秒
long status = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC));
if (status != 0)
{
if (self.runLoopActivity == kCFRunLoopBeforeSources || self.runLoopActivity == kCFRunLoopAfterWaiting)
{
...
//发生超过500毫秒的卡顿,此时去记录调用栈信息
}
}
/*
//方案二->连续5次卡顿50ms上报
long status = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (status != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
//保存调用栈信息
}
}
timeoutCount = 0;
*/
}
});
}
问题:控制器的生命周期
1.alloc:创建对象,分配空间
//是当从nib文件中加载对象的时候会调用
2.initWithCoder:(NSCoder *)aDecoder(如果使用storyboard或者xib)
3.init (initWithNibName):初始化对象,初始化数据
4.awakeFromNib:
5.loadView:
6.viewDidLoad:
7.viewWillAppear:
8.viewWillLayoutSubviews:控制器的view将要布局子控件
9.viewDidLayoutSubviews:控制器的view布局子控件完成这期间系统可能会多次调用viewWillLayoutSubviews 、viewDidLayoutSubviews 俩个方法
10.viewDidAppear:
11.viewWillDisappear:控制器的view即将消失的时候这期间系统也会调用viewWillLayoutSubviews 、viewDidLayoutSubviews 两个方法
12.viewDidDisappear:
13.dealloc:控制器销毁
- didReceiveMemoryWarning:内存警告
问题:UIButton 的父类是什么?UILabel 的父类又是什么?区别
UIButton -> UIControl -> UIView -> UIResponder
UILabel -> UIView -> UIResponder
//UIButton 继承 UIContorl 中的方法和属性说明:
// 1,设置按钮状态
//按钮启用或禁用
@property(nonatomic,getter=isEnabled) BOOL enabled;
//按钮选中或不选中
@property(nonatomic,getter=isSelected) BOOL selected;
//按钮高亮或不高亮
@property(nonatomic,getter=isHighlighted) BOOL highlighted;
//2,设置按钮文字的对齐方式
//纵向居中对齐方式
@property(nonatomic) UIControlContentVerticalAlignment contentVerticalAlignment;
//横向居中对齐方式
@property(nonatomic) UIControlContentHorizontalAlignment contentHorizontalAlignment;
//3,设置或取消监听事件
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIControlEvents)controlEvents;
问题:Xib布局scrollview
代码布局scrollview需要设置contenSize大小。
xib布局时没法直接设置contenSize所以需要想其他方法。
思路
1.添加ScrollView
2.给ScrollView设置上、下、左、右的约束
3.给ScrollView添加一个ContainView(就是普通view),设置它的上下左右约束
4.给ContainView添加子View,用以将父View撑开,从而可以滑动。
总结:
scrollView的frame通过与父视图的约束进行确定
scrollView的contentSize的高度宽度通过ContainView来确定
问题:比如一页有多行cell,能全部加载。说下数据源和代理调用顺序
iOS7之后新增了一个属性estimatedRowHeight(预估高度)
@property (nonatomic) CGFloat estimatedRowHeight API_AVAILABLE(ios(7.0)); // default is UITableViewAutomaticDimension, set to 0 to disable
苹果对它的描述是
If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
如果表格包含高度可变的行,则在表格加载时计算其所有高度可能会很昂贵。 使用估计可以使几何计算的某些成本从加载时间推迟到滚动时间。
我们知道tableView是继承于ScrollView的,一个scrollView能滑动,需要设置contentSize,那么tableView的contentSize怎么来呢?iOS11之前,会调用tableView每一个cell的heightForRowAtIndexPath来算出整个高度,从而相加得出contentSize来,这一个步骤挺耗性能!但是iOS11之后默认打开了estimatedRowHeight估算高度功能,当tableView创建完成后,contentSize为estimatedRowHeight(默认值为44)*cell的数量,不需要遍历每一个cell的heightForRowAtIndexPath来计算了,在滑动的时候,来准确计算这个值。
所以数据源和代理的调用顺序分为11之前和之后
iOS7~iOS10数据源和代理调用顺序
1.先调用numberOfRowsInSection
2.有多少row调用多次heightForRowAtIndexPath
以上两步可以计算出需要展示的contentSize高度
3.每调用一次cellForRowAtIndexPath之后调用一次heightForRowAtIndexPath去具体布局单个cell
iOS11之后
1.先调用numberOfRowsInSection
2.每调用一次cellForRowAtIndexPath之后调用一次heightForRowAtIndexPath去具体布局单个cell
但是如果iOS11之后设置了self.tableV.estimatedRowHeight = 0;(也就是说关闭了预估高度),调用顺序跟iOS11之前的调用一致。
问题:布局框架
MyLayout
MyLayout是一套iOS界面视图布局框架。
MyLayout的实现内核是基于frame的设置,而不是对AutoLayout的封装。因此在使用上不会受到任何操作系统版本的限制。
Masonry
基于NSLayoutConstraint封装
什么是NSLayoutConstraint?
在xib中,我们可以用拖拽约束的方式来给空间添加约束条件,但是如果控件过多,则整个xib文件中的线条会变得混乱不堪,虽然苹果在极力推荐可视化的加约束方式,但是还是给我们提供了代码的方式来添加约束:NSLayoutConstraint。
SDAutoLayout
问题:左右label布局,左边文字长时尽量大时,压缩右边,怎么实现?
设置右边label权重小于左边
//www.greatytc.com/p/a4b8e0c8e68d