Realm使用中碰到的問題(坑)及解決方案

realm

最近做個項(xiàng)目是需要大量的本地?cái)?shù)據(jù)交互保存持久化操作,由于是新項(xiàng)目所以我們打算使用比較新穎的框架來進(jìn)行開發(fā),最后經(jīng)過篩選使用了Realm來作為本地?cái)?shù)據(jù)操作框架。name我們?yōu)槭裁催x擇realm呢?大部分的數(shù)據(jù)庫框架還是使用2000年的SQLite,大部分的移動應(yīng)用還是直接或間接的使用SQLite來作為本地?cái)?shù)據(jù)庫比如:FMDB、Couchbase Lite,Core Data,ORMLite,而Realm是專門為移動端設(shè)計(jì)的框架,最后我們經(jīng)過比對選擇了Realm。
首先Realm 是一個跨平臺的移動數(shù)據(jù)庫引擎,其性能要優(yōu)于 FMDB、Couchbase Lite,Core Data,ORMLite - 移動端數(shù)據(jù)庫性能比較, 我們可以在 Android 端 realm-javaKotlin也可以使用,iOS端:Realm-Cocoa,同時支持 OC 和 Swift兩種語言開發(fā)。使用操作簡單、性能優(yōu)異、跨平臺、開發(fā)效率得到了大大提高(省去了數(shù)據(jù)模型與表存儲之間轉(zhuǎn)化的很多工作)、配備可視化數(shù)據(jù)庫查看工具。這些都滿足了我們項(xiàng)目的需要。
對于Realm的使用今天不在這里介紹,網(wǎng)上可以搜到很多具體的使用方法,也可以到官網(wǎng)文檔上查看Api。我們主要剖析下在項(xiàng)目開發(fā)過程中遇到到問題、疑難雜癥和解決的方案。

我們先來看下Realm不支持的地方及需要注意的地方:

1.不支持聯(lián)合主鍵
2.不支持自增長主鍵
3.不能跨線程共享realm實(shí)例,不同線程中,都要創(chuàng)建獨(dú)立的realm實(shí)例,只要配置(configuration)相同,它們操作的就是同一個實(shí)體數(shù)據(jù)庫。
4.存取只能以對象為單位,不能只查某個屬性,使用sql時,可以單獨(dú)查詢某個(幾個)獨(dú)立屬性,比如 select courseName from Courses where courseId = "001",而在realm中 + (RLMResults *)objectsWhere類似這種返回的是RLMResults對象。查詢相關(guān)函數(shù),得到的都是對象的集合,相對不夠靈活。
5.被查詢的RLMResults中的對象,任何的修改都會被直接同步到數(shù)據(jù)庫中,所以對對象的修改都必須被包裹在beginWriteTransaction中,Swift要包裹在try! Realm().write { }中,使用時要注意。
例如:

let results = SXRealm.queryByAll(DetailModel.self)
 let item = results[0]
  try!  Realm().write {//修改數(shù)據(jù),必須在此操作中,否則會造成Crash。
          item.uploadStatus = 2
          item.uploadFailedDes = "上傳失??!"
   }        

6.RLMResults與線程問題,在主線程查出來的數(shù)據(jù),如果在其他線程被訪問是不允許的,運(yùn)行時會報(bào)錯。
例如:

//這種是錯誤的,只能訪問同一線程的realm數(shù)據(jù)。
 RLMResults *results = [Course objectsWhere:@"courseId = '001'"];
 Course *getCourse = [results objectAtIndex:0];
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"%@",results);
        NSLog(@"%@",getCourse.courseName);
    });

7.auto-updating機(jī)制,十分方便,并保證了數(shù)據(jù)的實(shí)時性,但是在個別情況下,也許這種機(jī)制并不需要,可能會導(dǎo)致一些意外,所以需要注意。(OC舉例)

    RLMRealm *realm = [RLMRealm defaultRealm];
    Course *course = [[Course alloc] init];
    course.courseId = @"001";
    course.courseName = @"語文";
    [realm transactionWithBlock:^{
        [realm addObject:course];
    }];
    
    Course *getCourse1 = [Course objectsWhere:@"courseId = '001'"].firstObject;
    NSLog(@"%@",getCourse1);
    [realm transactionWithBlock:^{
        getCourse1.courseName = @"體育";
    }];
    
    NSLog(@"%@",course);

