iOS Core Data 數據遷移 指南

前言

Core Data是iOS上一個效率比較高的數據庫框架,(但是Core Data并不是一種數據庫,它底層還是利用Sqlite3來存儲數據的),它可以把數據當成對象來操作,而且開發者并不需要在乎數據在磁盤上面的存儲方式。它會把位于NSManagedObject Context里面的托管對象NSManagedObject類的實例或者某個NSManagedObject子類的實例,通過NSManagedObjectModel托管對象模型,把托管對象保存到持久化存儲協調器NSPersistentStoreCoordinator持有的一個或者多個持久化存儲區中NSPersistentStore中。使用Core Data進行查詢的語句都是經過Apple特別優化過的,所以都是效率很高的查詢。

當你進行簡單的設定,比如說設定某個實體的默認值,設定級聯刪除的操作,設定數據的驗證規則,使用數據的請求模板,這些修改Core Data都會自己完成,不用自己進行數據遷移。那那些操作需要我們進行數據遷移呢?凡是會引起NSManagedObjectModel托管對象模型變化的,都最好進行數據遷移,防止用戶升級應用之后就閃退。會引起NSManagedObjectModel托管對象模型變化的有以下幾個操作,新增了一張表,新增了一張表里面的一個實體,新增一個實體的一個屬性,把一個實體的某個屬性遷移到另外一個實體的某個屬性里面…………大家應該現在都知道哪些操作需要進行數據遷移了吧。

小技巧:

進入正題之前,我先說3個調試Core Data里面調試可能你會需要的操作。

1.一般打開app沙盒里面的會有三種類型的文件,sqlite,sqlite-shm,sqlite-wal,后面2者是iOS7之后系統會默認開啟一個新的“數據庫日志記錄模式”(database journaling mode)生成的,sqlite-shm是共享內存(Shared Memory)文件,該文件里面會包含一份sqlite-wal文件的索引,系統會自動生成shm文件,所以刪除它,下次運行還會生成。sqlite-wal是預寫式日志(Write-Ahead Log)文件,這個文件里面會包含尚未提交的數據庫事務,所以看見有這個文件了,就代表數據庫里面還有還沒有處理完的事務需要提交,所以說如果有sqlite-wal文件,再去打開sqlite文件,很可能最近一次數據庫操作還沒有執行。

所以在調試的時候,我們需要即時的觀察數據庫的變化,我們就可以先禁用這個日志記錄模式,只需要在建立持久化存儲區的時候存入一個參數即可。具體代碼如下

    NSDictionary *options =
    @{
          NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
     };
    
    NSError *error = nil;
    _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                        configuration:nil
                                                  URL:[self storeURL]
                                              options:options error:&error];

2.Mac上打開數據庫的方式很多,我推薦3個,一個是Firefox里面直接有sqlite的插件,免費的,可以直接安裝,也很方便。當然也有不用Firefox的朋友,就像我是Chrome重度使用者,那就推薦2個免費的小的app,一個是sqlitebrowser,一個是sqlite manager,這2個都比較輕量級,都比較好用。

3.如果你想看看Core Data到底底層是如何優化你的查詢語句的,這里有一個方法可以看到。

先點擊Product ->Scheme ->Edit Scheme


然后再切換到Arguments分頁中,在Arguments Passed On Launch里面加入 “- com.apple.CoreData.SQLDebug 3”,重新運行app,下面就會顯示Core Data優化過的Sql語句了。


好了,調試信息應該都可以完美顯示了,可以開始愉快的進入正文了!

一.Core Data自帶的輕量級的數據遷移

這種遷移可別小看它,在你新建一張表的時候還必須加上它才行,否則會出現如下的錯誤,

**Failed to add store. Error: Error Domain=NSCocoaErrorDomain Code=134100 "(null)" UserInfo={metadata={**
**    NSPersistenceFrameworkVersion = 641;**
**    NSStoreModelVersionHashes =     {**
**        Item = <64288772 72e62096 a8a4914f 83db23c9 13718f81 4417e297 293d0267 79b04acb>;**
**        Measurement = <35717f0e 32cae0d4 57325758 58ed0d11 c16563f2 567dac35 de63d5d8 47849cf7>;**
**    };**
**    NSStoreModelVersionHashesVersion = 3;**
**    NSStoreModelVersionIdentifiers =     (**
**        ""**
**    );**
**    NSStoreType = SQLite;**
**    NSStoreUUID = "9A16746E-0C61-421B-B936-412F0C904FDF";**
**    "_NSAutoVacuumLevel" = 2;**
**}, reason=The model used to open the store is incompatible with the one used to create the store}**

