iOS CallKit與PushKit的集成(一)

聲明一下,現(xiàn)在國區(qū)的App Store應(yīng)中國特色社會(huì)主義的要求吓肋,禁止上架有callkit功能的APP,已有的也要整改瑰艘,刪除callkit功能是鬼。

很多VoIP的開發(fā)者發(fā)現(xiàn),升級到Xcode9以后紫新,原來的Voice over IP的選項(xiàng)消失了均蜜,需要自行去info.plist中添加App provides Voice over IP services。


20180122153315445.png

隱藏了這個(gè)選項(xiàng)其實(shí)是為了強(qiáng)制大家使用CallKit+PushKit來做VoIP的應(yīng)用程序芒率。

我們的經(jīng)驗(yàn)是基于PushKit的VoIP應(yīng)用程序比那些使用傳統(tǒng)VoIP架構(gòu)的應(yīng)用程序更可靠囤耳,更省電。

具體來說偶芍,我們鼓勵(lì)VoIP應(yīng)用程序充分利用iOS 10 SDK中的新框架CallKit充择,從根本上改善了VoIP應(yīng)用程序的用戶體驗(yàn)。

另外匪蟀,請注意椎麦,macOS 10.12 Sierra不支持Xcode 7。

在某些時(shí)候材彪,對傳統(tǒng)VoIP架構(gòu)的支持將被刪除观挎,于是所有的VoIP應(yīng)用將不得不轉(zhuǎn)移到新的基于PushKit的VoIP架構(gòu)琴儿。

這里我就來簡單介紹一下如何集成CallKit與PushKit

要集成,首先就要導(dǎo)入framework嘁捷,圖中的三個(gè)framework都要導(dǎo)入造成,第一個(gè)framework是從通訊錄中直接撥打App電話所需要的。


A8E35734-9BDD-4921-A7E8-64E37AD3C407.png

PushKit

這個(gè)是iOS8后才支持的框架雄嚣,如果你的項(xiàng)目現(xiàn)在還在支持iOS7晒屎,那么你可以以辭職為籌碼去跟產(chǎn)品經(jīng)理斗智斗勇了。

集成PushKit很簡單缓升,跟注冊普通的APNS推送一個(gè)樣鼓鲁,先去注冊:

//import PushKit  這個(gè)加在文件頭部。大家都是老司機(jī)了仔沿,缺頭文件自己加。
let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]

然后注冊成功沒呢尺棋?看這個(gè)代理方法:

func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        if pushCredentials.token.count > 0 {
            var token = NSString(format: "%@", pushCredentials.token as CVarArg) as String
            print("pushRegistry credentialsToken \(token)")
        }
    }

大家注意了封锉,這里的token跟APNS的deviceToken雖然長度和格式一樣,但是內(nèi)容是不同的膘螟。這是因?yàn)樘O果需要區(qū)分這是PushKit的推送還是APNS的推送成福。

注冊好token后,就可以上傳給自己的服務(wù)器了荆残。然后需要自己的服務(wù)器發(fā)推送奴艾。
這里就牽扯到證書的問題了,首先要知道的是内斯,VoIP的PushKit推送證書跟APNS的是兩個(gè)不同的證書蕴潦,需要自己去生成,然后導(dǎo)出p12文件給服務(wù)器俘闯。


1870246-f767b26f3aceb124.png

導(dǎo)出證書這里就不做過多贅述潭苞,只要知道一點(diǎn),VoIP的PushKit證書只有Product環(huán)境的真朗,但是測試環(huán)境也能使此疹。


1870246-5d199f5d045e84c1.png

導(dǎo)出p12文件,注意導(dǎo)出的文件大小應(yīng)該有6kb遮婶,如果只有一半說明你沒把公鑰導(dǎo)進(jìn)去蝗碎。


1870246-e5d4fe2e73dfc69e.png

下面就可以測試推送啦。旗扑。蹦骑。
我們先來看看在哪里接推送,Appdelegate里面有這個(gè)方法:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
        guard type == .voIP else {
            log.info("Callkit& pushRegistry didReceiveIncomingPush But Not VoIP")
            return
        }
        log.info("pushRegistry didReceiveIncomingPush")
    }

這個(gè)方法里的PKPushPayload里有個(gè)dictionaryPayload臀防,是個(gè)字典脊串,作用跟APNS里的info一個(gè)樣辫呻。。琼锋。要學(xué)會(huì)舉一反三吶放闺。。

