Room為SQLite提供了一個抽象層,使得可以流暢使用SQLite的所有功能。
處理大量結構化數(shù)據(jù)的app可以從本地數(shù)據(jù)持久化中獲取巨大利益。最常見的用例是緩存相關的數(shù)據(jù)。在這種情況下,當設備無法訪問網(wǎng)絡的時候,用戶仍然可以在離線時瀏覽內(nèi)容。任何用戶原始數(shù)據(jù)的變化都會在連接網(wǎng)絡后同步。
核心框架提供了原生SQL的支持。盡管這些API很強大,但是比較底層并且需要花費大量的時間和努力去使用:
- 沒有原生SQL查詢語句的編譯時驗證。當你的數(shù)據(jù)結構變化時,你需要手動更新受影響的SQL。這個過程會花費大量的時間并且很容易錯誤頻出。
Room考慮到了這些,提供了SQLite的抽象層。
Room有三個主要的組件:
- 數(shù)據(jù)庫(Database):你可以使用該組件創(chuàng)建數(shù)據(jù)庫的持有者。該注解定義了實體列表,該類的內(nèi)容定義了數(shù)據(jù)庫中的DAO列表。這也是訪問底層連接的主要入口點。注解類應該是抽象的并且擴展自
RoomDatabase。在運行時,你可以通過調(diào)用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()獲取實例。 - 實體(Entity):這個組件代表了持有數(shù)據(jù)庫表記錄的類。對每種實體來說,創(chuàng)建了一個數(shù)據(jù)庫表來持有所有項。你必須通過
Database中的entities數(shù)組來引用實體類。實體的每個成員變量都被持久化在數(shù)據(jù)庫中,除非你注解其為@Ignore。
實體類可以擁有無參數(shù)構造函數(shù)(如果DAO類可以訪問每個持久化成員變量)或者擁有和實體類成員變量匹配參數(shù)的構造函數(shù)。Room也可以使用全部或者部分構造函數(shù),例如只接收部分成員變量的構造函數(shù)。
- 數(shù)據(jù)訪問對象(DAO):這個組件代表了作為DAO的類或者接口。DAO是Room的主要組件,負責定義訪問數(shù)據(jù)庫的方法。被注解
@Database的類必須包含一個無參數(shù)的抽象方法并返回被@Dao注解的類型。當編譯時生成代碼時,Room會創(chuàng)建該類的實現(xiàn)。
通過使用DAO類訪問數(shù)據(jù)庫而不是查詢構建器或直接查詢,你可以將數(shù)據(jù)庫架構的不同組件分離。此外,DAO允許你在測試時很容易地模擬數(shù)據(jù)訪問。
這些組件和app的其他部分關系圖如下:

(img)
下面的代碼片段包含了簡單的數(shù)據(jù)庫配置,含有1個實體和一個DAO:
//User.java
@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
// 省略Getters Setters(實際代碼中不可省略)
}
//UserDao.java
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
//AppDatabase.java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
創(chuàng)建上面的文件以后,你可以使用以下代碼獲取已創(chuàng)建數(shù)據(jù)庫實例:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
當實例化
AppDatabase對象時,你可以遵循單例設計模式,因為每個RoomDatabase實例代價是非常昂貴的,并且你幾乎不需要訪問多個實例。
實體
當一個類被@Entity注解,并被@Database注解的entities屬性引用時,Room為這個實體在數(shù)據(jù)庫中創(chuàng)建一個表。
默認情況,Room為實體類的每個成員變量創(chuàng)建一個列。如果一個實體類的某個成員變量不想被持久化,你可以使用Ignore注解標記,如:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;//不進行持久化
}
為了持久化成員變量,Room必須可以訪問它。你可以使成員變量是公共的,或者提供getter和setter方法。如果你使用getter/setter方法,請記住它們在Room中遵循Java Beans的概念。
主鍵
每個實體必須至少定義一個成員變量作為主鍵。甚至僅僅有一個成員變量,也要標記其為@PrimaryKey。同時,如果你想要Room指定ID自增,你可以設置@Primary的autoGenerate屬性。如果實體的主鍵是綜合的,你可以使用@Entity的primaryKeys屬性,如:
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
默認情況下,Room使用類名作為數(shù)據(jù)庫表的表名。如果你想要數(shù)據(jù)庫表有一個其他的名字,設置@Entity注解的tableName屬性即可:
@Entity(tableName = "users")
class User {
...
}
注意:SQLite中的表名是大小寫敏感的。
和tablename屬性相似,Room使用成員名作為列名,如果你想要改變類名,在成員上添加@ColumnInfo注解即可:
@Entity(tableName = "users")
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
索引與唯一
取決于你如何訪問數(shù)據(jù),你可能想要索引確切的字段以加速數(shù)據(jù)的查詢。為了向實體添加索引,在@Entity中添加indices屬性,列出你想要包括的字段名或者字段名組:
@Entity(indices = {@Index("name"), @Index("last_name", "address")})
class User {
@PrimaryKey
public int id;
public String firstName;
public String address;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
有些時候具體的成員或成員組必須是獨一無二的。你可以設置@Index的屬性unique為true:
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
關系
因為SQLite是一個關系型數(shù)據(jù)庫,你可以指定對象間的關系。即使大多數(shù)ORM類庫允許實體對象互相引用,Room則顯式地禁止了這一點。
即使你不能直接使用關系映射,Room仍然允許你去定義實體鍵的外鍵約束。
例如,有另一個叫做Book的實體,你可以通過使用@ForeignKey注解定義其和User實體的關系,如:
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
class Book {
@PrimaryKey
public int bookId;
public String title;
@ColumnInfo(name = "user_id")
public int userId;
}
外鍵是非常強大的,因為它們允許你指定引用實體更新時會發(fā)生什么、例如,你可以告訴SQLite去刪除所有的書籍,如果這些書所對應的User被刪除并且指定了@ForeignKey的屬性onDelete=CASCADE。
SQLite將
@Insert(OnConfilct=REPLACE)處理為REMOVE和REPLACE的集合而不僅僅是更新操作。這個替換沖突值的方法可能會對你的外鍵約束起作用。
內(nèi)嵌對象
有些時候你想要一個實體類或POJO類作為數(shù)據(jù)庫邏輯的一部分。這種情況下,你可以使用@Embedded注解來。你可以查詢內(nèi)嵌成員,就像你可能查詢其他字段一樣。
例如,我們的User類包含一個Address類型的成員,代表了street、city、state和postCode。為了分別存儲這些字段,在User類中包含一個Address成員并標記為@Embedded,如:
class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
這個表代表了一個User對象包含了以下字段:id,firstName,street,state,city和post_code。
內(nèi)嵌成員也可以含有其他內(nèi)嵌成員
如果一個實體含有多種相同類型的內(nèi)嵌成員,你可以通過設置prefix屬性保持每個字段的唯一性。Room之后添加提供的值到每個內(nèi)嵌對象的起始位置。
數(shù)據(jù)訪問對象(DAO)
Room的主要組件是Dao類。DAO以清晰的方式抽象除了訪問數(shù)據(jù)庫的行為。
Room不允許在主線程方位數(shù)據(jù)庫,除非你在Builder調(diào)用
allowMainThreadQueries(),因為這可能會導致UI被鎖住。而異步查詢則不受此約束,因為異步調(diào)用在后臺線程運行查詢工作。
便捷方法
有很多可以使用DAO類的便捷查詢方法,例如:
Insert
當你創(chuàng)建一個DAO方法并標記其為@Insert,Room會生成在單一事務中將所有參數(shù)存入數(shù)據(jù)庫的實現(xiàn):
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);
@Insert
public void insertBothUsers(User user1, User user2);
@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}
如果@Insert方法只接收一個參數(shù),它會返回long,表示新插入項的row Id。如果參數(shù)是數(shù)組或集合,它會返回long[]或者List<Long>。
Update
Update是更新一組實體的便捷方法。它查詢匹配主鍵的記錄然后更新。如:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
盡管通常并不需要如此,你可以讓該方法返回一個int值,表示更新至數(shù)據(jù)庫的行號。
Delete
Delete是刪除一組實體的便捷方法。它使用主鍵去尋找記錄并刪除:
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
同上,你可以讓該方法返回一個int值表示被刪除的行號。
使用@Query
@Query是用于DAO類的主要注解。它允許你在數(shù)據(jù)庫上執(zhí)行讀寫操作。每個Query方法都會在編譯時驗證,因此如果查詢語句有問題,那么編譯時就會報錯,而不是在運行時發(fā)生。
Room同樣驗證查詢的返回值,如果返回對象的成員名和字段名不一致,Room會以以下兩種方式警告:
- 如果僅僅部分成員名相符,則發(fā)出警告
- 如果沒有成員名相符,則發(fā)出錯誤
簡單查詢
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
這是一個加載所有用戶的簡單查詢。在編譯時,Room知道這是查詢用戶表的所有字段。如果查詢語句含有語法錯誤,或者用戶表在數(shù)據(jù)庫中并不存在,Room會顯示相應的錯誤。
查詢中傳遞參數(shù)
大多數(shù)時候,你需要在查詢中傳遞參數(shù)來執(zhí)行過濾操作,例如僅僅顯示具體年齡的用戶。為了完成這個任務,在你的Room注解中使用方法參數(shù),如:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
當編譯時處理這個查詢時,,Room將:minAge和minAge匹配在一起。Room使用參數(shù)名進行匹配,如果匹配不成功,會在編譯時報錯。
你也可以傳遞多個參數(shù)或引用多次,如:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
@Query("SELECT * FROM user WHERE first_name LIKE :search "
+ "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}
返回所有字段的子集
大多數(shù)時候,你可能需要一個實體的一部分成員變量,例如你的UI可能只顯示用戶的名和姓,而不是用戶的所有細節(jié)。通過僅僅獲取出現(xiàn)在你UI中的字段,你可以存儲很多資源,并且你的查詢完成地更快。
Room允許你從查詢中返回任何對象,只要結果字段集可以被映射到返回的對象上。例如,你可以創(chuàng)建下面的POJO類來獲取用戶的姓和名:
public class NameTuple {
@ColumnInfo(name="first_name")
public String firstName;
@ColumnInfo(name="last_name")
public String lastName;
}
現(xiàn)在你可以在你的查詢方法中這樣使用POJO類:
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
Room理解這次返回first_name和last_name字段的查詢,并可以映射到NameTuple類。這樣,Room就能生成正確的代碼。如果查詢返回太多的字段,或者某個字段并不存在于NameTuple中,Room會顯示一個警告。
傳遞參數(shù)集合
你的某些查詢可能會傳遞大量的參數(shù),而且直到運行時才知道具體的參數(shù)。例如,你可能會獲取關于用戶所屬區(qū)域的信息。當參數(shù)為集合時,Room能夠理解并自動根據(jù)當前提供的參數(shù)進行擴展:
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
可觀察查詢
當運行查詢時,你通常想要在數(shù)據(jù)變化的時候你的app界面自動更新。為了做到這一點,在查詢方法中使用LiveData類型的返回值。Room會生成所有必要的代碼,當數(shù)據(jù)更新時,會自動更新LiveData。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}
RxJava
Room也可以從你定義的查詢中直接返回RxJava2的Publisher和Flowable對象。為了使用這個功能,添加android.arch.persistence.room:rxjava2到你的Gradle構建依賴。你可以隨后返回RxJava2定義的類型,如:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
查詢多張表
你的一些查詢可能需要訪問多張表來計算結果。Room允許你寫任何的查詢,因此你可以使用連接表。另外,如果結果是可觀察數(shù)據(jù)類型,例如Flowable或者LiveData,Room會驗證所有SQL查詢語句。
下面的代碼片段展示了如何連接兩張表,一張表是包含用戶借書的信息,另一張包含當前借出的信息:
@Dao
public interface MyDao {
@Query("SELECT * FROM book "
+ "INNER JOIN loan ON loan.book_id = book.id "
+ "INNER JOIN user ON user.id = loan.user_id "
+ "WHERE user.name LIKE :userName")
public List<Book> findBooksBorrowedByNameSync(String userName);
}
你也可以從這些查詢中返回POJO類。例如你可以這樣寫一個用戶和其寵物姓名的查詢語句:
@Dao
public interface MyDao {
@Query("SELECT user.name AS userName, pet.name AS petName "
+ "FROM user, pet "
+ "WHERE user.id = pet.user_id")
public LiveData<List<UserPet>> loadUserAndPetNames();
// 你也可以在單獨的文件中定義該類,只要你添加了public修飾符
static class UserPet {
public String userName;
public String petName;
}
}
使用類型轉換
Room提供內(nèi)置工具用于基本類型和其封裝類型的裝換。但是有些時候你可能使用了使用了自定義數(shù)據(jù)類型,而想在數(shù)據(jù)庫表中始終單個字段。為了添加這類自定義類型支持,你需要提供一個TypeConverter,將自定義類轉換到Room已知可以持久化的類型。
例如,如果我們想要持久化Date實例,我們可以這樣寫:
public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}
上面的例子定義了兩個函數(shù),一個將Date類型轉換為Long類型,另一個進行相反的轉換。
接下來,在AppDataBase添加@TypeConverters注解,使得Room可以使用你定義的轉換器:
@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
使用了這些轉換器以后,你可以在其他查詢中使用你的自定義類型,就像基本類型一樣:
//User.java
@Entity
public class User {
...
private Date birthday;
}
//UserDao.java
@Dao
public interface UserDao {
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
你可以限制@TypeConverters的作用范圍,包括單獨實體,DAO以及DAO方法。
數(shù)據(jù)庫遷移
當你在app添加以及修改功能時,你需要修改你的實體類以響應這些變化。當一個用戶更新到最新版本的app時,你不想讓他們丟掉所有已經(jīng)存在的數(shù)據(jù),特別是不能再從遠程服務器獲取的數(shù)據(jù)。
Room允許你編寫Migration類來保護用戶數(shù)據(jù)。每個Migration類指定一個startVersion和endVersion。在運行時,Room運行每個Migration類的migrate()方法,使用正確的順序遷移至數(shù)據(jù)庫的更新版本。
如果你沒有提供必要的遷移,Room會重新構建數(shù)據(jù)庫,這意味著你將丟失所有數(shù)據(jù)庫中的數(shù)據(jù)。
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER");
}
};
為了保持你的遷移功能的正確性,使用完整的查詢語句而不是詢而不是引用表示查詢的常量。
在遷移過程完成后,Room驗證當前的表以確保遷移的正確性。如果Room找到問題,會拋出未匹配的異常信息。
測試遷移
遷移是很重要的事情,錯誤的編寫會導致你app的崩潰循環(huán)。為了保證app的穩(wěn)定性,你應該測試你的遷移工作。Room提供了一個測試的Maven構件來幫助測試。但是,為了該構件可以工作,你需要導出你的數(shù)據(jù)庫表。
導出數(shù)據(jù)庫表
//build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
Room會將你數(shù)據(jù)庫的表信息導出為一個json文件。你應該在版本控制系統(tǒng)中保存該文件,該文件代表了你的數(shù)據(jù)庫表歷史記錄,這樣允許Room創(chuàng)建舊版本的數(shù)據(jù)庫用于測試。
為了測試遷移,添加android.arch.persistence.room:testing到你的測試依賴,以及添加模式表的位置至asset文件夾,如:
//build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
測試包提供了一個MigrationTestHelper類,可以讀取這些模式表文件。
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
// db 版本為1\. 使用SQL添加一些數(shù)據(jù)
// 你不能使用DAO,因為它表示的是最新的數(shù)據(jù)庫
db.execSQL(...);
// 準備下個版本
db.close();
// 重新打開數(shù)據(jù)庫版本2
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
}
}
測試你的數(shù)據(jù)庫
當運行測試你的app時,如果沒有測試數(shù)據(jù)庫本身,你不需要創(chuàng)建全部的數(shù)據(jù)庫。Room允許在你的測試中模擬數(shù)據(jù)訪問層。這個過程是可能的,因為你的DAO并沒有泄漏任何數(shù)據(jù)庫的細節(jié)。當測試剩下的app部分時,你應該創(chuàng)建模擬你的DAP類。
這里有兩種測試數(shù)據(jù)庫的方式:
- 在你的開發(fā)主機上
- 在Android設備上
在你的主機上測試
Room使用SQLite支持庫,提供了匹配安卓框架類的接口。這種支持允許你傳遞支持類庫的自定義實現(xiàn)以測試你的數(shù)據(jù)庫。
即使這種方案允許你測試非??旖?,但是并不值得推薦,這是因為你設備上以及你用戶設備上運行的SQLite版本可能和你主機上運行的版本并不匹配。
在Android設備上測試
這種推薦的測試數(shù)據(jù)庫方法是編寫運行在安卓設備上的JUnit測試。因為這些測試并不需要創(chuàng)建Activity,它們應該會比在UI上測試要快。
當設置你的測試時,你應該創(chuàng)建一個數(shù)據(jù)庫的內(nèi)存版本來使得測試更密閉,如:
@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
private UserDao mUserDao;
private TestDatabase mDb;
@Before
public void createDb() {
Context context = InstrumentationRegistry.getTargetContext();
mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
mUserDao = mDb.getUserDao();
}
@After
public void closeDb() throws IOException {
mDb.close();
}
@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
mUserDao.insert(user);
List<User> byName = mUserDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
}
附加::沒有實體鍵的對象引用
從數(shù)據(jù)庫到對象間關系的映射是一個很常見的實踐,并且在服務端運行良好,在它們被訪問的時候進行高性能的惰性加載。
但是在客戶端,惰性加載并不可行,這是因為很有可能發(fā)生在主線程,在主線程查詢磁盤信息會導致很嚴重的性能問題。主線程有大概16ms來計算并繪制一個Activity的界面更新,因此甚至一個查詢僅僅耗費5ms,你的app仍然會耗光繪制畫面的時間,導致顯著的Jank問題。更糟的是,如果有個并發(fā)運行的數(shù)據(jù)庫事務,或者如果設備正忙于處理其他磁盤相關的繁重工作,查詢會花費更多的時間完成。如果你不使用惰性加載的方式,app會獲取多余其所需要的數(shù)據(jù),從而導致內(nèi)存消耗的問題。
ORM通常將該問題交給開發(fā)者決定,使得他們可以根據(jù)自己的用例選擇最佳的方式。不幸地是,開發(fā)者通常終止模型和UI之間的共享。當UI變更超時時,問題隨之發(fā)生并且很難預感和解決。
舉個例子,UI界面讀取一組Book列表,每本書擁有一個Author對象。你可能開始會設計你的查詢?nèi)ナ褂枚栊约虞d,從而Book實例使用getAuthor()方法查詢數(shù)據(jù)庫。過了一些時間,你意識到你需要在app的UI界面顯示作者名。你可以添加以下方法:
authorNameTextView.setText(user.getAuthor().getName());
但是這種看似沒有問題的代碼會導致Author表在主線程被查詢。
如果你急于查詢作者信息,這會變得很難去改變數(shù)據(jù)是如何加載的,如果你不再需要這個數(shù)據(jù)的話,例如當你app的UI不再需要顯示關于特定作者信息的時候。于是你的app必須繼續(xù)加載不再顯示的信息。這種方式更為糟糕,如果Author類引用了其他表,例如getBooks()方法。
由于這些原因,Room禁止實體間的對象引用。作為替換,你必須顯式地請求你所需要的數(shù)據(jù)。
簡單通俗地解釋一下Jank:第2幀畫面同步信號已經(jīng)到來,由于第2幀數(shù)據(jù)還沒有準備就緒,顯示的還是第1幀。這種情況被Android開發(fā)組命名為“Jank”