之前閑著無聊,研究了下 Android 存儲方面的知識,順便翻譯了下官方文檔(雖然有已經(jīng)被翻譯過...)。這里就算是水一篇博客好了 # ̄▽ ̄#
1.1 應用數(shù)據(jù)存儲
Android為你提供了一些用來持久保存應用數(shù)據(jù)的選擇。你所選擇的解決方案依賴于你明確的需求,例如數(shù)據(jù)對于你的應用來說是否是私有的,還是能被其他應用(以及用戶)所獲取,以及你的數(shù)據(jù)需要多大的存儲空間。
你的數(shù)據(jù)存儲方式為以下幾種:
(1) Shared Preferences 共享偏好
以鍵值對的方式存儲私有的簡單的數(shù)據(jù)
(2) Internal storage 內(nèi)部存儲空間
存儲私有數(shù)據(jù)在設(shè)備的內(nèi)存上
(3) External storage 外部存儲空間
存儲公共數(shù)據(jù)在共享的外部存儲上
(4) SQLite Databases 數(shù)據(jù)庫
存儲結(jié)構(gòu)化數(shù)據(jù)在私有的數(shù)據(jù)庫中
(5) Network Connection網(wǎng)絡(luò)連接
在網(wǎng)絡(luò)上用你自己的網(wǎng)絡(luò)服務器存儲數(shù)據(jù)
Android為你提供了一種為其它應用程序暴露私有數(shù)據(jù)的方式來——使用 content provider . (內(nèi)容共享)。content provider 是一種可選的組件,它為那些受限于你施加限制的應用數(shù)據(jù)暴露了讀寫方法。需要更多有關(guān)使用conten providers的信息,請見content providers文檔
1.1.1 使用共享偏好
SharedPreferences 該類 提供了一種通用的框架允許你去保存和重新獲取持久性的、以鍵值對方式保存的原始數(shù)據(jù)種類。你可以使用SharedPreferences去保存任意的原始數(shù)據(jù):布爾類型、單精度浮點數(shù)、整型、長整型、和字符串類型。這種數(shù)據(jù)會持續(xù)作用在整個用戶會話期間(即使你的應用被銷毀了)。
為了在你的應用中獲取sharedpreferences 對象,可以使用下列兩種方法之一:
· getSharePreferences() – 若果你需要獲取多種以文件名區(qū)分的偏好文件,請使用這種方法。可以用第一個參數(shù)來指定文件。
· getPreferences() – 若果你只需要一個偏好文件在你的Activity中,請使用這種方法。因為這種方法會為你的Activity提供唯一的preferences 文件,而你不需要提供文件名。
寫入值:
(1) 調(diào)用 edit() 函數(shù)來獲取一個 SharedPreferences.Editor。
(2) 使用諸如putBoolean()和putString()等方法來添加值。
(3) 使用commit()方法來提交新的值
讀取值,使用SharePreferences的方法,諸如getBoolean()和getString。
以下是一個例子,它在計算器中保存了靜音模式的偏好。
public class Calc extends Activity {
public static final String PREFS_NAME = "MyPrefsFile";
@Override
protected void onCreate(Bundle state){
super.onCreate(state);
. . .
//存儲偏好
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
boolean silent = settings.getBoolean("silentMode", false);
setSilent(silent);
}
@Override
protected void onStop(){
super.onStop();
// 我們需要一個Editor對象來保存偏好的改變
// 所有的對象都來自于android.context.Context
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("silentMode", mSilentMode);
// Commit the edits!
editor.commit();
}
}
1.1.2 使用內(nèi)部存儲
你可以直接在你的設(shè)備內(nèi)部存儲中保存文件。默認情況下,保存在內(nèi)部存儲中的文件對你的應用來說是私有的,其它應用不能獲取它們(即使是用戶)。當用戶卸載你的應用時,這些文件也會被刪除。
在你的內(nèi)部存儲中創(chuàng)建和寫一份私有文件:
(1) 通過文件名和操作模式調(diào)用 openFileOutput()。它將返回一個FileOutPutStream對象。
(2) 使用Write() 方法,對文件執(zhí)行寫操作
(3) 使用close() 方法關(guān)閉流
舉個例子
String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();
MODE_PRIVATE 會創(chuàng)建一個文件(或者取代原有的同名文件),并且使它對你的應用來說是私有的。
其它可以使用的模式有:MODE_APPEDN MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE
從內(nèi)部存儲中讀取文件
(1) 調(diào)用 openFileInput() 并傳遞文件名。它將返回一個FileInputStream對象
(2) 使用read()方法讀取字節(jié)
(3) 使用close()方法關(guān)閉流
小提示:在應用中,如果你想在編譯的時候保存一個靜態(tài)的文件,那么請在工程的res/raw/目錄保存它。你可以調(diào)用openRawResource()方法來打開它,通過傳遞R.raw.<Filename>的id,這個方法返回一個InputStream對象,你可以用它來讀取文件(但是你不能用他來寫入原始文件)
? 存儲緩存文件
如果你想要臨時存儲一些數(shù)據(jù),而不是永久性的存儲它。你應該使用getCacheDir()方法去打開一個文件。這個文件代表一個內(nèi)部存儲路徑,在這你可以存儲臨時的緩存文件。
當設(shè)備的內(nèi)部存儲空間較低時,Android系統(tǒng)可能會刪除這些緩存文件來恢復空間。然而,你不應該依賴于你的系統(tǒng)去為你清理這些文件。你應該始終自己去維持這些緩存文件,并且控制它的空間消耗在一個合理的范圍內(nèi),例如1MB。當用戶卸載你的應用時,這些文件將會被刪除。
? 其它有用的方法
getFilesDir()
獲取在文件系統(tǒng)中,你內(nèi)部文件保存的絕對路徑。
getDir()
在你內(nèi)部存儲空間中創(chuàng)建(如果存在則打開)路徑。
deleteFile()
刪除內(nèi)部存儲中的文件。
fileList();
以數(shù)組的方式返回你應用中存儲的文件列表。
1.1.3使用外部存儲
任何兼容Android的設(shè)備都支持一個共享的外部存儲,你可以在這里存儲文件。外部存儲可以是一種可移除的存儲媒體(例如 SD card)或者內(nèi)部的(不可移除的)儲存。當用戶接通了USB大容量存儲設(shè)備在電腦上傳輸文件時,保存在外部存儲上的文件時世界可讀的(world-readable)并且可以被用戶修改。
注意:外部存儲可能會變成不可獲得的狀態(tài),如果用戶將外部存儲連接到電腦上,或者用戶移除了媒體。并且,由于沒有一種作用于文件的強制安全措施,所用的應用都可以讀或者寫那些存放在外部存儲的文件,用戶也可以刪除它們。
? 獲得對外部存儲的訪問
為了讀寫外部存儲上的文件,你的應用必須獲取READ_EXTERNAL_STORAGE 或者 WRITE_EXTERNAL_STORAGE 兩個系統(tǒng)權(quán)限。舉個例子:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
如果你需要同時獲得讀和寫文件的權(quán)限,那么你只需要請求WRITE_EXTERNAL_STORAGE 權(quán)限。因為它也間接的請求了讀的權(quán)限。
提示:從android 4.4開始,這些權(quán)限并不是必須的如果你只是去讀寫你應用的私有文件。想要知道更多信息,可以查看以下章節(jié)關(guān)于 存儲應用私用的文件。
? 查看媒介是否可獲得
在你做任何有關(guān)外部存儲的工作之前,你應該總是先調(diào)用getExternalStorageState()方法來檢查媒介是否可獲得。媒介可能連接到了電腦,未找到,只讀,或者處于其它狀態(tài)。例如,你可以使用以下一堆代碼檢查外部存儲的狀態(tài)。
/* 檢查外部存儲是否可獲得以用來進行讀和寫操作*/
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
/* 檢查外部存儲是否可獲得并至少能進行讀操作*/
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
這個例子檢查了外部存儲設(shè)備是否可獲得以便去進行讀寫操作。getExternalStorageState()方法返回了一些你可能想要檢查的其它狀態(tài)。例如,媒介是否能被共享(連接到電腦),是否完全未找到,還是已經(jīng)被永久的移除,等等。當你的應用需要連接到媒體的時候,你可以使用這些狀態(tài)以告知用戶更多的信息。
? 存儲可以被其它應用共享的文件
通常來講,用戶可能會通過你的應用獲得新的文件,這些文件應該被保存在設(shè)備上的公有空間中。這樣其它應用就能獲取它們,并且用戶也能很容易拷貝這些文件。當你這樣做時,你應該使用其中一種共享空間的路徑,例如 Music/,Pictures/,and Rintones/
為了獲得一個代表合適的公有路徑的File對象,調(diào)用getExternalStoragePublicDirectory()方法,并傳遞一個你想要的路徑類型,例如DIRECTORY_MUSIC, DIRECTORY_PICTURES, DIRECTORY_RINGTONES,或其它。通過合理的存放你的文件在對應的媒體種類路徑下,系統(tǒng)的媒體掃描器就能合理的在系統(tǒng)中分類你的文件(例如,鈴聲會在系統(tǒng)設(shè)置中以鈴聲出現(xiàn),而不是音樂)。
舉個例子,以下是一種在公有相冊路徑下,為新的相冊創(chuàng)建路徑發(fā)方法。
public File getAlbumStorageDir(String albumName) {
// 獲取用戶公有的相冊路徑
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
? 存儲應用私有的文件
如果你正在處理一些不打算被其他應用使用的文件(例如,只會被你的應用使用的圖像紋理或者音效),你應該使用getExternalFilesDir()在外部存儲上創(chuàng)建一個私有存儲路徑。這個方法也需要一個類型參數(shù)來指明子路徑的類型(例如 DIRECTORY_MOVIES),如果不需要一個明確的媒體路徑,傳值null來獲得你應用私有路徑的根路徑。
從Android4.4開始,讀寫應用私有路徑中的文件不再需要 READ_EXTERNAL_STORAGE 或者WRITE_EXTERNAL_STORAGE 兩個權(quán)限。所以這兩個權(quán)限只有在maxSdkVersion的版本低于18時,才需要被聲明。
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest>
注意:當用戶卸載了你的應用時,這個路徑及其中的內(nèi)容都會被刪除。系統(tǒng)媒體掃描器也不會去讀取這些路徑下的文件。所以這些文件在meiaStore的content provider中是獲取不到的。同理,你也不應該在這些路徑存下存放那些本質(zhì)上屬于用戶的媒體,例如通過你的應用捕獲或處理過的照片,或者用戶通過你的應用購買的歌曲,這些文件看起來本應該被保存在共享路徑中的文件。
有時候,設(shè)備可能會分配一部分內(nèi)部存儲作為外部存儲,當然也可能會提供SD卡的卡槽。當這種設(shè)備運行在Android4.3或者更低的版本上時,getExternalFilesDir()方法只會提供一本分內(nèi)部存儲的使用權(quán),你的應用并不能讀寫SD卡上的文件。然而,從Android 4.4開始,通過getExternalFilesDirs()方法,你即可以獲得外部存儲的使用權(quán),也可以獲得內(nèi)部存儲的使用權(quán)。它返回一個File對象的數(shù)組,該數(shù)組包含每一個位置的入口路徑。數(shù)組中所包含的第一個入口路徑,被認為是主要的外部存儲,并且你應當使用這一場所,除非它已經(jīng)被占滿或者是不可獲得的。如果你想要在Android4.3或以下版本獲得兩者的路徑,可以使用支持包中的靜態(tài)方法。ContextCompat.getExternalFilesDirs(). 這個方法也返回了一個File對象的數(shù)組,但在Android4.3或以下的版本上,它通常只包含一個入口。
注意:盡管getExternalFilesDir()和getExternalFilesDirs()方法提供的路徑并不會被MediaStore的Content provider獲得。但是,其它擁有READ_EXTERNAL_STORAGE權(quán)限的應用可以獲得所有外部存儲上的文件,并包含它們。如果你需要嚴格的限制你的文件的使用權(quán),你應該將你的文件寫在內(nèi)部存儲上。
? 存儲緩存文件
想要打開一個代表著在外部存儲上存放緩存文件的路徑,你可以調(diào)用getExternalCacheDire()方法,如果用戶卸載你的應用,這些文件會自動被刪除。
類似于上述所提到的ContextCompat.getExternalFilesDirs()方法,你同樣可以獲取在第二個外部存儲上的緩存入口(如果可以獲得的話),通過調(diào)用ContextCompat.getExternalCacheDirs()方法。
小提示:為了保護你的文件存儲空間,并維持你應用的表現(xiàn)。在整個應用的生命周期中,細心的管理你的緩存文件,并在它們不被需要的時候移除它們這些,這是很重要的。
1.1.4使用數(shù)據(jù)庫
Android 提供了完整的SQLite數(shù)據(jù)庫的支持。任何類都能通過數(shù)據(jù)庫名稱來訪問你創(chuàng)建的數(shù)據(jù)庫,但是該應用之外的類側(cè)不能。
建議通過繼承SQLiteOpenHelper類來創(chuàng)建新的SQLite數(shù)據(jù)庫,并重寫其中的onCreate()方法,在該方法中你可以執(zhí)行一條SQLite命令以便在數(shù)據(jù)庫中創(chuàng)建表。
舉例:
public class DictionaryOpenHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 2;
private static final String DICTIONARY_TABLE_NAME = "dictionary";
private static final String DICTIONARY_TABLE_CREATE =
"CREATE TABLE " + DICTIONARY_TABLE_NAME + " (" +
KEY_WORD + " TEXT, " +
KEY_DEFINITION + " TEXT);";
DictionaryOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DICTIONARY_TABLE_CREATE);
}
}
然后你可以通過你定義的構(gòu)造器來獲得繼承SQLiteOpenHelper的類的實例。為了向數(shù)據(jù)庫中寫入和讀出數(shù)據(jù),可以分別調(diào)用getWriteableDatabase()和getReadableDatabase()方法。它們都會返回一個SQLiteDatabase對象,它代表著一個數(shù)據(jù)庫對象并向外提供對SQLite的操作方法。
通過使用SQLiteDatabase query()方法,你可以對SQLite數(shù)據(jù)庫執(zhí)行查詢操作,它可以接受不同種類的查詢參數(shù),例如表名、投影、選擇運算、列名、分類以及其它參數(shù)。為了執(zhí)行一些更復雜的查詢操作,例如那些需要列的別名的操作,你應該使用SQLiteQueryBuilder這個類,它提供了一些便捷的操作方法來構(gòu)建查詢操作。
每一次SQLite的查詢操作都會返回一個游標,它指向所有查詢結(jié)果的行。游標的原理是使你通過它可以駕馭你從數(shù)據(jù)庫中查詢的結(jié)果,并讀取行和列。
想獲得一些在Android中演示如何使用SQLite數(shù)據(jù)庫的例子,可以查看Note Pad和Searchable Ditionary 這些應用。
? 數(shù)據(jù)庫調(diào)試
Android SDK 包含了一個 sqlite3 數(shù)據(jù)庫工具,它允許你瀏覽表的內(nèi)容,運行SQL命令,并執(zhí)行其它有用的SQLite數(shù)據(jù)庫操作。查閱Examining sqlite3 databases from a remote shell 來學習如何使用該工具。
1.1.5使用網(wǎng)絡(luò)連接
你可以使用網(wǎng)絡(luò)(當可獲得時) 在你自己的基于web的服務上,來存儲并重新獲取數(shù)據(jù),為了獲得更多的聯(lián)網(wǎng)操作,你可以使用以下包中的類:
java.net
Android.net
1.2應用安裝路徑
從API版本8開始,你可以允許你的應用安裝在外部存儲上(例如,設(shè)備的SD卡)。這是一個可選項,你可以在Mainfest文件中用 android:installLocation 參數(shù)來聲明它。如果你沒有聲明這個元素,你的應用只會被裝在內(nèi)部存儲上,并且不能被移動到外部存儲中。
為了允許系統(tǒng)在外部存儲上安裝你的應用,修改mainfest文件,在其中包含android:installLocation參數(shù),可以使用的值有preferExternal 或者 auto
舉例:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="preferExternal"
... >
如果你聲明的參數(shù)的值是preferExternal ,則你要求你的應用裝載在外存儲上 。但是系統(tǒng)并不保證你的應用會被安裝在外部存儲上。如果外部存儲已經(jīng)滿了,系統(tǒng)會安裝在內(nèi)部存儲上。用戶可以在外部存儲和內(nèi)部存儲之間轉(zhuǎn)移你的應用。
如果你聲明的參數(shù)是 auto,則你暗示你的應用可能會被安裝在外部存儲上,但你對安裝的位置并沒有指明。系統(tǒng)會基于一些因素來決定在什么位置安裝你的應用。用戶同樣也可以在外部存儲和內(nèi)部存儲之間轉(zhuǎn)移你的應用。
當你的應用被安裝在外部存儲上時:
(1) 只要外部存儲處于連接狀態(tài),對于應用的表現(xiàn)就沒有任何影響。
(2) .apk文件會被保存在外部存儲上,但是用戶所有的私有數(shù)據(jù),數(shù)據(jù)庫,優(yōu)化后的.dex文件以及提取的本地代碼都會被保存在內(nèi)部存儲上。
(3) 唯一的用來存儲你應用的容器已經(jīng)被一個隨機產(chǎn)生的密鑰加密過。這個密鑰只會被最初安裝它的設(shè)備所加密。因此,一個被安裝在外部存儲上的應用只會為一臺設(shè)備工作。
(4) 用戶可以通過系統(tǒng)設(shè)置將你的應用移動到內(nèi)部存儲上。
警告:當用戶通過USB大容量存儲設(shè)備與你的電腦共享文件或者通過系統(tǒng)設(shè)置移除了你的SD卡,使得外部存儲對于你的設(shè)備來說變得不可獲得,那么被安裝在外部存儲上的應用都會立即被結(jié)束掉。
1.2.1向后兼容性
讓應用安裝在外部存儲上的這一特征,只有設(shè)備運行的api版本在8(Android的版本2.2)或以上才有用。那些基于api版本8構(gòu)建的應用總是會被安裝在內(nèi)部存儲上,并且不能被轉(zhuǎn)移到外部存儲上(即使設(shè)備的api版本為8),然而當你的應用是為api版本8或以下的版本來設(shè)計的時候,你可以選擇是否支持api版本為8或者以上的版本,并通過使用api版本8或者更低來編譯。
為了讓應用安裝在外部存儲上,并對低于api 8 的版本保持兼容,你需要:
(1) 在<mainifest>元素中,包含 android:installLocation參數(shù),使用”auto”或”preferExternal” 兩個值之一。
(2) 使你的 android:minSdkVersion 參數(shù)保持在低于api 8的版本,并確保你應用中的代碼只使用了那些兼容該版本的應用程序接口。
(3) 為了編譯你的應用,修改構(gòu)建目標的api 版本為 8。這是必須的,因為在更早的Android庫中不明白 android:installLocation 這個參數(shù),當它出現(xiàn)時,也不會去編譯它。
(4) 當你的應用被安裝在低于api本版8的設(shè)備上時,android:installLocation參數(shù)會被忽略,并且應用會被安裝在內(nèi)部存儲上
注意:盡管在更早的版本中,xml中的標記會被忽略,但當你的minSdkVersion小于8 時你必須小心不要去使用api版本8中的應用程序接口,除非你在你的代碼中做過了必要的向后兼容的工作。
1.2.2不應該被安裝在外部存儲上的應用
當用戶連接了USB大容量存儲設(shè)備在電腦上共享文件時(或者卸載、移除了外部存儲),任何正在運行的、安裝在外部存儲上的應用都會被結(jié)束掉。系統(tǒng)實際上會不知道這些應用在哪,直到大容量存儲設(shè)備被重新安裝到設(shè)備上。除去結(jié)束掉應用,使應用不能被用戶獲得的后果之外,這種做法也會使得某些種類的應用發(fā)生嚴重的錯誤。為了讓你的應用一直表現(xiàn)的如你預期那樣,如果它使用了以下的特征,你不應該允許你的應用被安裝在外部存儲上。當外部存儲未被裝載時,可能會出現(xiàn)以下結(jié)果:
服務:
你正在運行的服務會被結(jié)束掉,即使外部存儲被重新裝載時也不會重新運行。但是,你依然可以注冊ACTION_EXTERNAL_APPLICATIONS_AVAILABLE的事件廣播。當被安裝在外部存儲上的應用對系統(tǒng)來說變得可獲得時,這個廣播會通知你的應用,此時你可以重啟你的服務。
鬧鐘服務:
你的通過AlarmManager注冊的鬧鈴會被取消,你必須手動再次注冊鬧鐘服務,當外部存儲被重新裝載時。
輸入法引擎:
你的輸入法引擎會被默認的取代。當你的外部存儲被重新裝載時,用戶可以打開系統(tǒng)設(shè)置重新使用你的輸入法引擎。
動態(tài)壁紙:
你正在運行的動態(tài)壁紙會被默認的動態(tài)壁紙所代替。當外部存儲被重新裝載時,用戶可以重新選擇你的動態(tài)壁紙。
應用組件:
你的應用組件會從主界面移除。當外部存儲重新裝載時,你的應用組件對于用戶來說是不可選取的,直到系統(tǒng)重置主界面應用(通常直到系統(tǒng)重啟,都不會這樣)
賬戶管理:
你通過AccountManager創(chuàng)建的用戶會消失,直到外部存儲重新裝載。
異步適配:
你的AbstractThreadedSyncAdapter和其它異步方法都會停止工作,直到外部存儲重新裝載。
設(shè)備管理者:
你的DeviceAdminReceiver及其所有管理功能都會不能使用,這可能會給設(shè)備功能造成不可預見的后果,即便外部存儲重新裝載后,這個問題也會持續(xù)。
監(jiān)聽啟動完成的廣播接收者:
在外部存儲被裝載之前,系統(tǒng)會發(fā)送ACTION_BOOT_COMPLETED的廣播。如果你的應用安裝在外部存儲上,你永遠也接受不到這個廣播。
如果你的應用使用了任何以上列出的特征,你應該允許你的應用被安裝在外部存儲上。默認情況下,系統(tǒng)也不會允許你的應用安裝在外部存儲上,所以你也不需要擔心那些已經(jīng)存在的應用。然而,如果你的確信你的應用永遠也不應該被安裝在外部存儲上,那么你可以通過聲明android:installLoaction的值為”internalOnly”。盡管這并不會改變默認行為,但這個聲明明確的指出了你的應用應該被安裝在內(nèi)部存儲上,并作為一個提醒告知其它開發(fā)者。
1.2.3應該被安裝在外部存儲上的應用
簡而言之,任何沒有使用到上述特征的應用安裝在外部存儲上時,都是安全的。大型游戲一般都是應該被安裝在外部存儲上的應用類型,因為當游戲閑置時,不需要額外的服務。當外部存儲變得不可獲得時,游戲進程會被結(jié)束掉。當外部存儲重新可獲得,用戶重啟了游戲時(假設(shè)游戲在整個活動周期中合理的保存了它的狀態(tài)) 也不會有任何可見的影響。
如果你的應用需要一些兆字節(jié)的文件,你應該仔細考慮是否應該將應用安裝在外部存儲上,以便讓用戶更好的保護內(nèi)部存儲上的空間。