至此缕坎,一套PushKit的推送流程就搭建好了怖侦。。如果服務(wù)器沒搞好谜叹,但是想測試的話匾寝,可以用這個(gè):
https://github.com/noodlewerk/NWPusher
一個(gè)很牛逼的Push測試軟件。用的HTTP2荷腊,只要證書選對艳悔,token填對,就能發(fā)啦女仰。猜年。

CallKit

重點(diǎn)來了。疾忍。
對于CallKit首先要明確一點(diǎn)乔外。在你使用的時(shí)候,不要把他看成一個(gè)很復(fù)雜的框架一罩,他就是系統(tǒng)的打電話頁面杨幼,跟你自己寫的打電話頁面一樣一樣的;只要是頁面聂渊,就可以調(diào)用顯示和消失差购,可以對上面的按鈕進(jìn)行操作。

工欲善其事必先利其器汉嗽,我們首先來創(chuàng)建幾個(gè)工具類:
第一個(gè)歹撒,Call類,用來管理CallKit的電話诊胞,注意是管理CallKit的電話暖夭,跟你自己的電話邏輯不沖突!撵孤!

enum CallState { //狀態(tài)都能看得懂吧迈着。⌒奥耄看不懂的自己打個(gè)電話想想流程裕菠。
    case connecting
    case active
    case held
    case ended
    case muted
}

enum ConnectedState {
    case pending
    case complete
}

class Call {
  
    let uuid: UUID //來電的唯一標(biāo)識符
    let outgoing: Bool //是撥打的還是接聽的
    let handle: String //后面很多地方用得到,名字都是handle哈闭专,可以理解為電話號碼奴潘,其實(shí)就是自己App里被呼叫方的賬號(至少我們是這樣的)旧烧。。

    var state: CallState = .ended {
        didSet {
            stateChanged?()
        }
    }
  
    var connectedState: ConnectedState = .pending {
        didSet {
             connectedStateChanged?()
        }
    }
  
    var stateChanged: (() -> Void)?
    var connectedStateChanged: (() -> Void)?
  
    init(uuid: UUID, outgoing: Bool = false, handle: String) {
        self.uuid = uuid
        self.outgoing = outgoing
        self.handle = handle
    }
  
    func start(completion: ((_ success: Bool) -> Void)?) {
        completion?(true)

        DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 3) {
            self.state = .connecting
            self.connectedState = .pending
      
            DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) {
                self.state = .active
                self.connectedState = .complete
            }
        }
    }
  
    func answer() {
        state = .active
    }
  
    func end() {
        state = .ended
    }
}

然后建立一個(gè)Audio類画髓,用來管理音頻掘剪,鈴聲的播放。

func configureAudioSession() { //這里必須這么做奈虾。夺谁。不然會(huì)出現(xiàn)沒鈴聲的情況。原因嘛肉微。匾鸥。我也不知道。碉纳。
    log.info("Callkit& Configuring audio session")
    let session = AVAudioSession.sharedInstance()
    do {
        try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
        try session.setMode(AVAudioSessionModeVoiceChat)
    } catch (let error) {
        log.info("Callkit& Error while configuring audio session: \(error)")
    }
}

func startAudio() {
    log.info("Callkit& Starting audio")
    //開始播放鈴聲
}

func stopAudio() {
    log.info("Callkit& Stopping audio")
    //停止播放鈴聲
}

工具類都做好了勿负,下面開始集成CallKit~~~~~~~~~~~
首先,建立一個(gè)CallKitManager的類劳曹,只要是用戶發(fā)起的動(dòng)作奴愉,都跟這個(gè)類有關(guān)系。

@available(iOS 10.0, *)
class CallKitManager {
    
    static let shared = CallKitManager()
  
    var callsChangedHandler: (() -> Void)?

    private let callController = CXCallController()
    private(set) var calls = [Call]()
    
    private init(){}

    func callWithUUID(uuid: UUID) -> Call? {
        guard let index = calls.index(where: { $0.uuid == uuid }) else {
          return nil
        }
        return calls[index]
    }

    func add(call: Call) {
        calls.append(call)
        call.stateChanged = { [weak self] in
          guard let strongSelf = self else { return }
          strongSelf.callsChangedHandler?()
        }
        callsChangedHandler?()
    }

