問題: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本身的坐標系統(tǒng),默認永遠都是0,0點,除非調(diào)用了setbounds函數(shù)),而frame的原點卻是任意的(相對于父視圖中的坐標位置)。
frame: 該view在父view坐標系統(tǒng)中的位置和大小。(參照點是,父親的坐標系統(tǒng))
bounds:該view在本地坐標系統(tǒng)中的位置和大小。(參照點是,本地坐標系統(tǒng),就相當于ViewB自己的坐標系統(tǒng),以0,0點為起點)
center:該view的中心點在父view坐標系統(tǒng)中的位置和大小。
其實本地坐標系統(tǒng)的關(guān)鍵就是要知道的它的原點(0,0)在什么位置(這個位置又是相對于上層的view的本地坐標系統(tǒng)而言的,最上層view就是 window它的本地坐標系統(tǒng)原點就是屏幕的左上角了)。
通過修改view的bounds屬性可以修改本地坐標系統(tǒng)的原點位置。
所以,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相關(guān)
一、UIApplication
1.簡單介紹
(1)UIApplication對象是應(yīng)用程序的象征,一個UIApplication對象就代表一個應(yīng)用程序。
(2)每一個應(yīng)用都有自己的UIApplication對象,而且是單例的,如果試圖在程序中新建一個UIApplication對象,那么將報錯提示。
(3)通過[UIApplication sharedApplication]可以獲得這個單例對象
(4) 一個iOS程序啟動后創(chuàng)建的第一個對象就是UIApplication對象,且只有一個(通過代碼獲取兩個UIApplication對象,打印地址可以看出地址是相同的)。
(5)利用UIApplication對象,能進行一些應(yīng)用級別的操作
2.應(yīng)用級別的操作示例:
(1)設(shè)置應(yīng)用程序圖標右上角的紅色提醒數(shù)字(如QQ,微博等消息的時候,圖標上面會顯示1,2,3條新信息等。)
@property(nonatomic) NSInteger applicationIconBadgeNumber;
代碼實現(xiàn)和效果:
- (void)viewDidLoad{
[super viewDidLoad];
//創(chuàng)建并添加一個按鈕
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對象,不能再進行創(chuàng)建
// UIApplication *app=[[UIApplication alloc]init];
//通過sharedApplication獲取該程序的UIApplication對象
UIApplication *app=[UIApplication sharedApplication];
app.applicationIconBadgeNumber=123;
}

