iOS 技術
1. 請問前後臺切換,會發生些什麼,系統哪些方法會被呼叫,viewcontroller哪些方法會被呼叫
在不考慮 APP 在後臺被 kill 的情況: 進入後臺:
方法 | 作用 |
---|---|
applicationWillResignActive |
點選 Home 鍵,app開始準備進入後臺,這個時候會進入該回調,意味著 app 被掛起,程序即將失去活躍。經過不嚴謹的測試,大約有 10 分鐘左右的時間用來處理事務。 |
applicationDidEnterBackground |
當 applicationWillResignActive回撥方法完全執行完畢後,會進入 applicationDidEnterBackground 。 |
進入前臺:
方法 | 作用 |
---|---|
applicationWillEnterForeground |
在 app 未被殺死的情況下,點選 icon再次進入 app,重新回到前臺之前會先進入 applicationWillEnterForeground 回撥 |
applicationDidBecomeActive |
當 applicationWillEnterForeground 執行完畢後,會進入 applicationDidBecomeActive 回撥,正式迴歸活躍。 |
前後臺切換,主要的坑點在於:VC中並沒函式會呼叫,尤其注意:VC 相關的 Appear 和 Disappear 函式並不會被呼叫。想在VC中監聽切換,只能監聽通知,每個在appdelegate的生命代理方法都有對應的通知。
如果考慮 APP 在後臺被 kill 的情況:
進入後臺後,如果沒有後臺執行許可權及功能,可能在一段時間後被系統 kill 掉,再次進入app後,會重新進入啟動流程。
方法 | 作用 |
---|---|
main() 函式: | 這個階段一般是 可執行 .o 檔案,動態庫載入,objc類註冊,category 類註冊,selector 唯一性檢查,+(void)load 方法,C++ 靜態全域性變數的建立等。 |
didFinishLaunchingWithOptions |
使用者點選 icon 啟動 app,或者被 kill 後以任何方式進入 app,在 main() 執行後,會進入didFinishLaunchingWithOptions回撥,處理首屏渲染,以及其他業務相關的事件,例如監聽事件,配置檔案讀寫或者 SDK 初始化等等。 |
applicationDidBecomeActive |
在didFinishLaunchingWithOptions方法作用域結束後,會進入 applicationDidBecomeActive 回撥,也正式意味著 app 已經處於活躍狀態。 |
rootViewController 的相關的 Appear 函式 |
注意:此時rootViewController 的相關的 Appear 函式會被呼叫。 |
參考連結:WWDC 2016 - Session 406-Optimizing App Startup Time
2. 請問對無序的Array排序,有什麼好的方法,程式碼越少,API越高階越好。有無原生方法可以辦到。
蘋果為我們提供了很多 Array 的排序方法,但原理上可以看到就是 Comparator (比較器) 和 Descriptor (描述器) 兩種,像是 Selector 和 Function ,最終也是使用 Comparator 在做排序,只是響應方法不同。 其中 Swift 也有方法: array.sort(),見參考連結:Apple Documentation-Swift-Array-sorted 先說說 Comparator ,如果陣列中元素是 String 或 Number,首選 Comparator,可以將 compare: 方法的返回值直接作為 NSComparisonResult 返回值。實際的排序程式碼三行就可以搞定。當然用 Selector 和 Function,也是一樣的效果,但需要寫更多的程式碼。 例子一:
NSArray *sortedArray = [array sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1,NSString *obj2) {
if ([obj1 compare:obj2] == NSOrderedAscending) {
return NSOrderedAscending;
} else if ([obj1 compare:obj2] == NSOrderedDescending){
return NSOrderedDescending;
}else {
return NSOrderedSame;
}
}];
複製程式碼
例子二:
- (void)arraySortUsingCompare {
// 比較器 排序
NSMutableArray *arr = [NSMutableArray array];
for (int i = 0; i < 10; i ++) {
int n = arc4random() % (10 - 0) + 1;
[arr addObject:@(n)];
}
NSLog(@"排序前 ===== %@",arr);
[arr sortUsingComparator:^NSComparisonResult(NSNumber *num1,NSNumber *num2) {
// return [num1 compare:num2]; // 正序
return [num2 compare:num1]; // 倒序
}];
NSLog(@"排序後 %@",arr);
arr = [NSMutableArray array];
[arr addObject:@"Kobe Bryant"];
[arr addObject:@"LeBorn James"];
[arr addObject:@"Steve Nash"];
[arr addObject:@"Stephen Curry"];
[arr addObject:@"Monkey D Luffy"];
[arr addObject:@"Roronoa Zoro"];
NSLog(@"排序前 ==== %@",arr);
[arr sortUsingComparator:^NSComparisonResult(NSString *str1,NSString *str2) {
// return [str1 compare:str2]; // 正序
return [str2 compare:str1]; // 倒序
}];
NSLog(@"排序後 %@",arr);
}
複製程式碼
參考連結:Objective-C中的排序及Compare陷阱
但是如果需要針對一個物件的幾個屬性作為不同的維度去做排序,那選擇 Descriptor,因為不需要根據利用屬性對排序優先順序寫一大堆的邏輯判斷。主要將所有參與比較的屬性都放入描述器中即可,如果想對球員的年齡和號碼(優先順序分先後)進行排序,只需要依次加入描述器組,三行程式碼就可以完成。
- (void)arraySortUsingDescriptor {
NSMutableArray *arr = [NSMutableArray array];
Person *person = [[Person alloc] init];
person.name = @"Ingram";
person.age = 21;
person.number = 14;
[arr addObject:person];
person = [[Person alloc] init];
person.name = @"Ball";
person.age = 21;
person.number = 2;
[arr addObject:person];
person = [[Person alloc] init];
person.name = @"Zubac";
person.age = 21;
person.number = 15;
[arr addObject:person];
NSSortDescriptor *ageDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
NSSortDescriptor *numberDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"number" ascending:YES];
[arr sortUsingDescriptors:@[numberDescriptor,ageDescriptor]];
for (Person *person in arr) {
NSLog(@"\n 球員姓名: %@ \n 球員號碼: %d \n 球員年齡: %d \n -------- \n",person.name,person.number,person.age);
}
}
複製程式碼
3. 請問APNs推送如何區分裝置,如何將裝置的資訊傳給Apple,你上傳的時機時怎樣的,猜想這個裝置資訊是如何生成的
裝置資訊傳遞給apple
post請求; Use HTTP/2 and TLS 1.2 or later to establish a connection between your provider server and one of the following servers:
- Development server: api.sandbox.push.apple.com:443
- Production server: api.push.apple.com:443
也就是: 裝置資訊是通過一個POST請求將DeveiceToken和其他資訊傳送給APNS,需要用 HTTP/2 和 TLS 1.2或以上的版本,在自己提供的服務和以上服務之間建立連線。
開發環境:api.sandbox.push.apple.com:443
生產環境:api.push.apple.com:443
當然,還可以用一臺機器的 2197 埠讓 APNS 通過防火牆
請求示例:
HEADERS
- END_STREAM
+ END_HEADERS
:method = POST
:scheme = https
:path = /3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0
host = api.sandbox.push.apple.com
authorization = bearer eyAia2lkIjogIjhZTDNHM1JSWDciIH0.eyAiaXNzIjogIkM4Nk5WOUpYM0QiLCAiaWF0I
jogIjE0NTkxNDM1ODA2NTAiIH0.MEYCIQDzqyahmH1rz1s-LFNkylXEa2lZ_aOCX4daxxTZkVEGzwIhALvkClnx5m5eAT6
Lxw7LZtEQcH6JENhJTMArwLf3sXwi
apns-id = eabeae54-14a8-11e5-b60b-1697f925ec7b
apns-expiration = 0
apns-priority = 10
apns-topic = com.example.MyApp
DATA
+ END_STREAM
{ "aps" : { "alert" : "Hello" } }
複製程式碼
上傳時機
didRegisterForRemoteNotificationsWithDeviceToken
方法,回撥內處理裝置資訊上傳的業務。但有些情況是,我們希望根據使用者賬號來做推送,例如即時通訊應用。那麼我們就要在登入或自動登入後,上傳deviceToken,和使用者資訊繫結並處理替換邏輯,避免推送錯亂。
裝置資訊
This address takes the form of a device token unique to both the device and your app.
猜測:UDID+bundleId+生產/開發環境+時間戳。
其中注意帶時間戳hash是為什麼頻繁上傳device token的主要原因。長期不活躍app,比如使用者一個月或者兩個月沒開啟過該app,該伺服器後端就再也推不到了。
3. 謹慎iOS黑魔法 - Method Swizzling
優點:
區別於⼿動為每⼀個類編寫埋點⽅法或者寫⼀個基類來做統⼀的埋點,前兩者在某些場景下⼯ 作量都不算⼩。可以做⼀個UIViewController的Category,置換原⽣⽅法,在置換⽅法中將寫⼊埋點程式碼,這樣可以直接⼀鍵埋點完成。之後新增的UIViewController類也不需要再關⼼這些的埋點程式碼。
- (void)cyl_APOViewDidLoad {
Class class = [self class];
if (!([class isEqual:[UIViewController class]] || [class isEqual: [UINavigationController class]])) {
NSLog(@"統計該⻚⾯ %@",class);
}
}
複製程式碼
置換 NSDictionary
的 -setObject:forKey:
方法,用於防止 crash
。NSArray
同理。
- (void)cyl_safeSetObject:(id)object forKey:(id<NSCopying>)key {
if (object && key) {
[self safe_setObject:object forKey:key];
}
}
複製程式碼
缺點:
總結:一時hook一時爽,debug火葬場。
原因:
以下為What are the Dangers of Method Swizzling in Objective-C? 中列舉出的7個問題:
- Method swizzling is not atomic
- Changes behavior of un-owned code
- Possible naming conflicts
- Swizzling changes the method's arguments
- The order of swizzles matters
- Difficult to understand (looks recursive)
- Difficult to debug
可見,其沒有類似註解的東⻄,⽅法置換沒有有效宣告。如果濫⽤,反⽽會增加維護成本。若擅⾃使⽤未同步其他同學,會成為極⼤的項⽬隱患。尤其是⼀些封裝的模組。
這裡著重說明幾個場景:
場景:(iTeaTime(技術清談)@國家一級保護廢物 提供答案)
如果多次hook了同一個類的同一個方法, 跟分類重名的表現是一樣:表現為無法控制執行的先後順序,與編譯器build的順序有關,但編譯器順序有不可控性。
比如下面的實現方法,可能出現方法覆蓋的問題:
+ (void)load {
static dispatch_once_t onceToken; dispatch_once(&onceToken,^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(XK_ViewDidLoad);
Method originalMethod = class_getInstanceMethod(class,originalSelector);
Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod,swizzledMethod);
}
});
}
複製程式碼
場景:(iTeaTime(技術清談)@molon 提供) Hook了具有繼承關係的相同方法。
以下場景:
如果子類並沒有重寫父類的方法,拿父類的implement去swizzling本來就是錯誤的行為。
A<—繼承---B<—繼承---C (B是A的子類,C又是B的子類)
A 裡有 test 方法,但是 B 和 C 都沒有重寫。 通常如果要對 B 或者 C 的 test 進行hook的話,很多開發者都喜歡去給 B 或者 C add A.test 的 implemention。 那如果先hook的C,又hook的B,似乎就形成了C與A直接打交道的局面。但是以面向物件來說,C的原實現應該是B的當前實現才合理。 所以不應該hook當前類沒有重寫的方法,這種其實直接繼承(或者加category方法)就可以做了,不需要hook,需要呼叫原實現直接[super test]即可。
4. iPhone在無耳機狀態下,通過實體按鍵設定靜音後,以下路徑比如: 微信主tab-朋友圈-點開feed流中的小視訊,可以播放聲音。通過點選頭像-個人朋友圈主頁,點開視訊無法播放聲音。即使按聲音增加鍵也無法播放。請問這個表現不一致的現象,是feature還是bug,如果是bug你覺得是程式碼哪裡寫的有問題。寫出修復程式碼
視訊播放器預設靜音模式下是沒有聲音的,但可以控制即使是靜音模式下依然有聲音,顯然前者設定了,後者沒有設定。推測前者是被提交了bug所以fix掉了,後者使用場景比較少,所以沒有被注意到。
//忽略靜音按鈕
AVAudioSession *session =[AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
複製程式碼
完整程式碼:
- (AVAudioPlayer *)player {
if (!_player) {
NSURL *URL = [[NSBundle mainBundle] URLForResource:@"xxxx.wav"
withExtension:nil];
_player = [[AVAudioPlayer alloc] initWithContentsOfURL:URL error:nil];
AVAudioSession *autioSession = [AVAudioSession sharedInstance];
[autioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[autioSession setActive:YES error:nil];
[_player prepareToPlay];
}
複製程式碼
耳機場景下,統一做了處理,都可以播放視訊帶聲音。 比如以下程式碼用於判斷耳機狀態,因為AVAudioSession是單例,對耳機優先處理即可。
- (BOOL)isHeadsetPluggedIn {
AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];
for (AVAudioSessionPortDescription* desc in [route outputs]) {
if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones])
return YES;
}
return NO;
}
複製程式碼
5. 【iOS-autolayout】一個ScrollView上有3個UILabel,每個label字數不固定,類似字數很多的那種,要求上下依次排列,當文字超出ScrollView的時候可以滑動,左右不能滾動,上下可滾動。【難度???】【出題人群內大佬:@起點】
出題人提示
就是label的寬度設定跟scrollView等寬,最底下的label底部要跟scrollView的底部約束上就可以了。 考察的主要是scrollView的約束問題。scrollView的約束主要是從內部撐開寬度跟高度。
答案
三個label 那個,就是放了個scrollview然後裡面放三個label,從上往下邊距全部約束為0,然後label寬度與scrollview相同,最下面那個label距離底部scrollview為0。(在內部無需多放view)
- 在 Scrollview 新增⼀個 ContainView
- ContentView 完全覆蓋 Scrollview
- ContainView 上添加了三個 Label。View 的 bottom 和 第三個 Label 的 bottom 做約束
- 三個 Label 互相做間距和寬的約束,不約束⾼
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.contentView layoutIfNeeded];
self.scrollview.contentSize =
CGSizeMake(CGRectGetWidth(self.contentView.frame),CGRectGetHeight(self.contentView.frame));
}
複製程式碼
6. 如何用一行程式碼,互換兩個變數的值,且不產生第三個變數。
- 利用Swift元組特性:
可以在定義的同時就取出元祖中的值
// 相當於同時定義了三個變數
let (name,age,score) = (“a”,30,99.9)
根據這一特性,我們可以這樣互換值: (a,b) = (b,a)
- 異或或者加減
(a = a ^ b) && (b = a ^ b) && (a = a ^ b)
或者這樣
a = a ^ b;b = a ^ b;a = a ^ b;
(a = a + b) && (b = a - b) && (a = a - b)
(a = a x b) && (b = a / b) && (a = a / b)
7. 如何給view同時加上圓角和陰影?至少給出兩種實現方法,使用到的API越高階越好。
【提示】兩種方法,答案提示:UIBezierPath,和iOS11 layer有個新的方法 【答案】iOS11的layer是maskedCorners,CACornerMask。
參考連結:ios 圓角 cornerRadius 對效能的影響究竟多大? 你測試過嗎?
8. 猜想dequeueReusableCellWithIdentifier的實現是怎樣的,給出示例程式碼。注意邊界條件:相鄰cell的identifier相等時。你的實現中該函式的時間複雜度是多少。為什麼?【難度????】【出題人 微博@iOS程式犭袁】
cell複用機制的實現猜想,見GitHub-Chameleon:
- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
for (UITableViewCell *cell in _reusableCells) {
if ([cell.reuseIdentifier isEqualToString:identifier]) {
UITableViewCell *strongCell = cell;
// the above strongCell reference seems totally unnecessary,but without it ARC apparently
// ends up releasing the cell when it's removed on this line even though we're referencing it
// later in this method by way of the cell variable. I do not like this.
[_reusableCells removeObject:cell];
[strongCell prepareForReuse];
return strongCell;
}
}
return nil;
}
複製程式碼
時間複雜度為: O(n)
注意:
NSArray
/ NSMutableArray
containsObject:
,containsObject:``,indexOfObject*
,removeObject:
會遍歷裡面元素檢視是否與之匹對,所以複雜度等於或大於 O(n)。
這裡 _reusableCells
使用的是NSMutableSet
,而
NSSe
t / NSMutableSet
/ NSCountedSet
這些集合型別是無序沒有重複元素。這樣就可以通過 hash table
進行快速的操作。比如 addObject:
,removeObject:
,containsObject:
都是按照 O(1) 來的。需要注意的是將陣列轉成 Set 時會將重複元素合成一個,同時失去排序。
加之 for 迴圈,可以得到複雜度計算結果。
7. 【在IM開發中】app 接收到一個message,上層UI重新整理一次,要求考慮到CPU和電量消耗,解決短時間內接收到很多條訊息的問題。怎麼解決?有幾種方案?【出題人:遠之²³³³-free zone-北】【 難度??】
方案一:利用聯結(在非同步執行緒上呼叫dispatch_source_merge_data
後,就會執行 dispatch source
事先定義好的handler
)、DISPATCH_SOURCE_TYPE_DATA_ADD
,將重新整理UI的工作拼接起來,短時間內做盡量少次數的重新整理。
方案二:自己實現佇列、確定一個合適的時間閾值,在閾值時間到達時、主動取訊息或者被動接受訊息,最後重新整理UI,達到訊息限流的作用。舉例:假設我們訊息的獲取都是通過長連線推送過來的,而不是主動拉取的。可以用訊息佇列來做,消費者定期去佇列取資料進行資料展示。或者假設前一條訊息和後一條訊息間隔只在0.2s以內,就可以認為是頻繁收到訊息。然後把這0.2s內的訊息重新整理相關操作,比如做個動畫效果。
8. 如圖label1在containerView上,containerView、label2在cell.contentView上問題:label1與label2的字數不固定,需求是,無論label2字數多少,label1都不能被拉伸或者壓縮:【 難度???】【出題人:記憶、擱淺】
效果圖見:
【答案】需要給label1設定一下優先順序,設定平行的的Content compression resistance priority。系統 Autolayout 參考 :Apple-Documentation-UIView-setContentHuggingPriority(_:for:)
Masonry 參考以下屬性:
static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;
複製程式碼
9.【計算機常識】如果你一直在用GitLab開發,現在公司要切換到GitHub開發,可以兩個郵箱不一樣,你自己的提交記錄,GitHub無法識別,簽到資料也沒了,請問如何讓GitHub能夠識別你整個倉庫中所有的提交記錄。【難度??】【出題人 微博@iOS程式犭袁】
【注】“簽到資料”指的是下圖:
【答案】參考: