[App探索]JSBox中幽靈觸發(fā)器的實現(xiàn)原理探索

本文已收錄到1.1K Star數(shù)開源學(xué)習(xí)指南——《大廠面試指北》络凿,如果想要了解更多大廠面試相關(guān)的內(nèi)容及獲取《大廠面試指北》離線PDF版藻糖,請掃描下方二維碼碼關(guān)注公眾號“大廠面試”喇辽,謝謝大家了两嘴!

《大廠面試指北》最佳閱讀地址:

http://notfound9.github.io/interviewGuide/

《大廠面試指北》項目地址:

https://github.com/NotFound9/interviewGuide

獲取《大廠面試指北》離線PDF版,請掃描下方二維碼關(guān)注公眾號“大廠面試”

image.png

《大廠面試指北》項目截圖:

image.png

前言

幽靈觸發(fā)器是鐘穎大神的JSBox中的一個功能舰涌,在app進程被殺死的情況下陵像,也可以將通知固定在通知欄就珠,即便用戶點擊清除,也能馬上再彈出醒颖,永遠不消失妻怎,除非用戶關(guān)閉App的通知權(quán)限或者卸載App,才可以消失泞歉。這個功能確實比較有意思逼侦,而且鐘穎大神在介紹視頻里有提到是目前JSBox獨有的匿辩,說明實現(xiàn)得非常巧妙,自己研究的話還是很難想到的榛丢,非常值得學(xué)習(xí)铲球,而且當(dāng)你了解它的實現(xiàn)原理的話,會發(fā)現(xiàn)其實可以做很多其他的事情晰赞。當(dāng)某天產(chǎn)品經(jīng)理對App推送點擊率不滿意時稼病,可以向她祭出這件大殺器(哈哈,開玩笑的宾肺,無限推送這種功能其實蘋果很不推薦溯饵,因為確實有可能會被一些不良App采用,然后無限推送锨用,讓用戶反感)。以下內(nèi)容僅供學(xué)習(xí)討論隘谣,JSBox是一個很強大的App增拥,有很多值得學(xué)習(xí)的地方,強烈推薦大家去購買使用寻歧。

簡短的效果視頻

image

完整的介紹視頻

https://weibo.com/tv/v/G79vjviDM?fid=1034:1f37179499e39dbc8a7472897b9e056c
從2分6秒開始

探索歷程

因為沒有可以用來砸殼的越獄手機掌栅,而且PP助手也沒有JSBox的包,一開始是去搜幽靈觸發(fā)器码泛,無限通知的實現(xiàn)猾封,發(fā)現(xiàn)沒找到答案,stackoverflow上的開發(fā)者倒是對無限通知比較感興趣噪珊,問答比較多晌缘,但是沒有人給出答案,基本上也是說因為蘋果不希望開發(fā)者用這種功能去騷擾用戶痢站。所以只能自己閱讀通知文檔磷箕,查資料來嘗試實現(xiàn)了。

難道是使用時間間隔觸發(fā)器UNTimeIntervalNotificationTrigger來實現(xiàn)的嗎阵难?

因為看通知清除了還是一個接一個得出現(xiàn)岳枷,很自然就能想到是通過繞過蘋果的檢測,去改UNTimeIntervalNotificationTrigger的timeInterval屬性來實現(xiàn)的呜叫,所以寫出了一下代碼:

UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"推送標題";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger];
[center addNotificationRequest:request withCompletionHandler:nil];

通過傳入創(chuàng)建時間間隔為1s的實際間隔觸發(fā)器來實現(xiàn)空繁,運行后,第一個通知能正常顯示出來朱庆,清除第一個通知后盛泡,顯示第二個通知時,app崩潰了椎工,時間間隔不能小于60s饭于。

UserNotificationsDemo[14895:860379] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'time interval must be at least 60 if repeating'
*** First throw call stack:
(0x1ae2a3ea0 0x1ad475a40 0x1ae1b9c1c 0x1aeca7140 0x1b8738d0c 0x1b8738bdc 0x102d508ac 0x1db487658 0x1dad09a18 0x1dad09720 0x1dad0e8e0 0x1dad0f840 0x1dad0e798 0x1dad13684 0x1db057090 0x1b0cd96e4 0x1030ccdc8 0x1030d0a10 0x1b0d17a9c 0x1b0d17728 0x1b0d17d44 0x1ae2341cc 0x1ae23414c 0x1ae233a30 0x1ae22e8fc 0x1ae22e1cc 0x1b04a5584 0x1db471054 0x102d517f0 0x1adceebb4)
libc++abi.dylib: terminating with uncaught exception of type NSException

timeInterval是只讀屬性蜀踏,看來蘋果早有防范
@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval timeInterval;
但是這年頭,還能活著做iOS開發(fā)的誰沒還不會用KVC呀掰吕,所以很自然得就能想到使用KVC來改

UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"推送標題";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger];
[timeTrigger setValue:@1 forKey:@"timeInterval"];
[center addNotificationRequest:request withCompletionHandler:nil];

而且我打斷點看果覆,確實改成功了,


image.png

但是殖熟,很快局待,當(dāng)我把第一個通知清除時,手機變成這樣了


image

有那么一刻菱属,我心里很慌钳榨,我一定好好做人胞谈,不去改蘋果爸爸的只讀屬性了吏奸。

蘋果是在顯示第二個通知的時候才去判斷的,而我們的代碼只能控制到將通知請求request添加到UNUserNotificationCenter這一步豺型,所以不太好繞過赏陵。

難道是使用地點觸發(fā)器UNLocationNotificationTrigger來實現(xiàn)的嗎饼齿?

UNLocationNotificationTrigger可以通過判斷用戶進入某一區(qū)域,離開某一區(qū)域時觸發(fā)通知蝙搔,但是我去看了一下設(shè)置里面的權(quán)限缕溉,發(fā)現(xiàn)只使用這個功能的時候JSBox并沒有請求定位的權(quán)限,所以應(yīng)該不是根據(jù)地點觸發(fā)的吃型。

繼續(xù)閱讀文檔

然后我就去鐘穎大神的JSBox社區(qū)仔細查看開發(fā)者文檔证鸥,查看關(guān)于通知觸發(fā)相關(guān)的api,結(jié)果發(fā)現(xiàn)

image.png

不是通過repeats字段勤晚,而是通過renew這個字段來決定是否需要重復(fù)創(chuàng)建通知的枉层,所以很有可能不是通過時間觸發(fā)器來實現(xiàn)的,是通過自己寫代碼去創(chuàng)建一個通知运翼,然后將通知進行發(fā)送返干。
在大部分iOS開發(fā)同學(xué)心中(包括我之前也是這么認為的),普遍都認為當(dāng)app處于運行狀態(tài)時血淌,這樣的實現(xiàn)方案自然沒有問題矩欠,因為我們可以獲取到通知展示,用戶對通知操作的回調(diào)悠夯。當(dāng)app處于未運行狀態(tài)時癌淮,除非用戶點擊通知喚醒app,我們無法獲取到操作的回調(diào)沦补,但其實在iOS 10以后乳蓄,蘋果公開的UserNotifications框架,允許開發(fā)者通過實現(xiàn)UNUserNotificationCenter的代理方法夕膀,來處理用戶對通知的各種點擊操作虚倒。具體可以看蘋果的這篇文章Handling Notifications and Notification-Related Actions美侦,
翻譯其中主要的一段:
image.png

你可以通過實現(xiàn)UNUserNotificationCenter的代理方法,來處理用戶對通知的各種點擊操作魂奥。當(dāng)用戶對通知進行某種操作時菠剩,系統(tǒng)會在后臺啟動你的app并且調(diào)用UNUserNotificationCenter的代理對象實現(xiàn)的userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:方法,參數(shù)response中會包含用戶進行的操作的actionIdentifier耻煤,即便是系統(tǒng)定義的通知操作也是一樣具壮,當(dāng)用戶對通知點擊取消或者點擊打開喚醒App,系統(tǒng)也會上報這些操作哈蝇。
核心就是這個方法

// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14) __TVOS_PROHIBITED;

