iOS Apple Push Notification Service (APNs)

今天看了一篇有關(guān)iOS消息推送的文章 原文

開發(fā)環(huán)境:
** Xcode 8.3
Swift 3.1 **

隨著iOS版本的不斷提高脱货,iOS的消息推送也越來越強大蹭劈,也越容易上手了。 在iOS10中缓淹,消息推送的功能有:

  • 顯示文本信息
  • 播放通知聲音
  • 設(shè)置 badge number
  • 用戶不打開應(yīng)用的情況下提供actions(下拉推送消息即可看到)
  • 顯示一個媒體信息
  • 在 silent 的情況讓應(yīng)用在后臺執(zhí)行一些任務(wù)

測試iOS APNs 的時候磁滚,需要用到的:

  • 一臺iOS設(shè)備,因為在模擬器是不行的
  • 一個開發(fā)者賬號(配置APNs的時候需要證書)

在本文中,使用 Pusher 扮演向iOS設(shè)備推送消息的服務(wù)器的角色氛改,在本文的測試中你可以 直接下載Pusher

iOS的消息推送中胜卤,有三個主要步驟:

  1. app配置注冊APNS
  2. 一個Server推送消息給設(shè)備
  3. app接送處理APNs

13 主要是iOS開發(fā)者干的事情葛躏, 2 是消息推送服務(wù)端舰攒,國內(nèi)可以用一些第三方的比如 極光推送之類的芒率,當然公司也可以自己配制偶芍。

開始之前匪蟀,下載初始化的項目 starter project, 打開運行材彪,如圖所示:

initial_list-281x500.png

配置App

  • 打開應(yīng)用,在 General 改寫 Bundler Identifier 一個唯一的標示 如: com.xxxx.yyyy
Screen-Shot-2017-05-15-at-23.05.39-1-650x163.png
  • 選擇一個開發(fā)者賬號
  • Capabilities打開APNS:
screen_capabilities-480x97.png
注冊APNs

AppDelegate.swift頂部導(dǎo)入:

import UserNotifications

添加方法:

    func resgierForPushNotifications() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
            print("Permision granted: \(granted)")
        }
    }

在方法 application(_:didFinishLaunchingWithOptions:):中調(diào)用 registerForPushNotifications()

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        UITabBar.appearance().barTintColor = UIColor.themeGreenColor
        UITabBar.appearance().tintColor = UIColor.white
        
        
        resgierForPushNotifications()
        
        return true
    }

** UNUserNotificationCenter**是在iOS10才又的晒屎,它的作用主要是在App內(nèi)管理所有的通知活動

**requestAuthorization(options:completionHandler:) **認證APNs,指定通知類型港谊,通知類型有:

  • .badge 在App顯示消息數(shù)
  • .sound 允許App播放聲音
  • ** .alert** 推送通知文本
  • .carPlay CarPlay環(huán)境下的消息推送

運行項目歧寺,可看到:

IMG_7303-281x500.png

點擊 Allow 運行推送

AppDelegate:中添加方法:

    func getNotificationSettings() {
        UNUserNotificationCenter.current().getNotificationSettings { (settings) in
            print("Notification setting: \(settings)")
        }
    }

這個方法主要是查看用戶允許的消息推送類型,因為用戶可以拒絕消息推送奴艾,也可以在手機的設(shè)置中更改推送類型蕴潦。

在 ** requestAuthorization **中調(diào)用 getNotificationSettings()

    func resgierForPushNotifications() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
            print("Permision granted: \(granted)")
            
            guard granted else { return }
            self.getNotificationSettings()
        }
    }

更新 getNotificationSettings()方法 如下:

    func getNotificationSettings() {
        UNUserNotificationCenter.current().getNotificationSettings { (settings) in
            print("Notification setting: \(settings)")
            
            guard settings.authorizationStatus == .authorized else { return }
            UIApplication.shared.registerForRemoteNotifications()
        }
    }

** settings.authorizationStatus == .authorized** 表明用戶允許推送,** UIApplication.shared.registerForRemoteNotifications()**此疹,實際注冊APNs