(1)第一次查詢后,result中有一條記錄,后面即便沒有執(zhí)行重新查詢,新加入的數(shù)據(jù),自動就被同步到了result中。

        RLMRealm *realm = [RLMRealm defaultRealm];
    Course *course = [[Course alloc] init];
    course.courseId = @"001";
    course.courseName = @"語文";
    [realm beginWriteTransaction];
    [Course createOrUpdateInDefaultRealmWithValue:course];
    [realm commitWriteTransaction];
    
    RLMResults *result = [Course allObjects];
    NSLog(@"%@",result);
    
    Course *course2 = [[Course alloc] init];
    course2.courseId = @"002";
    course2.courseName = @"數(shù)學(xué)";
    [realm beginWriteTransaction];
    [Course createOrUpdateInDefaultRealmWithValue:course2];
    [realm commitWriteTransaction];
    
    NSLog(@"%@",result);

(2)開始查詢出課程id為001的課程模型getCourse1、getCourse2的課程名為語文,后面僅對getCourse2進(jìn)行修改后,getCourse1的屬性也被自動同步更新了。

    RLMRealm *realm = [RLMRealm defaultRealm];
    Course *course = [[Course alloc] init];
    course.courseId = @"001";
    course.courseName = @"語文";
    [realm beginWriteTransaction];
    [Course createOrUpdateInDefaultRealmWithValue:course];
    [realm commitWriteTransaction];
    
    Course *getCourse1 = [Course objectsWhere:@"courseId = '001'"].firstObject;
    NSLog(@"%@",getCourse1);
    Course *getCourse2 = [Course objectsWhere:@"courseId = '001'"].firstObject;
    [realm beginWriteTransaction];
    getCourse2.courseName = @"體育";
    [realm commitWriteTransaction];
    NSLog(@"%@",getCourse1);

(3).在別的線程中的修改,也會被同步過來

    Course *getCourse1 = [Course objectsWhere:@"courseId = '001'"].firstObject;
    NSLog(@"%@",getCourse1);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        RLMRealm *realm = [RLMRealm defaultRealm];
        Course *getCourse2 = [Course objectsWhere:@"courseId = '001'"].firstObject;
        [realm beginWriteTransaction];
        getCourse2.courseName = @"體育";
        [realm commitWriteTransaction];
        NSLog(@"%@",getCourse2);
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"%@",getCourse1);
        });
    });

8.從realm數(shù)據(jù)庫讀取出的數(shù)據(jù)模型,setter/getter方法會失效,集成realmObject的實(shí)力類setter/getter方法會失效,當(dāng)賦值的時候不會走set方法。
到這里我們已經(jīng)對Realm有了一定的了解,也熟悉了它的機(jī)制。

下面來說下在開發(fā)項(xiàng)目的時候具體碰到的問題:
一.數(shù)據(jù)解析轉(zhuǎn)換存儲,反轉(zhuǎn)換問題

由于項(xiàng)目中操作數(shù)據(jù)轉(zhuǎn)換的地方多,需要Json轉(zhuǎn)Model存入realm,獲取realm數(shù)據(jù)Model轉(zhuǎn)換成Json,但是realmSwift只支持把json轉(zhuǎn)換成realm所需的存儲Model,而不支持反轉(zhuǎn)。而Android的realm卻可以,這讓我很苦惱,而我又不想手動一二個一個來轉(zhuǎn)換,1是我們數(shù)據(jù)量太多,我覺得這種太耗費(fèi)精力2是也覺得這樣做有些low,于是乎遇到了瓶頸,逛各種技術(shù)論壇也沒有找到解決方案。靜下心來開始思考看HandyJson和realm的源碼,最后發(fā)現(xiàn)原來realm的數(shù)據(jù)類型是它自己定義的數(shù)組類型,而不是繼承iOSSwift的數(shù)據(jù)類型,這就造成HandyJson解析庫識別不了這些數(shù)據(jù)類型,最后導(dǎo)致沒辦法數(shù)據(jù)相互轉(zhuǎn)換。


