Android的兩種數(shù)據(jù)存儲方式分析(二)

二、SQLiteDatabase

做移動應(yīng)用的人,應(yīng)該沒有人不知道SQLite的吧粟瞬,但SQLite與其它的關(guān)系型數(shù)據(jù)庫有多大區(qū)別航揉?Android是怎么使用和操作SQLite的?SQLite的性能怎么樣临扮?平時困擾我們的各種數(shù)據(jù)庫異常都是怎么會事兒?有沒有什么解決辦法?帶著這些問題佩番,我們來深入學習一下Android中的SQLite吧!

1罢杉、SQLite的優(yōu)勢趟畏,Android為什么要選擇SQLite

(1)SQLite是一個單進程的數(shù)據(jù)庫(和應(yīng)用程序運行在一個進程中),不分Server/Client滩租。

(2)所有數(shù)據(jù)保存在一個文件中(transaction操作過程中會有一些事務(wù)記錄文件)赋秀,跨平臺。

(3)免安裝直接使用律想。

(4)驅(qū)動引擎包很小猎莲,500K,去掉一些不常用的功能技即,可以縮到300K著洼。

(5)資源占用非常少,可以運行在100K內(nèi)存的設(shè)備上姥份。

(6)在讀寫效率郭脂、消耗總量、延遲時間和整體簡單性上具有的優(yōu)越性

(7)完全免費澈歉,商用也免費展鸡;收費版有加密的功能。

它從設(shè)計開始就是為嵌入式設(shè)備服務(wù)的埃难,所以很適合手機莹弊,它不像其它的數(shù)據(jù)庫,啟動后會有一個Server的進程涡尘,訪問時再起一個客戶端忍弛,通過IPC進行通信。實際上SQLite現(xiàn)在的性能已經(jīng)很不錯了考抄,有不少小的網(wǎng)站也在用它细疚,SQLite的官網(wǎng)上說,它可以支持一天10~30W的訪問量川梅。

2疯兼、SQLite的數(shù)據(jù)類型

SQLite支持的數(shù)據(jù)類型比別的數(shù)據(jù)庫要少很多然遏;SQLite的數(shù)據(jù)存儲是動態(tài)數(shù)據(jù)類型的。

SQLite數(shù)據(jù)庫只支持五種數(shù)據(jù)類型吧彪,基本已經(jīng)抽象到極高的層次了待侵,而實際上我們只用到4種:

SQlite的五種數(shù)據(jù)類型

Null這種類型表示一個空值 ;Integer表示數(shù)學上的整數(shù)姨裸;Real表示數(shù)學上的小數(shù)秧倾;Text表示文本,計算機中叫字符串傀缩;Blob表示二進制那先;仔細想想,這幾種值的確已經(jīng)涵蓋了平常我們用到的各有種情況赡艰。如果真想再抽象胃榕,blob可以表示一切啦,計算機里一切都是byte嘛瞄摊。

再說說SQLite的動態(tài)數(shù)據(jù)類型,或者也可以叫弱數(shù)據(jù)類型:SQLite定義的表中的每一列是可以存儲非指定的類型的數(shù)據(jù)的苦掘,比如我們的表定義如下:

一個Sqlite的表定義

如果我們在Android中使用:

contentValue.put("group_name","group_name");
contentValue.put("group_name", new byte[]{1,2 3});
contentValue.put("group_name", 123);

在做插入或修改操作時换帜,都是可以執(zhí)行成功的,看起來GROUP_NAME的類型約束TEXT根本就不生效鹤啡,實際上惯驼,我把這那個類型去掉:

無類型定義的表創(chuàng)建語句

SQLite照樣能把這個張表創(chuàng)建出來,可見對于SQLite而言递瑰,類型根本不是個必要條件祟牲,任何一列都可以存不同類型的數(shù)據(jù),這也是SQLite區(qū)別于其它關(guān)系型數(shù)據(jù)庫的一個重要地方抖部。

我們在平時使用時可能也有發(fā)現(xiàn)另一種現(xiàn)象说贝,我存入的是一個String="123",但在cursor.getInt()時慎颗,竟然真的能得到int = 123乡恕,而你在使用cursor.getType(columnIndex)時獲取到的對應(yīng)字段的類型又的確是Text的,太混亂了俯萎。

另外傲宜,我們也注意到Cursor提供的方法,比如對于整數(shù)夫啊,它提供了以下方法:

getInt(int columnIndex)
getLong(int columnIndex)
getShort(int columnIndex)

而SQLite只有INTEGER這一種類型呀函卒,這怎么對應(yīng)起來的呢?

這是不是說撇眯,SQLite根本沒有數(shù)據(jù)類型可言报嵌?或者SQLite根本就不需要數(shù)據(jù)類型虱咧?

答案是NO,SQLite從來都沒有改變他自己定義的數(shù)據(jù)類型沪蓬,他內(nèi)部仍然存儲著五種數(shù)據(jù)類型彤钟,他做的僅僅是列內(nèi)數(shù)據(jù)動態(tài)而已,其它的工作都是Android自己做的跷叉,我們來看看Cursor的代碼逸雹,先看getshort和getInt:(源代碼在:CursorWindow.java)

Cursor的getShort和getInt

太暴力了,直接從long轉(zhuǎn)過來云挟,也不必數(shù)據(jù)丟失梆砸。再看同學暴力的getFloat和getDouble:

getFloat

從這里可以看出,SQLite內(nèi)部的確只存儲基本數(shù)據(jù)類型Integer和Real园欣,對應(yīng)于Java就是最大字節(jié)長度的long和double帖世,而使用時直接強轉(zhuǎn),風險由RD自己承擔沸枯,所以我們在寫程序時對用哪種get要自己小心日矫,保證你存的是什么類型的數(shù)就用什么來取,如果真的擔心绑榴,那就直接用getLong和getDouble吧哪轿,它們不會丟失數(shù)據(jù)。

好翔怎,我們再來看第二個問題窃诉,為什么存入一種類型,可以用另一種類型來get赤套,還是直接上個代碼飘痛,從上面的getDouble代碼來看,真正獲取數(shù)據(jù)是用JNI實現(xiàn)的容握,我們先來看nativeGetDouble:(源代碼在:android_database_CursorWindow.cpp)

JNI中g(shù)etDouble

從上面代碼可以看出宣脉,SQLite內(nèi)部還是有類型的,只不過Android為了大家使用方便唯沮,對每種類型做了轉(zhuǎn)換脖旱,如果存的是String,它會用strtod方法轉(zhuǎn)換一下(string to double)介蛉,這個方法并不是所有string都能轉(zhuǎn)過來的萌庆,如果不是一個小數(shù)形式的币旧,它會返回0.0的践险;再看最后一個紅框,如果你存入的是blob二進制,它就無能為力啦巍虫,給你拋個異常彭则。

getLong和getDouble是類似的,我們再來看看getString的實現(xiàn):

nativeGetString

注意紅框部分占遥,String使用utf16的形式俯抖;數(shù)向串轉(zhuǎn)換使用的是sprintf;string跟blob類型也是不兼容的瓦胎,也會出異常芬萍。我們再來看看另一個讓人疑惑的地方,nativeGetBlog:

nativeGetBlob

疑惑就是string不兼容blob搔啊,但blob可不管你柬祠,string它照樣按字節(jié)數(shù)組給讀出來,可能blob是更加底層的形態(tài)吧负芋;便我們往下面兩個紅框看漫蛔,blob又不兼容integer和real。

總結(jié)一下吧:

(1)SQLite的內(nèi)部設(shè)計是每一列內(nèi)都可以存放不同類型的數(shù)據(jù)旧蛾,但我們建議大家還是按強類型的關(guān)系型數(shù)據(jù)庫來莽龟,顯式定義列類型,同時列中的確存這種類型的數(shù)據(jù)锨天,這樣可以增加代碼的可讀性轧房,又能減少潛在和隱藏bug的出現(xiàn);

(2)盡管cursor的get可以獲取一個非它存入的類型绍绘,但這個的正確性需要程序員自己來保證,而且一不小心還可能被它拋出異常迟赃,所以陪拘,建議同上,我們按強類型庫的要求來做纤壁,存什么就取什么吧左刽。

3、SQLite的線程模式

Sqlite是怎么處理多線程情況的酌媒?

實際上欠痴,SQLite的引擎對多線程做了處理的,要注意秒咨,這是sqlite層做的處理喇辽,指的是,打開一個庫后雨席,多個線程去處理這個庫菩咨,而不是多個線程各自去打開這個庫。我們看看SQLite是的各種線程模式:

SQLite的線程模式