添加下面兩個方法蝗碎,它們會被調(diào)用蹦骑,顯示注冊結(jié)果:

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { data -> String in
            return String(format: "%02.2hhx", data)
        }
        
        let token = tokenParts.joined()
        print("Device Token: \(token)")
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register: \(error)")
    }

如果注冊成功,調(diào)用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**

注冊失敗袱衷,調(diào)用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**

在 application(_:didRegisterForRemoteNotificationsWithDeviceToken:)** 方法內(nèi)做的事情只是簡單的把 Data 轉(zhuǎn)換為 String

(一般情況如果注冊失敗可能是:在模擬器運行 或者 App ID 配置失敗致燥,所以輸出錯誤信息查看原因就很重要了 )

編譯運行谜叹,可看到輸出一個類似于這樣的字符串:

screen_device_token-480x20.png

把輸出的 Device Token 拷貝出來荷腊,放到一個地方女仰,等一下配置的時候需要這貨

創(chuàng)建 SSL 證書 和 PEM 文件

在蘋果官網(wǎng)的開發(fā)者賬號中 步驟:Certificates, IDs & Profiles -> Identifiers -> App IDs 在該應(yīng)用的 ** App IDs**中應(yīng)該可以看到這樣的信息:

screen_configurable_notifications-480x31.png

點擊 Edit 下拉到 Push Notifications:

screen_create_cert-650x410.png

Development SSL Certificate, 點擊 **Create Certificate… **跟隨步驟創(chuàng)建證書床三,最后下載證書,雙擊證書, 證書會添加到 Keychain

screen_keychain-650x51.png

回到開發(fā)者賬號, 在應(yīng)用到 **App ID ** 應(yīng)用可以看到:

screen_enabled-480x30.png

到這一步就OK了撇簿,你已經(jīng)有了APNs 的證書了

推送消息

還記得剛才下載的** Pusher **嗎聂渊? 用那貨來發(fā)送推送消息
打開 Pusher,完成以下步驟:

  • Pusher 中選擇剛剛生成的證書
  • 把剛剛生成的 Device Token 拷貝進去 (如果你忘了拷貝生成的 Device Token四瘫,把應(yīng)用刪除汉嗽,重新運行,拷貝即可)
  • 修改 Pusher 內(nèi)的消息體如下:
{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com"
  }
}
  • 應(yīng)用推到后臺找蜜,或者鎖定
  • Pusher 中點擊 Push 按鈕
Screen-Shot-2017-04-30-at-13.02.25-650x312.png

你的應(yīng)用應(yīng)該可以接受到首條推送消息:

IMG_7304-281x500.png

(如果你的應(yīng)用在前臺,你是收不到推送的洗做,推到后臺弓叛,重新發(fā)送消息)

推送的一些問題

一些消息推送接收不到: 如果你同時發(fā)送多條推送,而只有一些收到诚纸,很正常邪码!APNS 為每一個設(shè)備的App維護一個 QoS (Quality of Service) 隊列. 隊列的size是1,所以如果你同時發(fā)送多條推送咬清,最后一條推送是會被覆蓋的

連接 Push Notification Service 有問題: 一種情況是你用的 ports 被防火墻墻了闭专,另一種情況可能是你的APNs證書有錯誤

基本推送消息的結(jié)構(gòu)

更新中...

在目前的推送測試中,消息體是這樣的:

{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com"
  }
}

下面來分析一下

"aps" 這個key 的value中旧烧,可以添加 6 個key

  • alert. 可以是字符串影钉,亦可以是字典(如果是字典,你可以本地化文本或者修改一下通知的其它內(nèi)容)
  • badge. 通知數(shù)目
  • thread-id. 整合多個通知
  • sound. 通知的聲音掘剪,可以是默認的平委,也可以是自定義的,自定義需要少于30秒和一些小限制
  • content-availabel. 設(shè)置value 為 1 的時候夺谁, 該消息會成為 silent 的模式廉赔。下午會提及
  • category. 主要和 custom actions 相關(guān)肉微,下午會有介紹

記得 payload 最大的為 4096 比特

處理推送消息

處理推送過來的消息(使用 actions 或者 直接點擊消息 )

當你接收了一個推送消息的時候會發(fā)生什么