    func remove(call: Call) {
        guard let index = calls.index(where: { $0 === call }) else { return }
        calls.remove(at: index)
        callsChangedHandler?()
    }

    func removeAllCalls() {
        calls.removeAll()
        callsChangedHandler?()
    }
}

想必大家都發(fā)現(xiàn)了厚者,現(xiàn)在CallKitManager里面只有callController跟CallKit有關(guān)系躁劣,不急迫吐,我們一點(diǎn)一點(diǎn)的把這個(gè)類豐富起來库菲。這么做是為了加深理解,并不是簡單的復(fù)制代碼志膀,到時(shí)候出了問題知道在哪進(jìn)行改動(dòng)熙宇。

現(xiàn)在CallKitManager里面的函數(shù),其實(shí)是用了我們自己寫的Call類溉浙,對CallKit做一個(gè)邏輯的管理烫止,大家發(fā)現(xiàn)了,這里就跟隊(duì)列一個(gè)樣戳稽,add馆蠕、remove、removeAll惊奇、callWithUUID(根據(jù)uuid去找到這個(gè)call對象)互躬。

然后我們來看一下callController這個(gè)CXCallController對象,CallKitManager里面目前唯一與CallKit有關(guān)系就是他颂郎。CXCallController可以讓系統(tǒng)收到App的一些Request吼渡,用戶的action,App內(nèi)部的事件乓序。

我們現(xiàn)在來豐富CallKitManager寺酪,先從打電話開始:
添加下列代碼:

func startCall(handle: String, videoEnabled: Bool) {
        //一個(gè) CXHandle 對象表示了一次操作坎背,同時(shí)指定了操作的類型和值。App支持對電話號碼進(jìn)行操作寄雀,因此我們在操作中指定了電話號碼得滤。
        let handle = CXHandle(type: .phoneNumber, value: handle)
        //一個(gè) CXStartCallAction 用一個(gè) UUID 和一個(gè)操作作為輸入。
        let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
        //你可以通過 action 的 isVideo 屬性指定通話是音頻還是視頻咙俩。
        startCallAction.isVideo = videoEnabled
        let transaction = CXTransaction(action: startCallAction)
        requestTransaction(transaction)
    }

//調(diào)用 callController 的 request(_:completion:) 耿戚。系統(tǒng)會(huì)請求 CXProvider 執(zhí)行這個(gè) CXTransaction,這會(huì)導(dǎo)致你實(shí)現(xiàn)的委托方法被調(diào)用阿趁。
    private func requestTransaction(_ transaction: CXTransaction) {
        callController.request(transaction) { error in
            if let error = error {
                log.info("Callkit& Error requesting transaction: \(error)")
            } else {
                log.info("Callkit& Requested transaction successfully")
            }
        }
    }

是不是迫不及待的想調(diào)用一下這個(gè)函數(shù)了膜蛔?但是調(diào)用后發(fā)現(xiàn),并沒有什么事情發(fā)生脖阵。皂股。
其實(shí)就是這樣。命黔。因?yàn)槟阒幌蛳到y(tǒng)發(fā)送了要打電話的請求呜呐,但是系統(tǒng)也要告訴你你現(xiàn)在可不可以打,這樣才叫與系統(tǒng)通訊嘛悍募。蘑辑。不能只是單方面的要求,還需要對方的應(yīng)答坠宴。這里其實(shí)就跟服務(wù)器請求一個(gè)樣洋魂,發(fā)要求,等回應(yīng)喜鼓,收到回應(yīng)后進(jìn)行下一步操作副砍。

那么這里,我們就需要來接收系統(tǒng)的回應(yīng)了庄岖。豁翎。怎么接收到呢?
我們新建一個(gè)類隅忿,名字叫ProviderDelegate心剥,繼承自誰不重要,重要的是需要遵循CXProviderDelegate這個(gè)代理背桐。

@available(iOS 10.0, *)
class ProviderDelegate: NSObject, CXProviderDelegate {
    static let shared = ProviderDelegate()
    //ProviderDelegate 需要和 CXProvider 和 CXCallController 打交道优烧,因此保持兩個(gè)對二者的引用。
    private let callManager: CallKitManager //還記得他里面有個(gè)callController嘛牢撼。匙隔。
    private let provider: CXProvider
    
    override init() {
        self.callManager = CallKitManager.shared
        //用一個(gè) CXProviderConfiguration 初始化 CXProvider,前者在后面會(huì)定義成一個(gè)靜態(tài)屬性。CXProviderConfiguration 用于定義通話的行為和能力纷责。
        provider = CXProvider(configuration: type(of: self).providerConfiguration)
        super.init()
        //為了能夠響應(yīng)來自于 CXProvider 的事件捍掺,你需要設(shè)置它的委托。
        provider.setDelegate(self, queue: nil)
    }
    
    //通過設(shè)置CXProviderConfiguration來支持視頻通話再膳、電話號碼處理挺勿,并將通話群組的數(shù)字限制為 1 個(gè),其實(shí)光看屬性名大家也能看得懂吧喂柒。
    static var providerConfiguration: CXProviderConfiguration {
        let providerConfiguration = CXProviderConfiguration(localizedName: "Mata Chat")//這里填你App的名字哦不瓶。。
        providerConfiguration.supportsVideo = false
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.maximumCallGroups = 1
        providerConfiguration.supportedHandleTypes = [.phoneNumber]
        return providerConfiguration
    }
    
    //這個(gè)方法牛逼了灾杰,它是用來更新系統(tǒng)電話屬性的蚊丐。。
    func callUpdate(handle: String, hasVideo: Bool) -> CXCallUpdate {
        let update = CXCallUpdate()
        update.localizedCallerName = "ParadiseDuo"http://這里是系統(tǒng)通話記錄里顯示的聯(lián)系人名稱哦艳吠。需要顯示什么按照你們的業(yè)務(wù)邏輯來麦备。
        update.supportsGrouping = false
        update.supportsHolding = false
        update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) //填了聯(lián)系人的名字,怎么能不填他的handle('電話號碼')呢昭娩,具體填什么凛篙,根據(jù)你們的業(yè)務(wù)邏輯來
        update.hasVideo = hasVideo
        return update
    }

    //CXProviderDelegate 唯一一個(gè)必須實(shí)現(xiàn)的代理方法!栏渺!當(dāng) CXProvider 被 reset 時(shí)呛梆,這個(gè)方法被調(diào)用,這樣你的 App 就可以清空所有去電磕诊,會(huì)到干凈的狀態(tài)填物。在這個(gè)方法中,你會(huì)停止所有的呼出音頻會(huì)話秀仲,然后拋棄所有激活的通話融痛。
    func providerDidReset(_ provider: CXProvider) {
        stopAudio()
        for call in callManager.calls {
            call.end()
        }
        callManager.removeAllCalls()
        //這里添加你們掛斷電話或拋棄所有激活的通話的代碼壶笼。神僵。
    }
}

上面的ProviderDelegate準(zhǔn)備工作做好后,繼續(xù)我們打電話的邏輯覆劈,在ProviderDelegate添加代理方法:

func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
        //向系統(tǒng)通訊錄更新通話記錄
        let update = self.callUpdate(handle: action.handle.value, hasVideo: action.isVideo)
        provider.reportCall(with: action.callUUID, updated: update)
        
        let call = Call(uuid: action.callUUID, outgoing: true, handle: action.handle.value)
        //當(dāng)我們用 UUID 創(chuàng)建出 Call 對象之后保礼,我們就應(yīng)該去配置 App 的音頻會(huì)話。和呼入通話一樣责语,你的唯一任務(wù)就是配置炮障。真正的處理在后面進(jìn)行,也就是在 provider(_:didActivate) 委托方法被調(diào)用時(shí)
        configureAudioSession()
        //delegate 會(huì)監(jiān)聽通話的生命周期坤候。它首先會(huì)會(huì)報(bào)告的就是呼出通話開始連接胁赢。當(dāng)通話最終連上時(shí),delegate 也會(huì)被通知白筹。
        call.connectedStateChanged = { [weak self] in
            guard let w = self else {
                return
            }
            if call.connectedState == .pending {
                w.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
            } else if call.connectedState == .complete {
                w.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
            }
        }
        //調(diào)用 call.start() 方法會(huì)導(dǎo)致 call 的生命周期變化智末。如果連接成功谅摄,則標(biāo)記 action 為 fullfill。
        call.start { [weak self] (success) in
            guard let w = self else {
                return
            }
            if success {
               //這里填寫你們App內(nèi)打電話的邏輯系馆。送漠。
  
                w.callManager.add(call: call)
                //所有的Action只有調(diào)用了fulfill()之后才算執(zhí)行完畢。
                action.fulfill()
            } else {
                action.fail()
            }
        }
    }