(1)單線程模式,實際指的是數(shù)據(jù)庫引擎不加鎖抽米,如果調(diào)用者使用了多線程特占,那調(diào)用都自己來保證同步。這種模式下云茸,數(shù)據(jù)庫內(nèi)部的操作效率是最高的是目。

(2)多線程模式,這種模式下标捺,可以多線程操作數(shù)據(jù)庫懊纳,但有一個要求,一個DBconnection只能被一個線程使用宜岛,這一點也要求調(diào)用者自己來保證长踊。

(3)順序模式,此模式安全性最高萍倡,隨便操作數(shù)據(jù)庫身弊,當然,效率也最低列敲。

那Android使用的是哪種模式呢阱佛,我們看一下代碼:(android_database_SQLiteGlobal.cpp)

android 6.0上SQLite初始化的代碼

從上面代碼我們也可以看出,在Android6.0上戴而,使用的是多線程模式凑术。

我們?nèi)绻⒁獾絊QLiteDatabase的接口,會發(fā)現(xiàn)這樣一個方法:

SQLiteDatabase的一lock方法

可以看到所意,SQLiteDatabase提供了一個鎖來控制線程同步淮逊,但這個方法在api level 16就不建議用了,這又是為什么呢扶踊?我們?nèi)タ纯碅piLevel 16以前的SQLite配置吧泄鹏。

在jni中找了所有的database*.cpp文件,都沒有找到這個配置秧耗,再回去看看SQLite配置多線程模式的方法:

SQLite的線程模式配置方法

從這幾個配置方法和默認配置方法备籽,我猜測4.0及以前版本的SQLite用的不是默認的serialized模式,因為用這個模式Android就不用自己再加鎖了分井;那它應(yīng)該用的是編譯時設(shè)置線程模式车猬,而且是單線程模式。

那Android4.0以上是怎么實現(xiàn)每個connection只被一個線程操作的呢尺锚?我們接下來分析Android操作SQLiteDatabase的方法就能發(fā)現(xiàn)了珠闰。(4.0及以下使用的是鎖的方式,我們就不細看了)

4瘫辩、Android是如何實現(xiàn)操作SQLiteDatabase的

Android操作數(shù)據(jù)庫的流程比較簡單铸磅,牽扯到的類其實不多赡矢,一個簡單的圖就可以看清楚:

Sqlite操作流程

一句話描述整個流程就是:openHelper打開數(shù)據(jù)庫后,構(gòu)建DBConnection阅仔,使用sqliteQuery和SqliteStatement操作數(shù)據(jù)庫吹散。是不是很簡單,但它里面實際上還是有很多其它代碼邏輯八酒,我們主要從三個方面分析:openDatabase空民、session和兩個操作方法query+statement。

(1)Open Database

我們在使用db時羞迷,先要通過SQLiteOpenHelper獲取到SQLiteDatabase界轩,一般獲取方法有兩種:getReadableDatabase和getWritableDatabase,猛一看衔瓮,這兩個方法還挺有迷惑性浊猾,好像一個是得到一個read only的庫,一個用于得到一個可寫的庫热鞍,我們看看代碼中是這樣的嗎葫慎?

打開SQLiteDatabase

先注意第一個紅框,這兩個方法都是加了synchronized的薇宠,不會因為線程問題創(chuàng)建出兩個SQLiteDatabase;第二偷办、三個紅框 可以看出,他們都是調(diào)用了getDatabaseLocked方法澄港,只是參數(shù)不一樣椒涯,參數(shù)表明了打開哪種db;第四個紅框里的條件可以發(fā)現(xiàn),只要是非writable回梧,當前緩存的db就可能返回了废岂,或者當前數(shù)據(jù)是writable的,也可以返回了狱意,所以泪喊,獲取readable的數(shù)據(jù)庫并不一定返回的是只讀的哦,下面還有更吐血的髓涯。

打開Sqlite數(shù)據(jù)庫

第一個紅框比較好理解,要打開一個writable的哈扮,當前是readOnly的就重新打開一次纬纪;看第二個紅框,這個值是debug用的滑肉,真實環(huán)境一直是false包各,所以這個if是不會走的;看第三個紅框靶庙,我們發(fā)現(xiàn)打開數(shù)據(jù)庫時问畅,根本不管是要readable還是writable,統(tǒng)一打開Writable的,那還要傳進來的writable參數(shù)干什么呢护姆?再往下看矾端,第五個紅框,在打開readable失敗時卵皂,這時才去判斷秩铆,如果我們計劃打開的是readable的,它才去嘗試用readable去打開灯变。