當接收到推送消息的時候, UIApplicationDelegate 內(nèi)的代理方法會被調(diào)用蜡塌,調(diào)用的情況取決于目前 app 的狀態(tài)

  • 如果 app 沒運行碉纳,用戶點擊了推送消息,則推送消息會傳遞給 ** application(_:didFinishLaunchingWithOptions:).** 方法
  • 如果 app 在前臺或者后臺則 ** application(_:didReceiveRemoteNotification:fetchCompletionHandler:) ** 方法被調(diào)用馏艾。如果用戶通過點擊推送消息的方式打開 app 劳曹, 則 該方法可能會被再次調(diào)用,所以你可以依次更新一些UI信息或數(shù)據(jù)

處理 app 沒運行的消息推送情況

在** application(_:didFinishLaunchingWithOptions:).** 方法的 return 返回前加入:

        if let notification = launchOptions?[.remoteNotification] as? [String: AnyObject] {
            if let aps = notification["aps"] as? [String: AnyObject] {
                _ = NewsItem.makeNewsItem(aps)
                (window?.rootViewController as? UITabBarController)?.selectedIndex = 1
            }
        }

它會檢測 ** UIApplicationLaunchOptionsKey.remoteNotification.** 是否存在于 ** launchOptions **琅摩,編譯運行铁孵,把應(yīng)用結(jié)束運行關(guān)掉。用 Pusher發(fā)生一條推送消息房资,點擊推送消息蜕劝,應(yīng)該會顯示如圖:

IMG_7306-281x500.png

(如果沒有接收到推送消息,可能是你的設(shè)備的 device token 改變了轰异,在未安裝 app 或者重新安裝 app 的情況下熙宇,device token 會改變)

處理 app 運行在前臺或者后臺的消息推送

application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法 更新如下

func application(
  _ application: UIApplication,
  didReceiveRemoteNotification userInfo: [AnyHashable : Any],
  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
  
  let aps = userInfo["aps"] as! [String: AnyObject]
  _ = NewsItem.makeNewsItem(aps)
}

方法的做的主要是把推送消息直接加入 NewsItem (把推送消息顯示在視圖內(nèi)),編譯運行溉浙,保持應(yīng)用在前臺或者后臺烫止,發(fā)生推送消息,顯示如下:

IMG_7308-281x500.png

好了戳稽,現(xiàn)在 app 能給接收推送消息了馆蠕!

為 “推送通知“ 添加 Actions

為 “推送通知“ 添加 Actions 可以為消息添加一些自定義的按鈕。actions 的添加通過在 app 內(nèi)為通知注冊 ** categories** 惊奇,每一個 category 可以有自己的 action互躬。一旦注冊,推送的服務(wù)端可以設(shè)置消息的 category颂郎。

在示例中吼渡,會定義一個名為 ** News** category 和一個相對應(yīng)的名為 View 的 action,這個 action 會讓用戶選擇這個 action 后直接打開文章

AppDelegate 內(nèi)乓序,替換 **registerForPushNotifications() ** 如下

func registerForPushNotifications() {
  UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
    (granted, error) in      
    print("Permission granted: \(granted)")
    
    guard granted else { return }
    
    // 1
    let viewAction = UNNotificationAction(identifier: viewActionIdentifier,
                                          title: "View",
                                          options: [.foreground])
    
    // 2
    let newsCategory = UNNotificationCategory(identifier: newsCategoryIdentifier,
                                              actions: [viewAction],
                                              intentIdentifiers: [],
                                              options: [])
    // 3
    UNUserNotificationCenter.current().setNotificationCategories([newsCategory])
    
    self.getNotificationSettings()
  }
}

代碼干的事為:

// 1. 創(chuàng)建一個新的 notification action, 按鈕標題為 View , 當觸發(fā)時寺酪,app 在 foreground(前臺)打開. 這個 ation 有一個 identifier, 這個 identifier 是用來標示同一個 category 內(nèi)的不同 action 的

// 2. 定義一個新的 category . 包含剛剛創(chuàng)建的 action替劈, 有一個自己的 identifier

// 3. 通過 ** setNotificationCategories(_:) ** 方法注冊 category.