錯誤原因寫的比較清楚了,reason=The model used to open the store is incompatible with the one used to create the store,這個是因為我新建了一張表,但是我沒有打開輕量級的遷移Option。這里會有人會問了,我新建表從來沒有出現這個錯誤啊?那是因為你們用的第三方框架就已經寫好了改Option了。(場外人:這年頭誰還自己從0開始寫Core Data啊,肯定都用第三方框架啊)那這里我就當講解原理了哈。如果是自己從0開始寫的Core Data的話,這里是應該會報錯了,解決辦法當然是加上代碼,利用Core Data的輕量級遷移,來防止這種找不到存儲區的閃退問題


NSDictionary *options =
    @{
      NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"},
      NSMigratePersistentStoresAutomaticallyOption :@YES,
      NSInferMappingModelAutomaticallyOption:@YES
    };
    
    NSError *error = nil;
    _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                        configuration:nil
                                                  URL:[self storeURL]
                                              options:options error:&error];

這里說一下新增加的2個參數的意義:
NSMigratePersistentStoresAutomaticallyOption = YES,那么Core Data會試著把之前低版本的出現不兼容的持久化存儲區遷移到新的模型中,這里的例子里,Core Data就能識別出是新表,就會新建出新表的存儲區來,上面就不會報上面的error了。

NSInferMappingModelAutomaticallyOption = YES,這個參數的意義是Core Data會根據自己認為最合理的方式去嘗試MappingModel,從源模型實體的某個屬性,映射到目標模型實體的某個屬性。

接著我們來看看MagicRecord源碼是怎么寫的,所以大家才能執行一些操作不會出現我上面說的閃退的問題


+ (NSDictionary *) MR_autoMigrationOptions;
{
    // Adding the journalling mode recommended by apple
    NSMutableDictionary *sqliteOptions = [NSMutableDictionary dictionary];
    [sqliteOptions setObject:@"WAL" forKey:@"journal_mode"];
    
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
                             sqliteOptions, NSSQLitePragmasOption,
                             nil];
    return options;
}

上面這一段就是MagicRecord源碼里面替大家加的Core Data輕量級的數據遷移的保護了,所以大家不寫那2個參數,一樣不會報錯。(題外話:MagicRecord默認這里是開啟了WAL日志記錄模式了) 此處如果大家注銷掉那兩個參數,或者把參數的值設置為NO,再運行一次,新建一張表,就會出現我上面提到的錯誤了。大家可以實踐實踐,畢竟實踐出真知嘛。

只要打開上面2個參數,Core Data就會執行自己的輕量級遷移了,當然,在實體屬性遷移時候,用該方式不靠譜,之前我覺得它肯定能推斷出來,結果后來還是更新后直接閃退報錯了,可能是因為表結構太復雜,超過了它簡單推斷的能力范圍了,所以我建議,在進行復雜的實體屬性遷移到另一個屬性遷移的時候,不要太相信這種方式,還是最好自己Mapping一次。當然,你要是新建一張表的時候,這2個參數是必須要加上的!!!

二.Core Data手動創建Mapping文件進行遷移

這種方式比前一種方式要更加精細一些,Mapping文件會指定哪個實體的某個屬性遷移到哪個實體的某個屬性,這比第一種交給Core Data自己去推斷要靠譜一些,這種方法直接指定映射!
先說一下,如果復雜的遷移,不加入這個Mapping文件會出現什么樣的錯誤


**Failed to add store. Error: Error Domain=NSCocoaErrorDomain Code=134140 "(null)" UserInfo={destinationModel=(<NSManagedObjectModel: 0x7f82d4935280>) isEditable 0, entities {**
**    Amount = "(<NSEntityDescription: 0x7f82d4931960>) name Amount, managedObjectClassName NSManagedObject, renamingIdentifier Amount, isAbstract 0, superentity name (null), properties {\n    qwe = \"(<NSAttributeDescription: 0x7f82d4930f40>), name qwe, isOptional 1, isTransient 0, entity Amount, renamingIdentifier qwe, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue (null)\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**    Item = "(<NSEntityDescription: 0x7f82d4931a10>) name Item, managedObjectClassName Item, renamingIdentifier Item, isAbstract 0, superentity name (null), properties {\n    collected = \"(<NSAttributeDescription: 0x7f82d4930fd0>), name collected, isOptional 1, isTransient 0, entity Item, renamingIdentifier collected, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 0\";\n    listed = \"(<NSAttributeDescription: 0x7f82d4931060>), name listed, isOptional 1, isTransient 0, entity Item, renamingIdentifier listed, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 1\";\n    name = \"(<NSAttributeDescription: 0x7f82d49310f0>), name name, isOptional 1, isTransient 0, entity Item, renamingIdentifier name, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue New Item\";\n    photoData = \"(<NSAttributeDescription: 0x7f82d4931180>), name photoData, isOptional 1, isTransient 0, entity Item, renamingIdentifier photoData, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 1000 , attributeValueClassName NSData, defaultValue (null)\";\n    quantity = \"(<NSAttributeDescription: 0x7f82d4931210>), name quantity, isOptional 1, isTransient 0, entity Item, renamingIdentifier quantity, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 600 , attributeValueClassName NSNumber, defaultValue 1\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**}, fetch request templates {**
**    Test = "<NSFetchRequest: 0x7f82d49316c0> (entity: Item; predicate: (name CONTAINS \"e\"); sortDescriptors: ((null)); type: NSManagedObjectResultType; )";**
**}, sourceModel=(<NSManagedObjectModel: 0x7f82d488e930>) isEditable 1, entities {**
**    Amount = "(<NSEntityDescription: 0x7f82d488f880>) name Amount, managedObjectClassName NSManagedObject, renamingIdentifier Amount, isAbstract 0, superentity name (null), properties {\n    abc = \"(<NSAttributeDescription: 0x7f82d488f9d0>), name abc, isOptional 1, isTransient 0, entity Amount, renamingIdentifier abc, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue (null)\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**    Item = "(<NSEntityDescription: 0x7f82d488fbe0>) name Item, managedObjectClassName NSManagedObject, renamingIdentifier Item, isAbstract 0, superentity name (null), properties {\n    collected = \"(<NSAttributeDescription: 0x7f82d48901c0>), name collected, isOptional 1, isTransient 0, entity Item, renamingIdentifier collected, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 0\";\n    listed = \"(<NSAttributeDescription: 0x7f82d488fd20>), name listed, isOptional 1, isTransient 0, entity Item, renamingIdentifier listed, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 1\";\n    name = \"(<NSAttributeDescription: 0x7f82d488fdb0>), name name, isOptional 1, isTransient 0, entity Item, renamingIdentifier name, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue New Item\";\n    photoData = \"(<NSAttributeDescription: 0x7f82d488fad0>), name photoData, isOptional 1, isTransient 0, entity Item, renamingIdentifier photoData, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 1000 , attributeValueClassName NSData, defaultValue (null)\";\n    quantity = \"(<NSAttributeDescription: 0x7f82d488fc90>), name quantity, isOptional 1, isTransient 0, entity Item, renamingIdentifier quantity, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 600 , attributeValueClassName NSNumber, defaultValue 1\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**}, fetch request templates {**
**    Test = "<NSFetchRequest: 0x7f82d488fa60> (entity: Item; predicate: (name CONTAINS \"e\"); sortDescriptors: ((null)); type: NSManagedObjectResultType; )";**
**}, reason=Can't find mapping model for migration}**

直接看最后一行錯誤的原因Can't find mapping model for migration,這直接說出了錯誤的原因,那么接下來我們就創建一個Mapping Model文件。

在你xcdatamodeld相同的文件夾目錄下,“New File” ->"Core Data"->"Mapping Model"

選擇需要Mapping的源數據庫


再選擇目標數據庫


接著命名一下Mapping Model文件的名字


這里說明一下,名字最好能一眼看上去就能區分出是哪個數據庫的版本升級上來的,這里我寫的就是ModelV4ToV5,這樣一看就知道是V4到V5的升級。

這里說明一下Mapping文件的重要性,首先,每個版本的數據庫之間都最好能加上一個Mapping文件,這樣從低版本的數據庫升級上來,可以保證每個版本都不會出錯,都不會導致用戶升級之后就出現閃退的問題。

比如上圖,每個數據庫之間都會對應一個Mapping文件,V0ToV1,V1ToV2,V2ToV3,V3ToV4,V4ToV5,每個Mapping都必須要。

試想,如果用戶實在V3的老版本上,由于appstore的更新規則,每次更新都直接更新到最新,那么用戶更新之后就會直接到V5,如果缺少了中間的V3ToV4,V4ToV5,中的任意一個,那么V3的用戶都無法升級到V5上來,都會閃退。所以這里就看出了每個版本之間都要加上Mapping文件的重要性了。這樣任意低版本的用戶,任何時刻都可以通過Mapping文件,隨意升級到最新版,而且不會閃退了!

接下來再說說Mapping文件打開是些什么東西。

Mapping文件打開對應的就是Source源實體屬性,遷移到Target目標實體屬性的映射,上面是屬性,下面是關系的映射。$source就是代表的源實體

寫到這里,就可以很清楚的區分一下到目前為止,Core Data輕量級遷移和手動創建Mapping進行遷移,這2種方法的異同點了。我簡單總結一下:
1.Core Data輕量級遷移是適用于添加新表,添加新的實體,添加新的實體屬性,等簡單的,系統能自己推斷出來的遷移方式。
2.手動創建Mapping適用于更加復雜的數據遷移

舉個例子吧,假設我最初有一張很抽象的表,叫Object表,用來存儲東西的一些屬性,里面假設有name,width,height。突然我有一天有新需求了,需要在Object表里面新增幾個字段,比如說colour,weight等,由于這個都是簡單的新增,不涉及到數據的轉移,這時候用輕量級遷移就可以了。

不過突然有一個程序又有新需求了,需要增加2張表,一個是Human表,一個是Animal表,需要把當初抽象定義的Object表更加具體化。這時就需要把Object里面的人都抽出來,放到新建的Human表里,動物也都抽出來放到新建的Animal表里。由于新建的2張表都會有name屬性,如果這個時候進行輕量級的遷移,系統可能推斷不出到底哪些name要到Human表里,哪里要Animal表了。再者,還有一些屬性在Human表里面有,在Animal表里面沒有。這是時候就必須手動添加一個Mapping Model文件了,手動指定哪些屬性是源實體的屬性,應該映射到目標實體的哪個屬性上面去。這種更加精細的遷移方式,就只能用手動添加Mapping Model來完成了,畢竟iOS系統不知道你的需求和想法。

三.通過代碼實現數據遷移

這個通過代碼進行遷移主要是在數據遷移過程中,如果你還想做一些什么其他事情,比如說你想清理一下垃圾數據,實時展示數據遷移的進度,等等,那就需要在這里來實現了。

首先,我們需要檢查一下該存儲區存不存在,再把存儲區里面的model metadata進行比較,檢查一下是否兼容,如果不能兼容,那么就需要我們進行數據遷移了。

- (BOOL)isMigrationNecessaryForStore:(NSURL*)storeUrl
{
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    
    if (![[NSFileManager defaultManager] fileExistsAtPath:[self storeURL].path])
    {
        NSLog(@"SKIPPED MIGRATION: Source database missing.");
        return NO;
    }
    
    NSError *error = nil;
    NSDictionary *sourceMetadata =
    [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                                                               URL:storeUrl error:&error];
    NSManagedObjectModel *destinationModel = _coordinator.managedObjectModel;
    
    if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata])
    {
        NSLog(@"SKIPPED MIGRATION: Source is already compatible");
        return NO;
    }
    
    return YES;
}

當上面函數返回YES,我們就需要合并了,那接下來就是下面的函數了

- (BOOL)migrateStore:(NSURL*)sourceStore {
    
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    BOOL success = NO;
    NSError *error = nil;
    
    // STEP 1 - 收集 Source源實體, Destination目標實體 和 Mapping Model文件
    NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator
                                    metadataForPersistentStoreOfType:NSSQLiteStoreType
                                    URL:sourceStore
                                    error:&error];
    
    NSManagedObjectModel *sourceModel =
    [NSManagedObjectModel mergedModelFromBundles:nil
                                forStoreMetadata:sourceMetadata];
    
    NSManagedObjectModel *destinModel = _model;
    
    NSMappingModel *mappingModel =
    [NSMappingModel mappingModelFromBundles:nil
                             forSourceModel:sourceModel
                           destinationModel:destinModel];
    
    // STEP 2 - 開始執行 migration合并, 前提是 mapping model 不是空,或者存在
    if (mappingModel) {
        NSError *error = nil;
        NSMigrationManager *migrationManager =
        [[NSMigrationManager alloc] initWithSourceModel:sourceModel
                                       destinationModel:destinModel];
        [migrationManager addObserver:self
                           forKeyPath:@"migrationProgress"
                              options:NSKeyValueObservingOptionNew
                              context:NULL];
        
        NSURL *destinStore =
        [[self applicationStoresDirectory]
         URLByAppendingPathComponent:@"Temp.sqlite"];
        
        success =
        [migrationManager migrateStoreFromURL:sourceStore
                                         type:NSSQLiteStoreType options:nil
                             withMappingModel:mappingModel
                             toDestinationURL:destinStore
                              destinationType:NSSQLiteStoreType
                           destinationOptions:nil
                                        error:&error];
        if (success)
        {
            // STEP 3 - 用新的migrated store替換老的store
            if ([self replaceStore:sourceStore withStore:destinStore])
            {
                NSLog(@"SUCCESSFULLY MIGRATED %@ to the Current Model",
                          sourceStore.path);
                [migrationManager removeObserver:self
                                      forKeyPath:@"migrationProgress"];
            }
        }
        else
        {
            NSLog(@"FAILED MIGRATION: %@",error);
        }
    }
    else
    {
        NSLog(@"FAILED MIGRATION: Mapping Model is null");
    }
    
    return YES; // migration已經完成
}