結(jié)論:不管getReadableDatabase還是getWritableDatabase殴玛,OpenHelper都是優(yōu)先去打開writable的,對于getReadableDatabase的作用添祸,只是在打開writable失敗時(比如磁盤滿了),才會用第二案滚粟,嘗試用readable打開一下。

打開SQLiteDatabase

接下來就比較簡單了刃泌,同步的把各種on*方法回調(diào)走一遍凡壤,注意,這里的調(diào)用是同步依次調(diào)用蔬咬,不會有什么線程問題的鲤遥。

(2)SQLiteSession

SQLiteDatabase在操作數(shù)據(jù)庫時,都要獲取到一個session后才能開始操作林艘,從session這個單詞我們看以看出盖奈,它就像一個client一樣。

transaction狐援,query钢坦,statement都是要先獲取一個session

從這幾個方法可以看出,數(shù)據(jù)庫的操作都是要先獲取到session才可以操作啥酱,我們看看session定義的地方爹凹。

SQLiteDatabase中的SQLiteSession

我們可以看到,Session是用threadLocal保存的,SQLiteDatabase正是通過ThreadLocal來保證了每個線程拿到的Session唯一镶殷,線程結(jié)束了禾酱,它也就被釋放了。每個session中有一個dbconnection绘趋,此時颤陶,andriod已經(jīng)做到了每個connection只屬于一個線程,符合SQLite線程模式Multi-Thread的要求陷遮。

SQLiteSession的類注釋寫了好多內(nèi)容滓走,對我們理解數(shù)據(jù)庫操作非常有幫助,(我使用Google翻譯了一下帽馋,翻譯的很難懂搅方,還是直接看英文看的更加明白):

sqliteSession說明

database只能用session訪問數(shù)據(jù)庫比吭;多個readonly操作可以并行執(zhí)行,寫操作只能串行姨涡;session不是線程安全的衩藤,DB通過ThreadLocal來保證安全;多線程同時操作connection绣溜,有可能造成死鎖慷彤。

sqliteSession說明

transaction有兩種,隱式和顯式怖喻;平時的任何直接操作都會起一個隱式的底哗;只有調(diào)用beginTransaction,才發(fā)起一個顯式的transaction锚沸;transaction是可以嵌套的跋选,嵌套內(nèi)的任何一個沒有succesfull,最外層的都會回滾哗蜈;如果一個transaction執(zhí)行時間太長前标,可以通過yieldTransaction來讓出一段時間數(shù)據(jù)庫操作,但讓出這前的提交內(nèi)容會被commit距潘,且不會回滾(比較雞肋)炼列。

顯示transaction用法

顯式的transaction必須這樣用,try + finaly音比,像lock的用法一樣俭尖,否則會再現(xiàn)死鎖的情況。

connection和responsiveness

一個數(shù)據(jù)庫的connection是有限的洞翩,多線程并發(fā)比較高時稽犁,一些sessoin在獲取connection是要等待的;Android做了一個connection的pool來提高效率骚亿,實際上這個pool的max size很小的(系統(tǒng)配置)已亥,也就2~5個的樣子;

為了提升數(shù)據(jù)庫操作響應(yīng)速度来屠,一定要減少transaction執(zhí)行時間虑椎,特別是writable的,它們要串行執(zhí)行俱笛;為增加響應(yīng)速度和流暢性捆姜,有幾個需要注意的點:

[1]不要在主線程操作數(shù)據(jù)庫;
[2]保持transaction時間盡可能短嫂粟;
[3]簡單的查詢條件要比復(fù)雜查詢條件的速度快的多;
[4]表內(nèi)數(shù)據(jù)量的大小是非常影響查詢速度的墨缘,100行的表跟10000行的表完全不是一個速度級的星虹。

(3)關(guān)于query和statement就不細講了零抬,我們只要理解兩點:

[1]所有的query操作,包括rawQuery宽涌,最終都是組成一個SQLiteQuery對象來執(zhí)行平夜;
[2]所有的增、刪卸亮、改操作忽妒,包括executeSQL,最終都是組成一個SQLiteStatement對象來執(zhí)行兼贸;

具體代碼大家可以從SQLiteDatabase中相關(guān)方法跟下去看看:
https://android.googlesource.com/platform/frameworks/base/+/android-6.0.1_r77/core/java/android/database/sqlite/

5段直、SQLite的讀寫性能和內(nèi)存占用情況