所以我就寫了一個demo來實現(xiàn)這個功能棺妓,核心代碼如下:

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
    [self applyPushNotificationAuthorization:application];//請求發(fā)送通知授權(quán)
    [self addNotificationAction];//添加自定義通知操作擴展
    return YES;
}
//請求發(fā)送通知授權(quán)
- (void)applyPushNotificationAuthorization:(UIApplication *)application{
    if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0)) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (!error && granted) {
                NSLog(@"注冊成功");
            }else{
                NSLog(@"注冊失敗");
            }

        }];
        [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
            NSLog(@"settings========%@",settings);
        }];
    } else if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)){
        [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound ) categories:nil]];
    }
    [application registerForRemoteNotifications];
}

//添加自定義通知操作擴展
- (void)addNotificationAction {
    UNNotificationAction *openAction = [UNNotificationAction actionWithIdentifier:@"NotificationForeverCategory.action.look" title:@"打開App" options:UNNotificationActionOptionForeground];
    UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"NotificationForeverCategory.action.cancel" title:@"取消" options:UNNotificationActionOptionDestructive];
    UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@"NotificationForeverCategory" actions:@[openAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
    [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:notificationCategory]];
}


# pragma mark UNUserNotificationCenterDelegate
//app處于前臺時,通知即將展示時的回調(diào)方法炮赦,不實現(xiàn)會導(dǎo)致通知顯示不了
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
    completionHandler(UNNotificationPresentationOptionBadge|
                      UNNotificationPresentationOptionSound|
                      UNNotificationPresentationOptionAlert);
}

//app處于后臺或者未運行狀態(tài)時怜跑,用戶點擊操作的回調(diào)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
    if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {//點擊系統(tǒng)的清除按鈕
        UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.0001f repeats:NO];
        UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
        content.title = @"App探索-NotFound";
        content.body = @"[App探索]JSBox中幽靈觸發(fā)器的實現(xiàn)原理探索";
        content.badge = @1;
        content.categoryIdentifier = @"NotificationForeverCategory";
        UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger];
        [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil];
    }
    completionHandler();
}

- (void)applicationWillResignActive:(UIApplication *)application {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button addTarget:self action:@selector(sendNotification) forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"發(fā)送一個3s后顯示的通知" forState:UIControlStateNormal];
    button.frame = CGRectMake(0, 200, [UIScreen mainScreen].bounds.size.width, 100);
    [self.view addSubview:button];
}

//發(fā)送一個通知
- (void)sendNotification {
    UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3.0f repeats:NO];
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
    content.title = @"App探索-NotFound";
    content.body = @"[App探索]JSBox中幽靈觸發(fā)器的實現(xiàn)原理探索";
    content.badge = @1;
    content.categoryIdentifier = @"NotificationForeverCategory";
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger];
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    [center addNotificationRequest:request withCompletionHandler: nil];
}

必須在didFinishLaunchingWithOptions的方法返回前設(shè)置通知中心的代理,這個文檔里面都有提及吠勘,大家都知道妆艘,但是有兩個文檔里面未曾提及的難點需要注意:

隱藏關(guān)卡一 必須給通知添加自定義的通知操作

1.必須給通知添加自定義的通知操作,并且給發(fā)送的通知指定自定義的通知操作的categoryIdentifier看幼,這樣系統(tǒng)在用戶對通知進行操作時才會調(diào)用這個代理方法,
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler
自定義通知操作是用戶長按通知幌陕,下方彈出的actionSheet诵姜,在我們的Demo中,是“打開App”和“取消”兩個操作搏熄,其實不添加這些自定義操作的話棚唆,系統(tǒng)的這些“管理”,”“查看”心例,“清除”也是有的宵凌,但是當(dāng)用戶點擊“清除”時,我們的代理方法didReceiveNotificationResponse就不會被調(diào)用了止后,文檔里面沒有提及這個瞎惫,我也是試了好久才試出來的。

image.png

image.png

隱藏關(guān)卡二 必須使用上一個通知的requestIdentifier