realm數(shù)據(jù)類型

解決方案:
1.建立數(shù)據(jù)Model的時候需要在BaseModel里添加兩個方法函數(shù)解決list解析

import Foundation
import RealmSwift
import Realm
import HandyJSON

class BaseRLMObject: Object, NSCopying {
    func copy(with zone: NSZone? = nil) -> Any {
        return type(of: self).init()
    }
    
    //這個父類添加的屬性,子類解析不會賦值,因此在子類各自添加
//    @objc dynamic var primaryKey = UUID().uuidString
//    override static func primaryKey() -> String? {
//        return "primaryKey"
//    }
    
    //解析的Array數(shù)據(jù)添加到realm方法 例如:請求的Array數(shù)據(jù)需要添加到realm List數(shù)據(jù)庫時調(diào)用
     //注意點(diǎn):realmlist直接.append(objectsIn:)添加swift數(shù)組的時候,是可以添加到realmlist中的,原因realmlist數(shù)組能夠識別swift數(shù)組類型,但是反之就不行
    func addRealmData(){
        
    }
   
    //realm List數(shù)據(jù)傳遞給正常的Array方法 例如:realm List數(shù)據(jù)轉(zhuǎn)換成model Array時調(diào)用
    //注意點(diǎn):swift數(shù)組直接.append(contentsOf:)添加realmlist的時候,是添加不到正常數(shù)組里的,原因正常的swift數(shù)組不識別realmlist類型,但是反之就可以
    func addOriginalData(){
        
    }

}

2.子類需要繼承父類,然后實(shí)現(xiàn)這兩個方法,并且相同數(shù)組key屬性都需要創(chuàng)建兩個(一個是Json轉(zhuǎn)換Realm數(shù)據(jù)需要,一個是Realm數(shù)據(jù)轉(zhuǎn)換Json需要),每層都需要實(shí)現(xiàn)。
3.需要在HandyJson的ignoredProperties中忽略正常的list數(shù)據(jù),否則會在realm數(shù)據(jù)庫的字段表中出現(xiàn)該字段。
4.如果Bool型、Int型、Float型、Double型是需要非可空值的形式,則不需要特殊處理,但是如果這四種類型的數(shù)據(jù)是可空值形式,則需要特殊處理,轉(zhuǎn)換成String類型。原因是Bool、Int、Float、Double的可空值形式是RealmOptional<類型>(),解析庫識別不了realm自己定義的數(shù)據(jù)類型。
具體代碼:

import Foundation
import RealmSwift
import Realm
import HandyJSON

class PhotoModel : BaseRLMObject, HandyJSON {
    @objc dynamic var primaryKey = UUID().uuidString
    override static func primaryKey() -> String? {
        return "primaryKey"
    }

//    let id = RealmOptional<Int>()
    @objc dynamic var id: String? = nil
//    let vehicleId = RealmOptional<Int>()
    @objc dynamic var type: String? = nil
    @objc dynamic var delFlag:Bool = false // 刪除標(biāo)記
    let damageInfoList_realm: List<DamageInfoModel> = List<EQSDamageInfoModel>()//損傷點(diǎn)
    var damageInfoList: [DamageInfoModel] = []
    
    override static func ignoredProperties() -> [String] {
        return ["damageInfoList"]
    }
    
    override func addRealmData() {
        for item in self.damageInfoList {
            item.addRealmData()
        }
        if self.damageInfoList_realm.count > 0 && self.damageInfoList.count > 0 {
            self.damageInfoList_realm.removeAll()
        }
        self.damageInfoList_realm.append(objectsIn: self.damageInfoList)
    }
    
    override func addOriginalData() {
        if self.damageInfoList.count > 0 && self.damageInfoList_realm.count > 0{
            self.damageInfoList.removeAll()
        }
        
        for item in self.damageInfoList_realm {
            item.addOriginalData()
            self.damageInfoList.append(item)
        }
    }
}

