前言
最近需要實現(xiàn)一個新需求,用iOS 10出的CallKit實現(xiàn)將APP的通訊錄的信息同步到系統(tǒng)中,可以不把人員信息加到通訊錄中,實現(xiàn)來電號碼識別。這個功能在xx安全衛(wèi)士、xx管家中很早就實現(xiàn)了,但是網(wǎng)上相關(guān)的資料較少,而且官方的文檔寫的太簡單了,很多坑還要自己去摸索。于是記錄一下和各位分享,如有錯誤之處請各位指出!
PS: 先說個題外話吧,CallKit功能在iOS 10的時候還不太穩(wěn)定,iOS 10剛出來的時候為了體驗騷擾攔截功能,手賤裝了兩個不同的攔截APP,然后就悲劇了。盜一張網(wǎng)上的圖:

然后各種重啟、重裝APP都沒有用,寫的Demo也跑不起來,唯一的辦法只有重置系統(tǒng)。說多了都是淚!
一、Call Directory app extension
實現(xiàn)來電識別、來電攔截功能需要使用CallKit當中的Call Directory app extension,首先,需要了解extension。關(guān)于extension網(wǎng)上有很多教程,這里就不細說了。推薦兩篇文章,英文好的推薦看官方文檔,還有一篇中文博客。
使用Call Directory Extension主要需要和3個類打交道,分別是
CXCallDirectoryProvider、CXCallDirectoryExtensionContext、CXCallDirectoryManager。

本文涉及的Demo。
CXCallDirectoryProvider
官方文檔:The principal object for a Call Directory app extension for a host app.
正如官方文檔所說,這是Call Directory app extension最重要的一個類。
用系統(tǒng)模板新建Call Directory Extension之后會自動生成一個類,繼承自CXCallDirectoryProvider。入口方法:
// 有兩種情況改方法會被調(diào)用
// 1.第一次打開設(shè)置-電話-來電阻止與身份識別開關(guān)時,系統(tǒng)自動調(diào)用
// 2.調(diào)用CXCallDirectoryManager的reloadExtensionWithIdentifier方法會調(diào)用
- (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context {
context.delegate = self;
// 添加號碼識別信息與號碼攔截列表
[self addIdentificationPhoneNumbersToContext:context];
[context completeRequestWithCompletionHandler:nil];
}
CXCallDirectoryExtensionContext
官方文檔:A programmatic interface for adding identification and blocking entries to a Call Directory app extension.
CXCallDirectoryExtensionContext objects are not initialized directly, but are instead passed as arguments to the CXCallDirectoryProvider instance method beginRequestWithExtensionContext:.
大致意思就是說,這是一個為Call Directory app extension添加號碼識別、號碼攔截的入口。CXCallDirectoryExtensionContext不需要自己初始化,它會作為CXCallDirectoryProvider的beginRequestWithExtensionContext函數(shù)的參數(shù)傳遞給使用者。
它的主要方法有兩個:
// 設(shè)置號碼識別信息
- (void)addIdentificationEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber label:(NSString *)label;
// 設(shè)置號碼攔截列表
- (void)addBlockingEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber;
在設(shè)置時候要注意:
- 號碼不能重復(fù),不然會報錯
CXErrorCodeCallDirectoryManagerErrorDuplicateEntries- 號碼必須按照升序?qū)懭耄蝗粫箦e
CXErrorCodeCallDirectoryManagerErrorEntriesOutOfOrder- 號碼必須格式化后傳入,手機號碼必須加上國家碼,例如18012341234就不行,需要加上86,構(gòu)造成8618012341234;固話需要格式為:國家碼+區(qū)號(去掉第一個0)+號碼,例如010-61001234格式化之后為,861061001234。如果號碼格式錯誤,會導(dǎo)致識別不出來。
- 上限數(shù)據(jù)是200萬(在其它文章里看到的,然后自己測試了下,構(gòu)造了200萬條數(shù)據(jù)寫入的時候會報錯
CXErrorCodeCallDirectoryManagerErrorMaximumEntriesExceeded,150萬條數(shù)據(jù)是OK的,所以這個數(shù)據(jù)上限一定要注意。實測安裝了XX安全衛(wèi)士、XX管家實現(xiàn)騷擾電話攔截用了3個extension,可能數(shù)據(jù)量太大就是一個原因。)- 在用戶第一次打開設(shè)置時,會調(diào)用
beginRequestWithExtensionContext,這時候不宜寫太多數(shù)據(jù),不然會卡在設(shè)置那里轉(zhuǎn)圈,用戶體驗很差??梢韵葘懖糠謹?shù)據(jù),然后回到主APP了調(diào)用reloadExtensionWithIdentifier去刷新。
CXCallDirectoryManager
官方文檔:The programmatic interface to an object that manages a Call Directory app extension.
CXCallDirectoryManager主要作用是管理Call Directory app extension。
有兩個方法:
// 重新設(shè)置號碼識別、電話攔截列表
// 調(diào)用該方法后會重置之前設(shè)置的列表,然后調(diào)用beginRequestWithExtensionContext:
- (void)reloadExtensionWithIdentifier:(NSString *)identifier completionHandler:(nullable void (^)(NSError *_Nullable error))completion;
// 獲取extension是否可用,需要在“設(shè)置-電話-來電阻止與身份識別"中開啟權(quán)限
- (void)getEnabledStatusForExtensionWithIdentifier:(NSString *)identifier completionHandler:(void (^)(CXCallDirectoryEnabledStatus enabledStatus, NSError *_Nullable error))completion;
二、實戰(zhàn)
先上Demo地址。下面會一步步講解。
創(chuàng)建extension
新建一個Target(File-New-Target)。

會自動建立一個目錄,默認有三個文件。在.m文件中有系統(tǒng)給出的示例代碼

我們來看看系統(tǒng)的模板代碼,首先是入口函數(shù)
- (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context {
context.delegate = self;
if (context.isIncremental) {
[self addOrRemoveIncrementalBlockingPhoneNumbersToContext:context];
[self addOrRemoveIncrementalIdentificationPhoneNumbersToContext:context];
} else {
[self addAllBlockingPhoneNumbersToContext:context];
[self addAllIdentificationPhoneNumbersToContext:context];
}
[context completeRequestWithCompletionHandler:nil];
}
CallDirectoryHandler
我在Xcode 9生成的代碼,context.isIncremental是iOS 11才增加的,還有所有的remove的方法也是iOS 11才有的,為了適配iOS 10,還是不推薦使用。
系統(tǒng)模板代碼大致邏輯就是,先添加號碼識別、號碼攔截記錄,添加完成后調(diào)用completeRequestWithCompletionHandler:完成整個過程。
由于號碼攔截比較簡單,只是寫入一個號碼的數(shù)組,本文就以號碼識別為例,號碼識別方法系統(tǒng)模板這么寫的:
- (void)addAllIdentificationPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context {
CXCallDirectoryPhoneNumber allPhoneNumbers[] = { 8618788888888, 8618885555555 };
NSArray<NSString *> *labels = @[ @"送餐電話", @"詐騙電話" ];
NSUInteger count = (sizeof(allPhoneNumbers) / sizeof(CXCallDirectoryPhoneNumber));
for (NSUInteger i = 0; i < count; i += 1) {
CXCallDirectoryPhoneNumber phoneNumber = allPhoneNumbers[i];
NSString *label = labels[i];
[context addIdentificationEntryWithNextSequentialPhoneNumber:phoneNumber label:label];
}
}
這么多代碼,核心就是一行[context addIdentificationEntryWithNextSequentialPhoneNumber:phoneNumber label:label];,注意phoneNumber是CXCallDirectoryPhoneNumber類型,其實就是long long類型。
在這個函數(shù)里,需要把需要識別的號碼和識別信息,一條一條的寫入
檢查授權(quán)
開啟extension功能需要在“設(shè)置-電話-來電阻止與身份識別”中開啟,我們在寫入數(shù)據(jù)時第一步是引導(dǎo)用戶給我們的extension授權(quán)。
CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
[manage
getEnabledStatusForExtensionWithIdentifier:self.externsionIdentifier
completionHandler:^(CXCallDirectoryEnabledStatus enabledStatus, NSError * _Nullable error) {
// 根據(jù)error,enabledStatus判斷授權(quán)情況
// error == nil && enabledStatus == CXCallDirectoryEnabledStatusEnabled 說明可用
// error 見 CXErrorCodeCallDirectoryManagerError
// enabledStatus 見 CXCallDirectoryEnabledStatus
}];
寫入數(shù)據(jù)
用戶在設(shè)置開啟后,調(diào)用reloadExtensionWithIdentifier即可觸發(fā)CallDirectoryHandler更新數(shù)據(jù)邏輯。
CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
[manager reloadExtensionWithIdentifier:self.externsionIdentifier completionHandler:^(NSError * _Nullable error) {
// error 見 CXErrorCodeCallDirectoryManagerError
}];
驗證
接下來在真機下跑下(一定要在插了電話卡的iPhone上調(diào)試,模擬器不行?。?,寫入成功后,打開電話,撥號18788888888,提示”送餐電話”。說明寫入成功!

三、extension和containing app數(shù)據(jù)共享
上面的步驟中,號碼信息是寫死在代碼中的,在實際應(yīng)用中這些號碼信息肯定不是寫死的,一般需要從服務(wù)器獲取。這就需要我們的APP與extension進行通信,需要用到APP Groups,怎么用網(wǎng)上有很多文章了,我就不多說了,推薦一篇。
其實本質(zhì)就是通過APP Groups,開辟一片空間,extension和containing app都可以訪問,然后我們的APP就可以通過NSUserDefaults、文件、數(shù)據(jù)庫等方式共享數(shù)據(jù)給extension了。前期我使用過NSUserDefaults,效率很低,大概在5萬數(shù)據(jù)的時候就爆內(nèi)存了,使用extension一定要注意內(nèi)存,不然很容易被系統(tǒng)干掉,所以不推薦使用這種方式。
Demo中采用的是讀寫文件的方式,大致思路(具體實現(xiàn)看Demo):
- 在APP中把數(shù)據(jù)序列化之后寫到一個文件中
- 在extension中讀取這個文件,讀取一行,調(diào)用一次
addIdentificationEntryWithNextSequentialPhoneNumber,然后及時釋放
這種方式理論上是可以達到最大限制200w條的(實際測試150萬沒有問題)。
獲取APP Groups文件路徑
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:self.groupIdentifier];
containerURL = [containerURL URLByAppendingPathComponent:@"CallDirectoryData"];
NSString* filePath = containerURL.path;
進度監(jiān)控
在xx安全衛(wèi)士中,開啟騷擾電話攔截功能有一個進度條,非常的直觀。但是在extension中是沒法更新UI的,有一種實現(xiàn)方式,可以用開源框架MMWormhole來實現(xiàn)APP與extension通信,然后把進度從extension傳到APP中,在APP中更新進度條。理論上該方案是可行的,感興趣的同學(xué)可以嘗試下。
關(guān)于上架
在加上CallKit第一次上架時,收到了蘋果的拒信,說CallKit在中國區(qū)被禁止了。

后來觀察了下xx安全衛(wèi)士一直在正常更新,猜想蘋果只是禁止了CallKit關(guān)于VOIP這部分功能。
遇到蘋果的拒信不要慌,錄制一個來電識別的功能演示視頻,放到Y(jié)outube,然后在備注里面解釋一下,就可以通過審核了。
給一個審核備注模板:
關(guān)于CallKit,我們遵守中國的法律,沒有使用VOIP,我們僅使用了Call Directory。我們應(yīng)用內(nèi)有一個公司內(nèi)部通訊錄,為了增加用戶員工溝通效率,我們在電話頁面自動識別內(nèi)部通訊錄的人員 ,并展示姓名與崗位。我們錄制了一個演示視頻:視頻地址
- 參考鏈接
https://developer.apple.com/documentation/callkit
https://developer.apple.com/videos/play/wwdc2016/230/
https://colin1994.github.io/2016/06/17/Call-Directory-Extension-Study/
https://yunissong.github.io/2017/03/29/CallKit/
http://www.itdecent.cn/p/7f88cbe7948c
https://www.raywenderlich.com/150015/callkit-tutorial-ios
歡迎關(guān)注我的博客