本文涉及到的幾個問題來源於一次面試經歷 ,當時回答的一塌糊塗,所以回來就看看官方文件和一些部落格文章,並問下同學看看他們如何回答這幾個問題.
希望各位 iOS 開發同學,在學習使用 Cocoa的一些 Framework 時多看看一些官方文件說明,有時候官方文件都有明確說明,我就是沒有看, so 我面試時回答不出來.
宣告 : 本文程式碼部分來源於 objc 中國 View-Layer 協作 https://objccn.io/issue-12-4/ GitHub : https://github.com/ZhaoBingDong/CAAnimaiton 一 : 為什麼跟 UI 相關的 API 要在主執行緒 (個人理解)
Note
For the most part, use UIKit classes only from your app’s main thread. This is particularly true for classes derived from UIResponder or that involve manipulating your app’s user interface in any way.
在大多數的時候 , 在你的 app 裡使用 UIKit 的類只能在主執行緒. 尤其適用於類UIResponder源於或涉及操縱你的應用程式的使用者介面。
蘋果 Foundation 和 UIKit 大部分 API 不是執行緒安全的 因為程式涉及到多執行緒地方畢竟是少數應用場景 蘋果給每個方法屬性加這種執行緒鎖 執行緒同步鎖 自旋鎖 什麼的 太麻煩 和消耗效能 需要開發者在使用到地方時自己注意下執行緒安全,避免多個執行緒訪問同一塊資源,包括多個執行緒對同一個 UI 物件的操作 像 NSArray NSDictionary 不可變的都是執行緒安全的 而 NSMutableArray NSMutableDictionary 執行緒是不安全的.
複製程式碼
二 : 改變 UIView 的那些屬效能夠產生動畫效果
The following properties of the UIView class are animatable:
frame 包括了
Warning
If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
frame :The frame rectangle, which describes the view’s location and size in its superview’s coordinate system.
// frame 是一個 view 在父 view 座標系中的位置 和 尺寸 相對於父控制元件而言
Warning
If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
// 意思大概是 如果設定了 transform 屬性後 不是 CGAffineTransformIdentity型別的 之前設定的 frame 將不起作用 ,當重新設定成 view.transform = CGAffineTransformIdentity 後 view 會使用原來的 frame , 使用 transfrom 去改變 view 的形變 位移 旋轉效果後 ,動畫結束後需要 設定 CGAffineTransformIdentity 進行復位.
bounds : The bounds rectangle, which describes the view’s location and size in its own coordinate system.
// bounds 一個 view 在自身座標系的 位置和尺寸 相當於自設而言
center
transform
alpha
backgroundColor
改變以上屬性的值後 會產生一些動畫效果
複製程式碼
三 : 為什麼設定 view.frame bounds center transform 等會有效果
當設定 view.frame 時 view 內部會呼叫 view的 actionForLayer:forKey: 生成一個 遵守 CAAction 的物件 ,生成一個動畫的物件,並新增到 UIView 的 rootLayer 中去 ,然後呼叫 layer addAnimation 方法 將這個動畫新增到 layer 上
複製程式碼
1 通過重寫 MyView 的
+ (Class)layerClass {
return [MyLayer class];
}
// 生成動畫的物件 基於 CAAction 協議的 動畫物件 可以看做是 CAAnimation 或者遵守 CAAction 協議的私有類
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
return [super actionForLayer:layer forKey:event];
}
將 UIView 的 layer 由系統的 CALayer 變成子類化的 MyLayer 這樣便於重寫 MyLayer 內部的
檢視 那些動畫物件新增到 MyLayer 上 一個改變 view.frame 的操作 會生成多個 CAAnimtaion 對像
二 將動畫新增到 rootLayer 上 (一個 view 只有一個 rootLayer,它是view.layer 屬性 ,一個 layer可以有多個 subLayers ,一個 subLayer 有且只有一個父 layer)
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
NSLog(@"\nadding animation: %@\n", [anim debugDescription]);
[super addAnimation:anim forKey:key];
}
複製程式碼
四 : 如何實現自定義的 UIView animations
iOS4以後 UIView 提供了 animateWithDuration:animations: 動畫塊 方法 支援一些基礎動畫 ,在此支援 需要使用 beginAnimations:context: 和 commitAnimations 來管理動畫
將需要改變 view 的一些動畫屬性 封裝到塊程式碼裡邊. 那麼我們是否可以將一些常用動畫封裝成這樣的塊程式碼呢 ,減少一些重複程式碼的呼叫?
如果你看了 問題2後 一定會有所啟發 ,其實這是可以實現的,需要用到 runtime 去 攔截UIView系統的的 actionForLayer : forkey 為自己寫的方法
+ (void)load {
SEL originalSelector = @selector(actionForLayer:forKey:);
SEL extendedSelector = @selector(DR_actionForLayer:forKey:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method extendedMethod = class_getInstanceMethod(self, extendedSelector);
NSAssert(originalMethod, @"original method should exist");
NSAssert(extendedMethod, @"exchanged method should exist");
if(class_addMethod(self, originalSelector, method_getImplementation(extendedMethod), method_getTypeEncoding(extendedMethod))) {
class_replaceMethod(self, extendedSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, extendedMethod);
}
}
// 判斷不同的上下文 區別系統的 UIView animaitons 動畫 和自己寫的
//[UIView DR_popAnimationWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations]
- (id<CAAction>)DR_actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
if (DR_currentAnimationContext == DR_popAnimationContext) {
// 這裡寫我們自定義的程式碼...
// 儲存生成的每個動畫物件所在 layer 即要實現動畫的 view.layer 在 DR_popAnimationWithDuration 方法裡 取出來儲存的 layer 並 執行一些自定義的 CAAnimaiton 動畫
DRSavedPopAnimationState *state = [DRSavedPopAnimationState savedStateWithLayer:layer keyPath:event];
[[UIView DR_savedPopAnimationStates] addObject:state];
}
// 呼叫原始方法
return [self DR_actionForLayer:layer forKey:event]; // 沒錯,你沒看錯。因為它們已經被交換了
}
複製程式碼
2 : 實現自定義 UIView animations 程式碼部分
+ (void)DR_popAnimationWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations
{
DR_currentAnimationContext = DR_popAnimationContext;
// 執行動畫 (它將觸發交換後的 delegate 方法) 當你在 block 塊裡改變一些 view.frame 屬性時 就會攔截到 UIView 的 actionforLayer 方法 拿到需要執行動畫的 layer
animations();
// 取出儲存的 CALayer 物件 然後生成自定義的 CAAnimaiton 物件 新增到 layer 生成動畫
NSMutableArray *states = [UIView DR_savedPopAnimationStates];
for (DRSavedPopAnimationState *state in states) {
DRSavedPopAnimationState *savedState = (DRSavedPopAnimationState *)state;
CALayer *layer = savedState.layer;
NSString *keyPath = savedState.keyPath;
id oldValue = savedState.oldValue;
id newValue = [layer valueForKeyPath:keyPath];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:keyPath];
CGFloat easing = 0.2;
CAMediaTimingFunction *easeIn = [CAMediaTimingFunction functionWithControlPoints:1.0 :0.0 :(1.0-easing) :1.0];
CAMediaTimingFunction *easeOut = [CAMediaTimingFunction functionWithControlPoints:easing :0.0 :0.0 :1.0];
anim.duration = duration;
anim.keyTimes = @[@0, @(0.35), @1];
anim.values = @[oldValue, newValue, oldValue];
anim.timingFunctions = @[easeIn, easeOut];
// 不帶動畫地返回原來的值
[CATransaction begin];
[CATransaction setDisableActions:YES];
[layer setValue:oldValue forKeyPath:keyPath];
[CATransaction commit];
// 新增 "pop" 動畫
[layer addAnimation:anim forKey:keyPath];
}
// 動畫執行完後 移除儲存的 layer 等一些動畫屬性資訊
[states removeAllObjects];
/* 一會兒再新增 */ 清空上下文資訊 下次還根據這個上下文 區分 animateWithDuration 和 自定義的 DR_popAnimationWithDuration 方法
DR_currentAnimationContext = NULL;
}
複製程式碼
// 通過以上的程式碼部分 大致瞭解到了 UIView aniaitons 動畫塊內部都做了哪些操作 對核心動畫有了進一步的瞭解
最終效果如下 :