上面的函數中,如果遷移進度有變化,會通過觀察者,observeValueForKeyPath來告訴用戶進度,這里可以監聽該進度,如果沒有完成,可以來禁止用戶執行某些操作


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if ([keyPath isEqualToString:@"migrationProgress"]) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            float progress =
            [[change objectForKey:NSKeyValueChangeNewKey] floatValue];
          
            int percentage = progress * 100;
            NSString *string =
            [NSString stringWithFormat:@"Migration Progress: %i%%",
             percentage];
            NSLog(@"%@",string);

        });
    }
}

當然,這個合并數據遷移的操作肯定是用一個多線程異步的執行,免得造成用戶界面卡頓,再加入下面的方法,我們來異步執行


- (void)performBackgroundManagedMigrationForStore:(NSURL*)storeURL
{
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    
    dispatch_async(
                   dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
                       BOOL done = [self migrateStore:storeURL];
                       if(done) {
                           dispatch_async(dispatch_get_main_queue(), ^{
                               NSError *error = nil;
                               _store =
                               [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                          configuration:nil
                                                                    URL:[self storeURL]
                                                                options:nil
                                                                  error:&error];
                               if (!_store) {
                                   NSLog(@"Failed to add a migrated store. Error: %@",
                                         error);abort();}
                               else {
                                   NSLog(@"Successfully added a migrated store: %@",
                                         _store);}
                           });
                       }
                   });
}