先說讀寫性能,我自己做的測試溶诞,一般的手機
query100條以內(nèi):1ms
query500條以內(nèi):<10ms
query1000條:>10ms

插件操作:
Insert一條記錄:20ms

從上面的數(shù)據(jù)可以看出鸯檬,正常情況下查詢少量數(shù)據(jù)是非常快的螺垢,這里說的是正常情況喧务,所以,如果我們只是查很少量的數(shù)據(jù)枉圃,有時可以放在主線程操作功茴,但是不建議,個別手機上還是有可能anr孽亲;

修改操作很快坎穿,必須放在工作線程完成。

關(guān)于內(nèi)存占用情況墨林,我測試打開一個庫赁酝,建立一個connection,正常情況下一個connection會占用2K左右的內(nèi)存旭等,而庫的主要內(nèi)存就在這里酌呆,看connectionPool的大小了。
所以搔耕,對于數(shù)據(jù)庫內(nèi)存方面的建議是:如果這個庫要經(jīng)常操作隙袁,可以不用關(guān),占用內(nèi)容并不多弃榨,因為再打開一次開銷還是挺大的(50~100ms)菩收;如果不常用的數(shù)據(jù)庫,還是及時關(guān)閉吧鲸睛。

6娜饵、開發(fā)中如何正確的使用SQLite

關(guān)于使用數(shù)據(jù)庫的建議:

(1)保證sqliteOpenHelper單例,或者全局唯一官辈,不要讓一個庫在一個進程中同時存在多個OpenHelper箱舞;

(2)不要在多個進程中操作同一個庫遍坟,如果有多進程的情況,使用contentProvider來操作庫吧晴股,各進程都通過contentProvider來獲取數(shù)據(jù)愿伴;

(3)數(shù)據(jù)庫是否需要關(guān)閉根據(jù)業(yè)務(wù)情況來,它占用的內(nèi)存其實不多电湘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末隔节,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子寂呛,更是在濱河造成了極大的恐慌怎诫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昧谊,死亡現(xiàn)場離奇詭異刽虹,居然都是意外死亡,警方通過查閱死者的電腦和手機呢诬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門涌哲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人尚镰,你說我怎么就攤上這事阀圾。” “怎么了狗唉?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵初烘,是天一觀的道長。 經(jīng)常有香客問我分俯,道長肾筐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任缸剪,我火速辦了婚禮吗铐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杏节。我一直安慰自己唬渗,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布奋渔。 她就那樣靜靜地躺著镊逝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嫉鲸。 梳的紋絲不亂的頭發(fā)上撑蒜,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音,去河邊找鬼座菠。 笑死染突,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的辈灼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼也榄,長吁一口氣:“原來是場噩夢啊……” “哼巡莹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起甜紫,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤降宅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后囚霸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腰根,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年拓型,在試婚紗的時候發(fā)現(xiàn)自己被綠了额嘿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡劣挫,死狀恐怖册养,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情压固,我是刑警寧澤球拦,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站帐我,受9級特大地震影響坎炼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拦键,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一谣光、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧矿咕,春花似錦抢肛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至莲镣,卻和暖如春福稳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瑞侮。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工的圆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鼓拧,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓越妈,卻偏偏與公主長得像季俩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子梅掠,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,516評論 25 707
  • 作為一個完成的應(yīng)用程序酌住,數(shù)據(jù)存儲操作是必不可少的。因此阎抒,Android系統(tǒng)一共提供了四種數(shù)據(jù)存儲方式酪我。分別是:Sh...
    AiPuff閱讀 542評論 0 0
  • 驚蟄天氣,風雨如晦且叁。 粉桃玉李紛繁都哭,與豆蔻白芷一道提前綻開了美的盛宴。 山村小樓一夜春雨逞带。夢里都是馬尾松翻涌的波...
    疏蟬閱讀 430評論 0 2
  • 短短三日欺矫,北京之行結(jié)束。伴著腳板的酸痛和游玩的余興未盡展氓,決定記錄這次旅行汇陆。 前言 兩個人,來返火車带饱≌贝看似很辛苦,卻...
    c28369096728閱讀 288評論 0 0
  • 關(guān)于選擇勺疼,首先要了解一個概念“奧卡姆剃刀”教寂。維基百科的解釋有:切勿浪費較多的東西,去做用較少的東西同樣可以做好的事...
    雷哥復(fù)利筆記閱讀 323評論 1 1