(2)設(shè)置聯(lián)網(wǎng)指示器的可見性
@property(nonatomic,getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;
代碼和效果:
//設(shè)置指示器的聯(lián)網(wǎng)動畫
app.networkActivityIndicatorVisible=YES;

(3)管理狀態(tài)欄
- 從iOS7開始,系統(tǒng)提供了2種管理狀態(tài)欄的方式
通過UIViewController管理(每一個UIViewController都可以擁有自己不同的狀態(tài)欄).
在iOS7中,默認情況下,狀態(tài)欄都是由UIViewController管理的,UIViewController實現(xiàn)下列方法就可以輕松管理狀態(tài)欄的可見性和樣式
狀態(tài)欄的樣式
- (UIStatusBarStyle)preferredStatusBarStyle;
狀態(tài)欄的可見性
-(BOOL)prefersStatusBarHidden;
//pragma mark-設(shè)置狀態(tài)欄的樣式
-(UIStatusBarStyle)preferredStatusBarStyle{
//設(shè)置為白色
//return UIStatusBarStyleLightContent;
//默認為黑色
return UIStatusBarStyleDefault;
}
//pragma mark-設(shè)置狀態(tài)欄是否隱藏(否)
-(BOOL)prefersStatusBarHidden{
return NO;
}
-
通過UIApplication管理(一個應(yīng)用程序的狀態(tài)欄都由它統(tǒng)一管理)
如果想利用UIApplication來管理狀態(tài)欄,首先得修改Info.plist的設(shè)置image
//通過sharedApplication獲取該程序的UIApplication對象
UIApplication *app=[UIApplication sharedApplication];
app.applicationIconBadgeNumber=123;
//設(shè)置指示器的聯(lián)網(wǎng)動畫
app.networkActivityIndicatorVisible=YES;
//設(shè)置狀態(tài)欄的樣式
//app.statusBarStyle=UIStatusBarStyleDefault;//默認(黑色)
//設(shè)置為白色+動畫效果
[app setStatusBarStyle:UIStatusBarStyleLightContent animated:YES];
//設(shè)置狀態(tài)欄是否隱藏
app.statusBarHidden=YES;
//設(shè)置狀態(tài)欄是否隱藏+動畫效果
[app setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
- 補充
既然兩種都可以對狀態(tài)欄進行管理,那么什么時候該用什么呢?
如果狀態(tài)欄的樣式只設(shè)置一次,那就用UIApplication來進行管理;
如果狀態(tài)欄是否隱藏,樣式不一樣那就用控制器進行管理。
UIApplication來進行管理有額外的好處,可以提供動畫
(4)openURL:方法
UIApplication有個功能十分強大的openURL:方法
-(BOOL)openURL:(NSURL*)url;
openURL:方法的部分功能有
打電話
UIApplication *app = [UIApplicationsharedApplication]; [app openURL:[NSURLURLWithString:@"tel://10086"]];
發(fā)短信 [app openURL:[NSURLURLWithString:@"sms://10086"]];
發(fā)郵件 [app openURL:[NSURLURLWithString:@"mailto://12345@qq.com"]];
打開一個網(wǎng)頁資源 [app openURL:[NSURLURLWithString:@"http://ios.itcast.cn"]];
打開其他app程序 openURL方法,可以打開其他APP。
URL補充:
URL:統(tǒng)一資源定位符,用來唯一的表示一個資源。
URL格式:協(xié)議頭://主機地址/資源路徑
網(wǎng)絡(luò)資源:http/ ftp等 表示百度上一張圖片的地址 http://www.baidu.com/images/20140603/abc.png
本地資源:file:///users/apple/desktop/abc.png(主機地址省略)
二、UIApplication Delegate
1.簡單說明
所有的移動操作系統(tǒng)都有個致命的缺點:app很容易受到打擾。比如一個來電或者鎖屏會導致app進入后臺甚至被終止。
還有很多其它類似的情況會導致app受到干擾,在app受到干擾時,會產(chǎn)生一些系統(tǒng)事件,這時UIApplication會通知它的delegate對象,讓delegate代理來處理這些系統(tǒng)事件。
作用:當被打斷的時候,通知代理進入到后臺。

每次新建完項目,都有個帶有“AppDelegate”字眼的類,它就是UIApplication的代理,NJAppDelegate默認已經(jīng)遵守了UIApplicationDelegate協(xié)議,已經(jīng)是UIApplication的代理。

2.代理方法
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
// 當應(yīng)用程序啟動完畢的時候就會調(diào)用(系統(tǒng)自動調(diào)用)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}
//當應(yīng)用程序程序失去焦點的時候調(diào)用(系統(tǒng)自動調(diào)用)
- (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.
}
//當程序進入后臺的時候調(diào)用
//一般在這里保存應(yīng)用程序的數(shù)據(jù)和狀態(tài)
- (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.
}
//將要進入前臺的是時候調(diào)用
//一般在該方法中恢復應(yīng)用程序的數(shù)據(jù),以及狀態(tài)
- (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.
}
//應(yīng)用程序獲得焦點
- (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.
}
// 應(yīng)用程序即將被銷毀的時候會調(diào)用該方法
// 注意:如果應(yīng)用程序處于掛起狀態(tài)的時候無法調(diào)用該方法
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@end
應(yīng)用程序一般有五個狀態(tài):官方文檔app.states
三、程序啟動原理
UIApplicationMain
main函數(shù)中執(zhí)行了一個UIApplicationMain這個函數(shù)
intUIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);
*argc、argv:直接傳遞給UIApplicationMain進行相關(guān)處理即可
*principalClassName:指定應(yīng)用程序類名(app的象征),該類必須是UIApplication(或子類)。如果為nil,則用UIApplication類作為默認值
1、delegateClassName:指定應(yīng)用程序的代理類,該類必須遵守UIApplicationDelegate協(xié)議
2、UIApplicationMain函數(shù)會根據(jù)principalClassName創(chuàng)建UIApplication對象,根據(jù)delegateClassName創(chuàng)建一個delegate對象,并將該delegate對象賦值給UIApplication對象中的delegate屬性
接著會建立應(yīng)用程序的Main Runloop(事件循環(huán)),進行事件的處理(首先會在程序完畢后調(diào)用delegate對象的application:didFinishLaunchingWithOptions:方法)
程序正常退出時UIApplicationMain函數(shù)才返回
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
/*
argc: 系統(tǒng)或者用戶傳入的參數(shù)個數(shù)
argv: 系統(tǒng)或者用戶傳入的實際參數(shù)
1.根據(jù)傳入的第三個參數(shù)創(chuàng)建UIApplication對象
2.根據(jù)傳入的第四個產(chǎn)生創(chuàng)建UIApplication對象的代理
3.設(shè)置剛剛創(chuàng)建出來的代理對象為UIApplication的代理
4.開啟一個事件循環(huán)
*/
}
}
系統(tǒng)入口的代碼和參數(shù)說明:
argc:系統(tǒng)或者用戶傳入的參數(shù)
argv:系統(tǒng)或用戶傳入的實際參數(shù)
1.根據(jù)傳入的第三個參數(shù),創(chuàng)建UIApplication對象
2.根據(jù)傳入的第四個產(chǎn)生創(chuàng)建UIApplication對象的代理
3.設(shè)置剛剛創(chuàng)建出來的代理對象為UIApplication的代理
4.開啟一個事件循環(huán)(可以理解為里面是一個死循環(huán))這個時間循環(huán)是一個隊列(先進先出)先添加進去的先處理
ios程序啟動原理

四、程序啟動的完整過程
1.main函數(shù)
2.UIApplicationMain
- 創(chuàng)建UIApplication對象
- 創(chuàng)建UIApplication的delegate對象
3.delegate對象開始處理(監(jiān)聽)系統(tǒng)事件(沒有storyboard) - 程序啟動完畢的時候, 就會調(diào)用代理的application:didFinishLaunchingWithOptions:方法
- 在application:didFinishLaunchingWithOptions:中創(chuàng)建UIWindow
- 創(chuàng)建和設(shè)置UIWindow的rootViewController
- 顯示窗口
3.根據(jù)Info.plist獲得最主要storyboard的文件名,加載最主要的storyboard(有storyboard)
- 創(chuàng)建UIWindow
- 創(chuàng)建和設(shè)置UIWindow的rootViewController
- 顯示窗口
鏈接:UIApplication相關(guān)轉(zhuǎn)載自http://www.itdecent.cn/p/16b65b9c22b0
五、具體在main函數(shù)之前還有很多細則
鏈接: https://blog.csdn.net/Hello_Hwc/article/details/78317863
問題:響應(yīng)者鏈,以及不能響應(yīng)的控件什么時候事件被拋棄
響應(yīng)者對象:
在iOS中不是任何對象都能處理事件,只有繼承了UIResponder的對象才能接收并處理事件。我們稱之為“響應(yīng)者對象”。
我們熟悉的UIApplication、UIViewController、UIWindow和所有繼承自UIView的UIKit類都直接或間接的繼承自UIResponder,因此它們都是響應(yīng)者對象,都能夠接收并處理事件。
UIView是UIResponder的子類,可以覆蓋下列4個方法處理不同的觸摸事件。
1. 一根或者多根手指開始觸摸屏幕
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
2.一根或者多根手指在屏幕上移動(隨著手指的移動,會持續(xù)調(diào)用該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
3.一根或者多根手指離開屏幕
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
4.觸摸結(jié)束前,某個系統(tǒng)事件(例如電話呼入)會打斷觸摸過程
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
針對上面方法的touches,類型是UITouch,點進去查看頭文件
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UITouch : NSObject
//時間戳記錄了觸摸事件產(chǎn)生或變化時的時間,單位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
//觸摸事件在屏幕上有一個周期,即觸摸開始、觸摸點移動、觸摸結(jié)束,還有中途取消。通過phase可以查看當前觸摸事件在一個周期中所處的狀態(tài)。
@property(nonatomic,readonly) UITouchPhase phase;
//點按次數(shù)(點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 : 集合,同樣是保存一組數(shù)據(jù),不過集合中的對象“沒有順序”
// 要訪問NSSet中的對象,使用anyObject
// 集合的用處:例如可重用單元格,在緩沖區(qū)找一個就拿出來了
// NSArray : 存儲有序的對象,對象的順序是按照添加的先后次序來決定,通過下標來訪問數(shù)組中的對象
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里的代碼,可實現(xiàn)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. 設(shè)置視圖位置
// self.myView.center = CGPointMake(self.myView.center.x + offset.x, self.myView.center.y + offset.y);
// 6. 使用transform設(shè)置位置,提示,在調(diào)整對象位置時,最好使用transform
self.myView.transform = CGAffineTransformTranslate(self.myView.transform, offset.x, 0);
}
- (void)moveView1:(UITouch *)touch
{
// 隨著手指移動,移動紅色的視圖
// 1. 取出觸摸對象
CGPoint location = [touch locationInView:self.view];
// 2. 設(shè)置紅色視圖的位置,在第一次移動的時候,會產(chǎn)生跳躍
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 ()
/** 圖片數(shù)組 */
@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去響應(yīng)了點擊。
此處提示:如果設(shè)置了綠色view..clipsToBounds = YES;這句代碼,含義就是裁剪超出綠色view的范圍,那么黃色view就不會顯示了。


響應(yīng)者鏈條
響應(yīng)者鏈條,是通過遞歸構(gòu)成的一組UIResponder對象的鏈式序列!

上圖簡述:
1.運行循環(huán)監(jiān)聽到屏幕被點擊時,首先會通知UIApplication去找找誰被點擊了,然后UIApplication會去通知UIWindow去找找誰被點擊了,然后UIWindow會去通知控制器去找找誰被點擊了,然后控制器會去通知view去找找誰被點擊了,然后view會去通知內(nèi)部的button是不是它被點擊了,button說是我被點擊了,然后告訴了view,然后view告訴控制器是button被點擊了,然后控制器告訴UIWindow是button被點擊了,然后UIWindow告訴UIApplication是button被點擊了,然后UIApplication告訴運行循環(huán)是button被點擊了,之后運行循環(huán)會通知控制器執(zhí)行點擊方法。
2.如果查找時,控制器找到view,view找到button,button說我沒有被點擊,那么會告訴view,然后view告訴控制器button沒有被點擊,然后控制器告訴UIWindow這個button沒有被點擊,然后UIWindow告訴UIApplication這個button沒有被點擊,然后UIApplication告訴運行循環(huán)這個button沒有被點擊,然后這個事件此次會被拋棄。
hit-test
//1> 系統(tǒng)會自動遞歸調(diào)用hitTest方法來判斷哪一個視圖來響應(yīng)點擊事件
// 2> hitTest方法是系統(tǒng)"底層專門"用來"遞歸遍歷"哪一個視圖應(yīng)該對點擊做出響應(yīng)的方法!
// 3> point參數(shù)是當前視圖的坐標點,專門用來判斷用戶觸摸點是否在視圖的"有效范圍"內(nèi)!
// 4> 每一個控件都有hitTesthitTest方法,需要時重寫即可。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [super hitTest:point withEvent:event];
}