當(dāng)用戶點擊“清除”按鈕時译株,即便app處于未運行狀態(tài)瓜喇,系統(tǒng)也會在后臺運行我們的app,并且執(zhí)行didReceiveNotificationResponse這個代理方法歉糜,在這個方法里面我們會創(chuàng)建一個UNNotificationRequest乘寒,把他添加到通知中心去,然后通知會展示出來匪补。但是系統(tǒng)好像對于在app正常運行時添加的UNNotificationRequest跟在didReceiveNotificationResponse方法里添加的UNNotificationRequest做了區(qū)分伞辛,后者在被用戶點擊“清除”按鈕后烂翰,app不會收到didReceiveNotificationResponse回調(diào)方法,可能系統(tǒng)也是考慮到開發(fā)者可能會利用這個機制去實現(xiàn)無限通知的功能蚤氏。所以我在創(chuàng)建UNNotificationRequest時甘耿,使用的identifier是前一個通知的identifier,這也是實現(xiàn)無限通知的最巧妙的地方瞧捌,可能很多開發(fā)者是知道實現(xiàn)這個代理方法來接受用戶點擊“清除”的回調(diào)棵里,然后做一些通知上報,隔一段時間再次發(fā)送通知事情姐呐,但是再次創(chuàng)建并發(fā)送的通知在被點擊“清除”時已經(jīng)不會再執(zhí)行didReceiveNotificationResponse回調(diào)了殿怜。


        UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger];

擴展

如果我們做的是效率工具類型的App,利用這個功能做一些固定通知之類的功能曙砂,如果我們做的是一些資訊類的App头谜,可以做一些不定間隔推送的功能,而不需要每次用戶點擊“清除”后鸠澈,將用戶操作通過網(wǎng)絡(luò)請求上報給服務(wù)器柱告,然后服務(wù)器根據(jù)情況給用戶發(fā)推送。更多的玩法有待我們探索笑陈。

Demo https://github.com/577528249/NotificationForever

Demo 演示Gif

[圖片上傳失敗...(image-fe3c57-1546414668879)]

寫文章太耗費時間了际度,可以的話,求大家給我點個關(guān)注吧涵妥,會定期寫原創(chuàng)文章乖菱,謝謝了!

PS:
之前建了一個微信群蓬网,里面技術(shù)氛圍比較濃厚窒所,會經(jīng)常討論一些水平比較高的技術(shù)問題,包括本文的幽靈觸發(fā)器的實現(xiàn)也是群里的一位同學(xué)提出帆锋,我發(fā)現(xiàn)網(wǎng)上沒有相關(guān)的實現(xiàn)吵取,花了幾天時間研究出來的。歡迎更多對技術(shù)有熱情的同學(xué)加入我們锯厢,可以加我微信ruiwendelll皮官,我拉大家進群。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哲鸳,一起剝皮案震驚了整個濱河市臣疑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌徙菠,老刑警劉巖讯沈,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡缺狠,警方通過查閱死者的電腦和手機问慎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挤茄,“玉大人如叼,你說我怎么就攤上這事∏钆” “怎么了笼恰?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歇终。 經(jīng)常有香客問我社证,道長,這世上最難降的妖魔是什么评凝? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任追葡,我火速辦了婚禮,結(jié)果婚禮上奕短,老公的妹妹穿的比我還像新娘宜肉。我一直安慰自己,他們只是感情好翎碑,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布谬返。 她就那樣靜靜地躺著,像睡著了一般日杈。 火紅的嫁衣襯著肌膚如雪朱浴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天达椰,我揣著相機與錄音,去河邊找鬼项乒。 笑死啰劲,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的檀何。 我是一名探鬼主播蝇裤,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼频鉴!你這毒婦竟也來了栓辜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤垛孔,失蹤者是張志新(化名)和其女友劉穎藕甩,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體周荐,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡狭莱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年僵娃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腋妙。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡默怨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出骤素,到底是詐尸還是另有隱情匙睹,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布济竹,位于F島的核電站痕檬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏规辱。R本人自食惡果不足惜谆棺,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望罕袋。 院中可真熱鬧改淑,春花似錦、人聲如沸浴讯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽榆纽。三九已至仰猖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奈籽,已是汗流浹背饥侵。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留衣屏,地道東北人躏升。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像狼忱,于是被迫代替她去往敵國和親膨疏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

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