編譯運行 app寄雀。

替換 Pusher 內(nèi)的推送消息如下:

{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com",
    "category": "NEWS_CATEGORY"
  }
}

如果一切運行正常,下拉推送消息陨献,顯示如下:


IMG_7309-281x500.png

nice, 點擊 View, 打開 app盒犹,但是什么都沒有發(fā)生,你還需要實現(xiàn)一些代理方法來處理 action

處理通知的 Actions

當 actions 被觸發(fā)的時候, UNUserNotificationCenter 會通知它的 delegate急膀。在 AppDelegate.swift 內(nèi)添加如下 extension

extension AppDelegate: UNUserNotificationCenterDelegate {
  
  func userNotificationCenter(_ center: UNUserNotificationCenter,
                              didReceive response: UNNotificationResponse,
                              withCompletionHandler completionHandler: @escaping () -> Void) {
    // 1
    let userInfo = response.notification.request.content.userInfo
    let aps = userInfo["aps"] as! [String: AnyObject]
    
    // 2
    if let newsItem = NewsItem.makeNewsItem(aps) {
      (window?.rootViewController as? UITabBarController)?.selectedIndex = 1
      
      // 3
      if response.actionIdentifier == viewActionIdentifier,
        let url = URL(string: newsItem.link) {
        let safari = SFSafariViewController(url: url)
        window?.rootViewController?.present(safari, animated: true, completion: nil)
      }
    }
    
    // 4
    completionHandler()
  }
}

方法代碼主要做的是判斷 action 的 identifier沮协, 打開推送過來的 url。

application(_:didFinishLaunchingWithOptions:): 方法內(nèi)卓嫂,設(shè)置 ** UNUserNotificationCenter** 的代理

UNUserNotificationCenter.current().delegate = self

編譯運行慷暂,關(guān)掉 app, 替換推送消息如下:

{
  "aps": {
    "alert": "New Posts!",
    "sound": "default",
    "link_url": "https://raywenderlich.com",
    "category": "NEWS_CATEGORY"
  }
}

下拉推送消息命黔,點擊 View action呜呐, 顯示如下:

IMG_7310-281x500.png

現(xiàn)在 app 已經(jīng)能夠處理 action 了就斤, 你也可以定義自己的 action 試一試悍募。

Silent 推送消息

Silent 推送消息可以 在后臺默默的喚醒你的 app 去執(zhí)行一些任務(wù). WenderCast 可以使用它來更新 podcast list.

App Settings -> CapabilitesWenderCast 打開 Background Modes . 勾選 Remote Notifications

現(xiàn)在 app 在接收到這類消息的時候就會在后臺喚醒。

在 ** AppDelegate** 內(nèi)洋机,替換 ** application(_:didReceiveRemoteNotification:) ** 如下:

func application(
  _ application: UIApplication,
  didReceiveRemoteNotification userInfo: [AnyHashable : Any],
  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
  
  let aps = userInfo["aps"] as! [String: AnyObject]
  
  // 1
  if aps["content-available"] as? Int == 1 {
    let podcastStore = PodcastStore.sharedStore
    // Refresh Podcast
    // 2
    podcastStore.refreshItems { didLoadNewItems in
      // 3
      completionHandler(didLoadNewItems ? .newData : .noData)
    }
  } else  {
    // News
    // 4
    _ = NewsItem.makeNewsItem(aps)
    completionHandler(.newData)
  }
}

代碼干的事為:

// 1 判斷 content-available 是否為 1 來確定是否為 Silent 通知
// 2 異步更新 podcast list
// 3 當更新完以后坠宴,調(diào)用 completionHandler 來讓系統(tǒng)確定是否有新數(shù)據(jù)載入了
// 4 如果不是 silent 通知,假定為普通消息推送

確定調(diào)用 completionHandler 的時候傳入真實的數(shù)據(jù)绷旗,系統(tǒng)會依次判斷電池在后臺運行的消耗情況喜鼓,系統(tǒng)會在需要的時候可能會把你的 app 殺掉。

替換 Pusher 如下:

{
  "aps": {
    "content-available": 1
  }
}