到這里,數據遷移都完成了,不過目前還有一個問題就是,我們應該何時去執行該遷移的操作,更新完畢之后?appDelegate一進來?都不好,最好的方法還是在把當前存儲區添加到coordinator之前,我們就執行好數據遷移!

- (void)loadStore
{
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    
    if (_store) {return;} // 不要再次加載了,因為已經加載過了
    
    BOOL useMigrationManager = NO;
    if (useMigrationManager &&
        [self isMigrationNecessaryForStore:[self storeURL]])
    {
        [self performBackgroundManagedMigrationForStore:[self storeURL]];
    }
    else
    {
        NSDictionary *options =
        @{
          NSMigratePersistentStoresAutomaticallyOption:@YES
          ,NSInferMappingModelAutomaticallyOption:@YES
          ,NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
          };
        NSError *error = nil;
        _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                            configuration:nil
                                                      URL:[self storeURL]
                                                  options:options
                                                    error:&error];
        if (!_store)
        {
            NSLog(@"Failed to add store. Error: %@", error);abort();
        }
        else
        {
            NSLog(@"Successfully added store: %@", _store);
        }
    }

}

這樣就完成了數據遷移了,并且還能顯示出遷移進度,在遷移中還可以自定義一些操作,比如說清理垃圾數據,刪除一些不用的表,等等。

結束

好了,到此,Core Data數據遷移的幾種方式我就和大家分享完了,如果文中有不對的地方,歡迎大家提出來,我們一起交流進步!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容