//當(dāng)系統(tǒng)激活 CXProvider 的 audio session時(shí)由蘑,委托會(huì)被調(diào)用闽寡。這給你一個(gè)機(jī)會(huì)開始處理通話的音頻。
    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        startAudio() //一定要記得播放鈴聲吶尼酿。爷狈。
    }

至此,通過CallKit撥打電話的邏輯就完成了裳擎。你只要在自己App需要打電話的地方淆院,調(diào)用
CallKitManager.shared.startCall(handle: userName, videoEnabled: false)就行啦。句惯。但是有一點(diǎn)需要注意土辩,CallKit只有iOS 10以上支持,所以iOS 10以下的手機(jī)還是要支持你們原來打電話的邏輯抢野,像這樣:

if #available(iOS 10.0, *) {
       CallKitManager.shared.startCall(handle:userName, videoEnabled: false)
} else {
      //原來打電話的邏輯
}

然后當(dāng)你興沖沖的去用CallKit打電話的時(shí)候拷淘,卻發(fā)現(xiàn)彈出的是自己的通話頁面。指孤。启涯。T_T
但是此時(shí)你查看系統(tǒng)的通話記錄,應(yīng)該會(huì)發(fā)現(xiàn)通話記錄里面新增了一條從自己App打出去的記錄恃轩。這樣就說明CallKit撥打電話接入成功了结洼!

因?yàn)閮?nèi)容較多,分成了三篇文章叉跛,下一篇講如何接電話松忍,繼續(xù)完善這篇文章的代碼。
iOS CallKit與PushKit的集成(二)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筷厘,一起剝皮案震驚了整個(gè)濱河市鸣峭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌酥艳,老刑警劉巖摊溶,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異充石,居然都是意外死亡莫换,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拉岁,“玉大人溃列,你說我怎么就攤上這事√叛Γ” “怎么了听隐?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哄啄。 經(jīng)常有香客問我雅任,道長,這世上最難降的妖魔是什么咨跌? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任沪么,我火速辦了婚禮,結(jié)果婚禮上锌半,老公的妹妹穿的比我還像新娘禽车。我一直安慰自己,他們只是感情好刊殉,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布殉摔。 她就那樣靜靜地躺著,像睡著了一般记焊。 火紅的嫁衣襯著肌膚如雪逸月。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天遍膜,我揣著相機(jī)與錄音碗硬,去河邊找鬼。 笑死瓢颅,一個(gè)胖子當(dāng)著我的面吹牛恩尾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挽懦,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼翰意,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了巾兆?” 一聲冷哼從身側(cè)響起猎物,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤虎囚,失蹤者是張志新(化名)和其女友劉穎角塑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淘讥,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡圃伶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窒朋。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搀罢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侥猩,到底是詐尸還是另有隱情榔至,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布欺劳,位于F島的核電站唧取,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏划提。R本人自食惡果不足惜枫弟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鹏往。 院中可真熱鬧淡诗,春花似錦、人聲如沸伊履。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽唐瀑。三九已至宙攻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間介褥,已是汗流浹背座掘。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柔滔,地道東北人溢陪。 一個(gè)月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像睛廊,于是被迫代替她去往敵國和親形真。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言 現(xiàn)在第三方推送也很多 超全,比如極光咆霜,融云,信鴿嘶朱,其原理也是相同利用APNS推送機(jī)制 蛾坯,公司讓做自己的推送。避免...
    修_遠(yuǎn)閱讀 10,845評論 9 19
  • 概述 在多數(shù)移動(dòng)應(yīng)用中任何時(shí)候都只能有一個(gè)應(yīng)用程序處于活躍狀態(tài)疏遏,如果其他應(yīng)用此刻發(fā)生了一些用戶感興趣的那么通過通知...
    莫離_焱閱讀 6,517評論 1 8
  • 前言: APNs 協(xié)議在近兩年的 WWDC 上改過兩次, 15 年 12 月 17 日更是推出了革命性的新特性倘零。但...
    iOS程序犭袁閱讀 28,662評論 16 300
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,262評論 25 707
  • 忘了哪一天唱遭, tanya蔡健雅的聲音侵入腦髓 ,印象中她的面孔總是模糊呈驶,影像里總是把她和阿桑拷泽,許美靜聯(lián)系在一起,她...
    素小白1024閱讀 408評論 1 2