在使用的時候每次轉(zhuǎn)換都需要調(diào)用add方法

//添加到realm數(shù)據(jù)庫
 if let object = JSONDeserializer<Model>.deserializeFrom(json:  json) {
                            object.addRealmData()
                            SXRealm.addAsync(object)
                    } 
//realm數(shù)據(jù)庫數(shù)據(jù)轉(zhuǎn)換成Json
 let model =  SXRealm.queryByPrimaryKey(DetailModel.self, primaryKey: detailModel.primaryKey)
 guard model == nil else {
      SXRealm.doWriteHandler {
              model.addOriginalData()   
      }
     let json =   mode.toJSON()!
 }
 
二.primaryKey主鍵問題

經(jīng)過測試逐漸定義不能在父類基礎(chǔ)類定義,必須要在各個子類都要定義。Realm的機(jī)制可能是檢測到這個字段有值就不會重新自動賦值,所以說不能偷懶在父類定義。

//這個父類添加的屬性,子類解析不會賦值,因此在子類各自添加
   @objc dynamic var primaryKey = UUID().uuidString
    override static func primaryKey() -> String? {
        return "primaryKey"
    }
三.刪除對應(yīng)數(shù)據(jù)問題

根據(jù)Realm提供的刪除方法,只能刪除該對象,卻不能刪除該對象相關(guān)聯(lián)的對象,這點(diǎn)感覺很坑,如果只刪除該對象后,其相關(guān)聯(lián)的對象就會變成臟數(shù)據(jù),永遠(yuǎn)保存在數(shù)據(jù)庫中,會造成體積越來越大。
解決方案:
1.采用代碼批量刪除方法,把該對象下邊的list中的數(shù)據(jù)循環(huán)刪除(先刪除子對象,再刪除外層對象)

 func deleteOrganizationUpgradeRealm() {
        let data = SXRealm.BGqueryByAll(OrganizationItem.self)
        
        if data.count > 0 {
            SXRealm.BGdelete(SXRealm.BGqueryByAll(ChildItem.self))
            SXRealm.BGdelete(SXRealm.BGqueryByAll(OrganizationItem.self))
        }
    }

  static func BGdelete<T: Object>(_ objects: Results<T>) {
        
        try! Realm().write {
            try! Realm().delete(objects)
        }
    }

2.采用遞歸方式刪除(對于復(fù)雜數(shù)據(jù)結(jié)構(gòu),但是數(shù)據(jù)量超級大的時候不建議使用此方法)

static func BGdeleteRealmCascadeObject(object:Object){
        for property in object.objectSchema.properties {
            if property.type == .object{
                if property.isArray{
                    let list:RLMArray<AnyObject> = RLMArray(objectClassName: property.objectClassName!)
                    list.addObjects(object.value(forKeyPath: property.name) as! NSFastEnumeration)
                    for i in 0..<list.count {
                        deleteRealmCascadeObject(object: list.object(at: i) as! Object)
                    }
                    
                } else {
                    let object:SXRLMObject = object.value(forKeyPath: property.name) as! SXRLMObject
                    if !object.isInvalidated{
                         try! Realm().delete(object)
                    }
                   
                }
                
            }
        }
        if !object.isInvalidated{
            try! Realm().delete(object)
        }
    }
四.修改更新操作realm對象時,需要在寫入操作中實(shí)現(xiàn),并且只能有一層寫入操作方法。
//在這如果做了doWrite操作,name在addOriginalData方法中就不能做都Write操作,否則Crash。
SXRealm.doWriteHandler {
             model.addOriginalData()
  }

 static func doWriteHandler(_ clouse: @escaping ()->()) { // 這里用到了 Trailing 閉包
        try! sharedInstance.write {
            clouse()
        }
    }
五.realm數(shù)據(jù)對象不能帶alloc、new、copy、mutableCopy之類的跟iOS語言相關(guān)的關(guān)鍵字、前綴字段,否則會造成Crash。(這點(diǎn)感覺好蛋疼)那么我們只能夠跟之前操作list的時候一樣,同樣的原理做橋接。
解決方法:
//解析使用  realm 不能有new alloc "copy", "mutableCopy" 等關(guān)鍵字前綴字段
var newVehicleSuggestionPrice: String? = nil
var newVehicleNetPrice:String? = nil
@objc dynamic var vehicleSuggestionPrice_realm: String? = nil
@objc dynamic var vehicleNetPrice_realm: String? = nil