首先看如上圖層級關(guān)系,在控制器里實現(xiàn)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,此方法不會被調(diào)用
- (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
總結(jié)
通過打印可以發(fā)現(xiàn)如果點擊白色view,系統(tǒng)會輪循調(diào)用到紅色view里的hitTest,然后touchesBegan里拿到的view是白色view;如果點擊紅色view,系統(tǒng)會輪循調(diào)用到藍色view里的hitTest,然后touchesBegan里拿到的view是紅色view。所以通過響應(yīng)者鏈條,可以知道是找到響應(yīng)者后,最多再查下子控件,之后不在深入去輪循。如上面代碼,如果我們把紅色view里把hitTest的返回寫成 return self,那么,我們點擊白色view時,會輪循到紅色view里的hitTest,然后touchesBegan里拿到的view就變成了紅色view了,因為紅色view的hitTest強制返回self。如果我們點擊紅色view,touchesBegan里拿到的view是紅色view,同時不再去藍色view里調(diào)用hitTest;如果我們點擊藍色的view,那么touchesBegan返回的依舊是紅色view,同時調(diào)用到紅色view的hitTest就會停止調(diào)用藍色view的hitTest。
如何讓button點擊范圍變大,讓view點擊范圍變大?
點擊view頭文件里看以查找到如下方法
//返回視圖層級中能響應(yīng)觸控點的最深視圖
- (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已經(jīng)知道了是系統(tǒng)輪循遍歷響應(yīng)者的。那么pointInside則可以達到改變點擊范圍。至于如何做到,且看下面。
最土的一個辦法自然就是直接在button上蓋一個大點的view添加響應(yīng)事件,還有其他辦法更簡單實現(xiàn)。
第一種
繼承于UIButton,然后重寫方法 -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
如下實踐,繼承于UIButton,實現(xiàn)如下效果:
#import <UIKit/UIKit.h>
@interface MyBigButton : UIButton
@end
#import "MyBigButton.h"
@implementation MyBigButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event{
CGRect bounds = self.bounds;
//若原熱區(qū)小于44x44,則放大熱區(qū),否則保持原大小不變
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
/*
擴展
//該結(jié)構(gòu)體的應(yīng)用是以原rect為中心,再參考dx,dy,進行縮放或者放大。
CGRectInset CGRect CGRectInset (
CGRect rect,
CGFloat dx,
CGFloat dy
);
函數(shù)CGRectContainsPoint(CGRect rect, CGPoint point)是用于判斷,參數(shù)2point是否包含在參數(shù)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基礎(chǔ)上根據(jù)邊緣距離切一個rect出來
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
什么是離屏渲染等,為什么會觸發(fā)
屏幕顯示圖像的原理

首先從過去的 CRT 顯示器原理說起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產(chǎn)生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發(fā)出一個水平同步信號(horizonal synchronization),簡稱 HSync;而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產(chǎn)生的頻率。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了,但原理仍然沒有變。
垂直同步信號(VSync)
|--------------------> 水平同步信號(HSync)
|--------------------> 水平同步信號(HSync)
|--------------------> 水平同步信號(HSync)
|--------------------> 水平同步信號(HSync)
|--------------------> 水平同步信號(HSync)
|--------------------> 水平同步信號(HSync)
|--------------------> 水平同步信號(HSync)
\|/
CPU和GPU
在屏幕成像的過程中,CPU和GPU起著至關(guān)重要的作用
CPU(Central Processing Unit,中央處理器)
對象的創(chuàng)建和銷毀、對象屬性的調(diào)整、布局計算、文本的計算和排版、圖片的格式轉(zhuǎn)換和解碼、圖像的繪制(Core Graphics)
GPU(Graphics Processing Unit,圖形處理器)
紋理的渲染
計算 渲染 讀取 展示
CPU ----> GPU ---> 幀緩存 ---> 視頻控制器 --->屏幕
(渲染后放到緩存區(qū)) (由視頻控制器從緩存區(qū)讀取展示)
通常來說,計算機系統(tǒng)中 CPU、GPU、顯示器是以上面這種方式協(xié)同工作的。CPU 計算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。
在最簡單的情況下,幀緩沖區(qū)只有一個,這時幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題。為了解決效率問題,顯示系統(tǒng)通常會引入兩個緩沖區(qū),即雙緩沖機制。在這種情況下,GPU 會預先渲染好一幀放入一個緩沖區(qū)內(nèi),讓視頻控制器讀取,當下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升。
雙緩沖雖然能解決效率問題,但會引入一個新的問題。當視頻控制器還未讀取完成時,即屏幕內(nèi)容剛顯示一半時,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫面撕裂現(xiàn)象,如下圖:

為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync),當開啟垂直同步后,GPU 會等待顯示器的 VSync 信號發(fā)出后,才進行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度,但需要消費更多的計算資源,也會帶來部分延遲。
那么目前主流的移動設(shè)備是什么情況呢?從網(wǎng)上查到的資料可以知道,iOS 設(shè)備會始終使用雙緩存,并開啟垂直同步。而安卓設(shè)備直到 4.1 版本,Google 才開始引入這種機制,目前安卓系統(tǒng)是三緩存+垂直同步。
卡頓產(chǎn)生的原因和解決方案

在 VSync 信號到來后,系統(tǒng)圖形服務(wù)會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內(nèi)容不變。這就是界面卡頓的原因。
從上面的圖中可以看到,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象。所以開發(fā)時,也需要分別對 CPU 和 GPU 壓力進行評估和優(yōu)化。
CPU 資源消耗原因和解決方案
對象創(chuàng)建
對象的創(chuàng)建會分配內(nèi)存、調(diào)整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多,那么不需要響應(yīng)觸摸事件的控件,用 CALayer 顯示會更加合適。如果對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建,但可惜的是包含有 CALayer 的控件,都只能在主線程創(chuàng)建和操作。通過 Storyboard 創(chuàng)建視圖對象時,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術(shù)選擇。
盡量推遲對象創(chuàng)建的時間,并把對象的創(chuàng)建分散到多個任務(wù)中去。盡管這實現(xiàn)起來比較麻煩,并且?guī)淼膬?yōu)勢并不多,但如果有能力做,還是要盡量嘗試一下。如果對象可以復用,并且復用的代價比釋放、創(chuàng)建新對象要小,那么這類對象應(yīng)當盡量放到一個緩存池里復用。
對象調(diào)整
對象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方。這里特別說一下 CALayer:CALayer 內(nèi)部并沒有屬性,當調(diào)用屬性方法時,它內(nèi)部是通過運行時 resolveInstanceMethod 為對象臨時添加一個方法,并把對應(yīng)屬性值保存到內(nèi)部的一個 Dictionary 里,同時還會通知 delegate、創(chuàng)建動畫等等,非常消耗資源。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進行調(diào)整時,消耗的資源要遠大于一般的屬性。對此你在應(yīng)用中,應(yīng)該盡量減少不必要的屬性修改。
當視圖層次調(diào)整時,UIView、CALayer 之間會出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時,應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖。
對象銷毀
對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的。通常當容器類持有大量對象時,其銷毀時的資源消耗就非常明顯。同樣的,如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。這里有個小 Tip:把對象捕獲到 block 中,然后扔到后臺隊列去隨便發(fā)送個消息以避免編譯器警告,就可以讓對象在后臺線程銷毀了。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
布局計算
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局、并且對視圖布局進行緩存,那么這個地方基本就不會產(chǎn)生性能問題了。
不論通過何種技術(shù)對視圖進行布局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調(diào)整上。上面也說過,對這些屬性的調(diào)整非常消耗資源,所以盡量提前計算好布局,在需要時一次性調(diào)整好對應(yīng)屬性,而不要多次、頻繁的計算和調(diào)整這些屬性。
Autolayout
Autolayout 是蘋果本身提倡的技術(shù),在大部分情況下也能很好的提升開發(fā)效率,但是 Autolayout 對于復雜視圖來說常常會產(chǎn)生嚴重的性能問題。隨著視圖數(shù)量的增長,Autolayout 帶來的 CPU 消耗會呈指數(shù)級上升。具體數(shù)據(jù)可以看這個文章:http://pilky.me/36/。 如果你不想手動調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
文本計算
如果一個界面中包含大量文本(比如微博微信朋友圈等),文本的寬高計算會占用很大一部分資源,并且不可避免。如果你對文本顯示沒有特殊要求,可以參考下 UILabel 內(nèi)部的實現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本。盡管這兩個方法性能不錯,但仍舊需要放到后臺線程進行以避免阻塞主線程。
如果你用 CoreText 繪制文本,那就可以先生成 CoreText 排版對象,然后自己計算了,并且 CoreText 對象還能保留以供稍后繪制使用。
文本渲染
屏幕上能看到的所有文本內(nèi)容控件,包括 UIWebView,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進行的,當顯示大量文本時,CPU 的壓力會非常大。對此解決方案只有一個,那就是自定義文本控件,用 TextKit 或最底層的 CoreText 對文本異步繪制。盡管這實現(xiàn)起來非常麻煩,但其帶來的優(yōu)勢也非常大,CoreText 對象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計算(調(diào)整 UILabel 大小時算一遍、UILabel 繪制時內(nèi)部再算一遍);CoreText 對象占用內(nèi)存較少,可以緩存下來以備稍后多次渲染。
圖片的解碼
當你用 UIImage 或 CGImageSource 的那幾個方法創(chuàng)建圖片時,圖片數(shù)據(jù)并不會立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會得到解碼。這一步是發(fā)生在主線程的,并且不可避免。如果想要繞開這個機制,常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。目前常見的網(wǎng)絡(luò)圖片庫都自帶這個功能。
圖像的繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中,然后從畫布創(chuàng)建圖片并顯示這樣一個過程。這個最常見的地方就是 [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)和頂點描述(三角形),應(yīng)用變換(transform)、混合并渲染,然后輸出到屏幕上。通常你所能看到的內(nèi)容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
紋理的渲染
所有的 Bitmap,包括圖片、文本、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調(diào)整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。
當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說,iPhone 4S 以上機型,紋理尺寸上限都是 4096×4096,更詳細的資料可以看這里:iosres.com。所以,盡量不要讓圖片和視圖的大小超過這個值。
視圖的混合 (Composing)
當多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結(jié)構(gòu)過于復雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應(yīng)用應(yīng)當盡量減少視圖數(shù)量和層次,并在不透明的視圖里標明 opaque 屬性以避免無用的 Alpha 通道合成。當然,這也可以用上面的方法,把多個視圖預先渲染為一張圖片來顯示。
圖形的生成
CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示,通常會觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中。當一個列表視圖中出現(xiàn)大量圓角的 CALayer,并且快速滑動時,可以觀察到 GPU 資源已經(jīng)占滿,而 CPU 資源消耗很少。這時界面仍然能正?;瑒樱骄鶐瑪?shù)會降到很低。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去。對于只需要圓角的某些場合,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法,就是把需要顯示的圖形在后臺線程繪制為圖片,避免使用圓角、陰影、遮罩等屬性。
卡頓檢測
可以先用簡易控件 YYFPSLabel看一下 CPU 消耗,發(fā)現(xiàn)幀數(shù)在出現(xiàn)明顯卡頓時仍保持在55-60之間,可排除 CPU。
用 Instuments 的 GPU Driver 預設(shè),能夠?qū)崟r查看到 CPU 和 GPU 的資源消耗。在這個預設(shè)內(nèi),你能查看到幾乎所有與顯示有關(guān)的數(shù)據(jù),比如 Texture 數(shù)量、CA 提交的頻率、GPU 消耗等,在定位界面卡頓的問題時,這是最好的工具。
可以添加Observer到主線程RunLoop中,通過監(jiān)聽RunLoop狀態(tài)切換的耗時,以達到監(jiān)控卡頓的目的
離屏渲染
油畫算法
計算機圖層的疊加繪制大概遵循油畫算法,在這種算法下會按層繪制,首先繪制距離較遠的場景,然后用繪制距離較近的場景覆蓋較遠的部分,如下圖。

這樣就不會導致遠的物體擋住近的物體,但是有個局限,就是無法在后面一層渲染完成后,再回去修改前面圖層,因為前面的圖層已經(jīng)被覆蓋了
離屏渲染
對于有前后依賴的圖層(如全局剪切,陰影等),油畫算法無法滿足我們的需求,對于有前后依賴的圖層,我們可以再另開辟一個空間,用于臨時渲染,渲染完成后再渲染到當前的緩沖區(qū)上,這個臨時渲染,就是離屏渲染,由于需要開辟一個新的內(nèi)存空間,并且共享同一個上下文,所以還需要做上下文切換(狀態(tài)切換),并且渲染完成后還要進行拷貝操作。
1.開辟臨時緩存空間
2.上下文切換,上下文對象比較大,切換操作會帶來一定的性能消耗
3.內(nèi)存拷貝
4.額外的渲染
上面4項帶來的開銷會很大,并且每一幀渲染都需要執(zhí)行,如果屏幕上觸發(fā)離屏渲染的操作過多,會導致GPU渲染時間過長造成卡頓,應(yīng)該避免觸發(fā)離屏渲染。
離屏渲染檢測
模擬器的工具欄選擇debug -> 選取 color Offscreen-Rendered.
開啟后會把那些需要離屏渲染的圖層高亮成黃色,這就意味著黃色圖層可能存在性能問題
哪些操作會觸發(fā)離屏渲染?
圓角,同時設(shè)置layer.masksToBounds = YES、layer.cornerRadius大于0
光柵化,layer.shouldRasterize = YES (光柵化含義:將圖轉(zhuǎn)化為一個個柵格組成的圖象。 光柵化特點:每個元素對應(yīng)幀緩沖區(qū)中的一像素。)
遮罩,layer.mask
陰影,layer.shadowXXX
如果設(shè)置了layer.shadowPath就不會產(chǎn)生離屏渲染
圓角觸發(fā)離屏條件
1.設(shè)置layer.masksToBounds = YES
2.設(shè)置layer.cornerRadius大于0
3.同時設(shè)置上面?zhèn)z條件后并且用于渲染的圖層大于1,就會觸發(fā)離屏渲染。(背景顏色相當于一個單獨一個圖層)
3.1> imageView單圖層,同時設(shè)置背景和圓角可觸發(fā)。
3.2> button多圖層,設(shè)置圓角可觸發(fā)。
3.3> 無繪制或無子控件的view是單圖層,添加子控件并設(shè)置圓角可觸發(fā)
3.4> label單圖層,設(shè)置圓角背景還不能觸發(fā)。
優(yōu)化圓角觸發(fā)離屏
1.避免使用裁切(masksToBounds)操作,如果我們能確保View里面的內(nèi)容不會溢出,就可以不用masksToBounds
2.即使要用到裁切的操作,盡量放到子view里面,不要在上層view使用masksToBounds,因為裁切需要對所有的layer和subview所有圖層都進行裁切,這樣離屏渲染會需要更大的空間,裁切更多的圖層,應(yīng)該只對必要的view/layer進行裁切
3.提前切好需要的圓角,避免渲染的時候再切
4.使用1. 使用YYWebImage去處理
問題: 展開講下利用RunLoop檢測卡頓
source0 處理的是 app 內(nèi)部事件,包括 UI 事件,每次處理的開始和結(jié)束的耗時決定了當前頁面刷新是否正常,即 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 之間。因此創(chuàng)建一個子線程去監(jiān)聽主線程狀態(tài)變化,通過dispatch_semaphore 在主線程進入上面兩個狀態(tài)時發(fā)送信號量,子線程設(shè)置超時時間循環(huán)等待信號量,若超過時間后還未接收到主線程發(fā)出的信號量則可判斷為卡頓,此時可以保存當前調(diào)用棧信息作為后續(xù)分析依據(jù),線上卡頓監(jiān)控多采用這種方式
#pragma mark - 注冊RunLoop觀察者
//在主線程注冊RunLoop觀察者
- (void)registerMainRunLoopObserver
{
//監(jiān)聽每個步湊的回調(diào)
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;
//觸發(fā)信號,說明開始執(zhí)行下一個步驟。
if (self.semaphore != nil)
{
dispatch_semaphore_signal(self.semaphore);
}
}
#pragma mark - RunLoop狀態(tài)監(jiān)測
//創(chuàng)建一個子線程去監(jiān)聽主線程RunLoop狀態(tài)
- (void)createRunLoopStatusMonitor
{
//創(chuàng)建信號
self.semaphore = dispatch_semaphore_create(0);
if (self.semaphore == nil)
{
return;
}
//創(chuàng)建一個子線程,監(jiān)測Runloop狀態(tài)時長
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
while (YES)
{
//如果觀察者已經(jīng)移除,則停止進行狀態(tài)監(jiān)測
if (self.runLoopObserver == nil)
{
self.runLoopActivity = 0;
self.semaphore = nil;
return;
}
//信號量等待。狀態(tài)不等于0,說明狀態(tài)等待超時
//方案一->設(shè)置單次超時時間為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)
{
...
//發(fā)生超過500毫秒的卡頓,此時去記錄調(diào)用棧信息
}
}
/*
//方案二->連續(xù)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;
//保存調(diào)用棧信息
}
}
timeoutCount = 0;
*/
}
});
}
問題:控制器的生命周期
1.alloc:創(chuàng)建對象,分配空間
//是當從nib文件中加載對象的時候會調(diào)用
2.initWithCoder:(NSCoder *)aDecoder(如果使用storyboard或者xib)
3.init (initWithNibName):初始化對象,初始化數(shù)據(jù)
4.awakeFromNib:
5.loadView:
6.viewDidLoad:
7.viewWillAppear:
8.viewWillLayoutSubviews:控制器的view將要布局子控件
9.viewDidLayoutSubviews:控制器的view布局子控件完成這期間系統(tǒng)可能會多次調(diào)用viewWillLayoutSubviews 、viewDidLayoutSubviews 倆個方法
10.viewDidAppear:
11.viewWillDisappear:控制器的view即將消失的時候這期間系統(tǒng)也會調(diào)用viewWillLayoutSubviews 、viewDidLayoutSubviews 兩個方法
12.viewDidDisappear:
13.dealloc:控制器銷毀
- didReceiveMemoryWarning:內(nèi)存警告
問題:UIButton 的父類是什么?UILabel 的父類又是什么?區(qū)別
UIButton -> UIControl -> UIView -> UIResponder
UILabel -> UIView -> UIResponder
//UIButton 繼承 UIContorl 中的方法和屬性說明:
// 1,設(shè)置按鈕狀態(tài)
//按鈕啟用或禁用
@property(nonatomic,getter=isEnabled) BOOL enabled;
//按鈕選中或不選中
@property(nonatomic,getter=isSelected) BOOL selected;
//按鈕高亮或不高亮
@property(nonatomic,getter=isHighlighted) BOOL highlighted;
//2,設(shè)置按鈕文字的對齊方式
//縱向居中對齊方式
@property(nonatomic) UIControlContentVerticalAlignment contentVerticalAlignment;
//橫向居中對齊方式
@property(nonatomic) UIControlContentHorizontalAlignment contentHorizontalAlignment;
//3,設(shè)置或取消監(jiān)聽事件
- (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需要設(shè)置contenSize大小。
xib布局時沒法直接設(shè)置contenSize所以需要想其他方法。
思路
1.添加ScrollView
2.給ScrollView設(shè)置上、下、左、右的約束
3.給ScrollView添加一個ContainView(就是普通view),設(shè)置它的上下左右約束
4.給ContainView添加子View,用以將父View撐開,從而可以滑動。
總結(jié):
scrollView的frame通過與父視圖的約束進行確定
scrollView的contentSize的高度寬度通過ContainView來確定
問題:比如一頁有多行cell,能全部加載。說下數(shù)據(jù)源和代理調(diào)用順序
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能滑動,需要設(shè)置contentSize,那么tableView的contentSize怎么來呢?iOS11之前,會調(diào)用tableView每一個cell的heightForRowAtIndexPath來算出整個高度,從而相加得出contentSize來,這一個步驟挺耗性能!但是iOS11之后默認打開了estimatedRowHeight估算高度功能,當tableView創(chuàng)建完成后,contentSize為estimatedRowHeight(默認值為44)*cell的數(shù)量,不需要遍歷每一個cell的heightForRowAtIndexPath來計算了,在滑動的時候,來準確計算這個值。
所以數(shù)據(jù)源和代理的調(diào)用順序分為11之前和之后
iOS7~iOS10數(shù)據(jù)源和代理調(diào)用順序
1.先調(diào)用numberOfRowsInSection
2.有多少row調(diào)用多次heightForRowAtIndexPath
以上兩步可以計算出需要展示的contentSize高度
3.每調(diào)用一次cellForRowAtIndexPath之后調(diào)用一次heightForRowAtIndexPath去具體布局單個cell
iOS11之后
1.先調(diào)用numberOfRowsInSection
2.每調(diào)用一次cellForRowAtIndexPath之后調(diào)用一次heightForRowAtIndexPath去具體布局單個cell
但是如果iOS11之后設(shè)置了self.tableV.estimatedRowHeight = 0;(也就是說關(guān)閉了預估高度),調(diào)用順序跟iOS11之前的調(diào)用一致。
問題:布局框架
MyLayout
MyLayout是一套iOS界面視圖布局框架。
MyLayout的實現(xiàn)內(nèi)核是基于frame的設(shè)置,而不是對AutoLayout的封裝。因此在使用上不會受到任何操作系統(tǒng)版本的限制。
Masonry
基于NSLayoutConstraint封裝
什么是NSLayoutConstraint?
在xib中,我們可以用拖拽約束的方式來給空間添加約束條件,但是如果控件過多,則整個xib文件中的線條會變得混亂不堪,雖然蘋果在極力推薦可視化的加約束方式,但是還是給我們提供了代碼的方式來添加約束:NSLayoutConstraint。
SDAutoLayout
問題:左右label布局,左邊文字長時盡量大時,壓縮右邊,怎么實現(xiàn)?
設(shè)置右邊label權(quán)重小于左邊
http://www.itdecent.cn/p/a4b8e0c8e68d