如果一切正常衔肢,你是看不到什么的庄岖,當然你也可以直接加入一些print方法,查看控制臺輸出情況 看是否執(zhí)行了, 比如替換如下:

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        let aps = userInfo["aps"] as! [String: AnyObject]
        
        if aps["content-available"] as? Int == 1 {
            print("=== content-available")
            let podcastStore = PodcastStore.sharedStore
            podcastStore.refreshItems({ (didLoadNewItems) in
                completionHandler(didLoadNewItems ? .newData : .noData)
            })
        } else {
            print("=== no, content-availabel")
            
            _ = NewsItem.makeNewsItem(aps)
            
            completionHandler(.newData)
        }
    }

原文查看是否運行的方法是:

打開scheme:

screen_editscheme-480x191.png

** Run -> Info** 選擇 Wait for executable to be launched:

screen_scheme-480x288.png

application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法內(nèi)角骤,打斷點看是否運行隅忿。

最后

你也可以下載 完成代碼 看運行情況,當然需要做的是修改 Bundle ID邦尊, 替換自己的證書背桐。

雖然 APNs 對于 app 來說很重要,但是如果發(fā)生太頻繁的推送消息蝉揍,用戶很可能會把 app 卸載掉链峭,所以還是要合理發(fā)生推送消息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末又沾,一起剝皮案震驚了整個濱河市弊仪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杖刷,老刑警劉巖撼短,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異挺勿,居然都是意外死亡曲横,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來禾嫉,“玉大人灾杰,你說我怎么就攤上這事∥醪危” “怎么了艳吠?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長孽椰。 經(jīng)常有香客問我昭娩,道長,這世上最難降的妖魔是什么黍匾? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任栏渺,我火速辦了婚禮,結(jié)果婚禮上锐涯,老公的妹妹穿的比我還像新娘磕诊。我一直安慰自己,他們只是感情好纹腌,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布霎终。 她就那樣靜靜地躺著,像睡著了一般升薯。 火紅的嫁衣襯著肌膚如雪莱褒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天涎劈,我揣著相機與錄音广凸,去河邊找鬼。 笑死责语,一個胖子當著我的面吹牛炮障,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播坤候,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胁赢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了白筹?” 一聲冷哼從身側(cè)響起智末,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎徒河,沒想到半個月后系馆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡顽照,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年由蘑,在試婚紗的時候發(fā)現(xiàn)自己被綠了闽寡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡尼酿,死狀恐怖爷狈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情裳擎,我是刑警寧澤涎永,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站鹿响,受9級特大地震影響羡微,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惶我,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一妈倔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧指孤,春花似錦启涯、人聲如沸贬堵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽黎做。三九已至叉跛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蒸殿,已是汗流浹背筷厘。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宏所,地道東北人酥艳。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像爬骤,于是被迫代替她去往敵國和親充石。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

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

  • 概述 在多數(shù)移動應(yīng)用中任何時候都只能有一個應(yīng)用程序處于活躍狀態(tài)霞玄,如果其他應(yīng)用此刻發(fā)生了一些用戶感興趣的那么通過通知...
    莫離_焱閱讀 6,512評論 1 8
  • 極光推送: 1.JPush當前版本是1.8.2骤铃,其SDK的開發(fā)除了正常的功能完善和擴展外也緊隨蘋果官方的步伐,SD...
    Isspace閱讀 6,719評論 10 16
  • 周偉濤撕瞧,現(xiàn)數(shù)人科技(主要產(chǎn)品數(shù)人云,基于 Mesos 和 Docker 技術(shù)的云操作系統(tǒng))云平臺負責人,曾就職于國...
    優(yōu)云數(shù)智閱讀 4,781評論 0 14
  • #玩卡不卡·每日一抽# 每一位都可以通過這張卡片覺察自己: 1丛版、直覺他叫什么名字咨跌?小可憐 2、他幾歲了硼婿? 3 3锌半、...
    燕燕584閱讀 203評論 0 2
  • 楊癡草 在希望的田野上 種上失望的莊稼 澆水 鋤草 施肥 不打農(nóng)藥 不關(guān)心 天氣 長勢 和收成 有一天 苞谷 會長...
    楊癡草閱讀 187評論 1 1