//忽略realm數(shù)據(jù)庫對應(yīng)字段
override static func ignoredProperties() -> [String] {
       return ["newVehicleSuggestionPrice","newVehicleNetPrice"]
 }

 //注意點(diǎn):realmlist直接.append(objectsIn:)添加swift數(shù)組的時候,是可以添加到realmlist中的,原因realmlist數(shù)組能夠識別swift數(shù)組類型,但是反之就不行
 override func addRealmData() {
        self.vehicleSuggestionPrice_realm = self.newVehicleSuggestionPrice
        self.vehicleNetPrice_realm = self.newVehicleNetPrice
  }

//注意點(diǎn):swift數(shù)組直接.append(contentsOf:)添加realmlist的時候,是添加不到正常數(shù)組里的,原因正常的swift數(shù)組不識別realmlist類型,但是反之就可以
 override func addOriginalData() {
         self.newVehicleSuggestionPrice = self.vehicleSuggestionPrice_realm
         self.newVehicleNetPrice  = self.vehicleNetPrice_realm
  }
六.系統(tǒng)的數(shù)組和realm數(shù)組轉(zhuǎn)換問題

如果需要把系統(tǒng)的數(shù)組中的數(shù)據(jù)添加到realm數(shù)組中可以直接調(diào)用realm數(shù)組的.append(objectsIn: Sequence)方法

public func append<S: Sequence>(objectsIn objects: S) where S.Iterator.Element == Element {
        for obj in objects {
            _rlmArray.add(dynamicBridgeCast(fromSwift: obj) as AnyObject)
        }
}

但是如果需要把realm數(shù)組中的數(shù)據(jù)添加到系統(tǒng)的數(shù)組中,就不能使用系統(tǒng)的.append(contentsOf: Sequence)方法,而需要自己遍歷循環(huán)一個一個添加

//list_realm:realm數(shù)組類型變量    list:系統(tǒng)的長長數(shù)組類型變量
 for item in self.list_realm {
       self.list.append(item)
 }
七.description HandyJson解析問題

這個問題其實(shí)不是realm的問題,而是HandyJson的問題,HandyJson的時候?qū)τ贘son中的description字段是解析不成功的,按照正常操作是需要進(jìn)行一層轉(zhuǎn)換,但是又由于與realm的Model是同一個Model,兩者共同使用就造成了問題的出現(xiàn),想要轉(zhuǎn)換的變量必須以var來修飾,而realm中則需要@objc dynamic var來修飾,因此就出現(xiàn)了這個問題

解決方法:

需要中間創(chuàng)建個變量進(jìn)行橋接,在轉(zhuǎn)換的時候同時進(jìn)行賦值操作轉(zhuǎn)換。

import Foundation
import RealmSwift
import Realm
import HandyJSON

class XXXModel: SXRLMObject, HandyJSON{
    @objc dynamic var primaryKey = UUID().uuidString
    override static func primaryKey() -> String? {
        return "primaryKey"
    }
   
    //解析使用description關(guān)鍵字系統(tǒng)不支持
    var sdescription: String = ""http://圖片描述
    @objc dynamic var description_realm: String = ""http://圖片描述
    func mapping(mapper: HelpingMapper) {
        // specify 'description' field in json map to 'sdescription' property in object
        mapper <<<
            self.sdescription <-- "description"
    }
    
    override static func ignoredProperties() -> [String] {
        return ["sdescription"]
    }
    
    override func addRealmData() {
         self.description_realm = self.sdescription
    }
    
    override func addOriginalData() {
        self.sdescription = self.description_realm
    }
}
*以上就是RealmSwift的一些特性和我們項(xiàng)目中實(shí)踐過程踩過的坑。如果之后使用過程中碰到問題,會持續(xù)更新。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374

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