Android 原生 SQLite 資料庫的一次封裝實踐

vivo網際網路技術發表於2020-08-10

本文首發於 vivo網際網路技術 微信公眾號 
連結: https://mp.weixin.qq.com/s/CL4MsQEsrWS8n7lhXCOQ_g
作者:Li Bingyan

本文主要講述原生SQLite資料庫的一次ORM封裝實踐,給使用原生資料庫操作的業務場景(如:本身是一個SDK)帶來一些啟示和參考意義,以及跟隨框架的實現思路對資料庫操作、APT、泛型等概念更深一層的理解。

實現思路:透過動態代理獲取請求介面引數進行SQL拼湊,並以介面返回值(泛型)型別的RawType和ActualType來適配呼叫方式和執行結果,以此將實際SQL操作封裝在其內部來簡化資料庫操作的目的。

一、背景 

毫無疑問,關於Android資料庫現在已經有很多流行好用的ORM框架了,比如:Room、GreenDao、DBFlow等都提供了簡潔、易用的API,尤其是谷歌開源的Room是目前最主流的框架。

既然已經有了這麼多資料庫框架了,為什麼還要動手封裝所謂自己的資料庫框架呢?對於普通 APP 的開發確實完全不需要,這些框架中總有一款可以完全滿足你日常需求;但如果你是一個SDK開發者,而且業務是一個比較依賴資料庫操作的場景,如果限制不能依賴第三方SDK(主要考量維護性、問題排查、穩定性、體積大小),那就不得不自己去寫原生SQLite操作了,這將是一個既繁瑣又容易出錯的過程(資料庫升級/降級/開啟/關閉、多執行緒情況、拼湊SQL語句、ContentValues插資料、遊標遍歷/關閉、Entity轉換等)。

為了在SDK的開發場景中避免上述繁瑣且容易出錯的問題,於是就有了接下來的一系列思考和改造。

二、預期目的

  1. 能簡化原生的增刪改查冗長操作,不要再去寫容易出錯的中間邏輯步驟
  2. 自動生成資料庫的建表、升級/降級邏輯
  3. 易用的呼叫介面(支援同步/非同步、執行緒切換)
  4. 穩定可靠,無效能問題

三、方案調研

觀察我們日常業務程式碼可以發現:一次資料庫查詢與一次網路請求在流程上是極為相似的,都是經過構造請求、發起請求、中間步驟、獲取結果、處理結果等幾個步驟。因此感覺可以將資料庫操作以網路請求的方式進行抽象和封裝,其詳細對比如下表所示:

Android 原生 SQLite 資料庫的一次封裝實踐

透過上述相似性的對比並綜合現有ORM框架來考慮切入口,首先想到的是使用註解:

主流Room使用的是 編譯時註解(更有利於效能),但在具體編碼實現Processor過程中發現增刪改查操作的出參和入參處理有點過於繁瑣(參考 ),不太適用於本身就是一個SDK的場景,最終pass掉了。

執行時註解處理相對更簡單一些(介面和引數較容易適配、處理流程也可以直接寫我們熟悉的安卓原生程式碼),而且前面已經有了大名鼎鼎的網路請求庫Retrofit使用執行時註解實現網路請求的典型範例,因此可以依葫蘆畫瓢嘗試實現一下資料庫增刪改查操作,也是本次改造最終的實現方案。

相信大部分安卓客戶端開發同學都用過Retrofit(網路請求常用庫),其大概原理是:使用動態代理獲取介面對應的Method物件為入口,並透過該Method物件的各種引數(註解修飾)構造出Request物件拋給okhttp做實際請求,返回值則透過Conveter和Adapter適配請求結果(bean物件)和呼叫方式,如:Call<List<Bean>>、Observable<List<Bean>>等。

它以這種方式將網路請求的內部細節封裝起來,極大簡化了網路請求過程。根據其相似性,資料庫操作(增刪改查)也可以使用這個機制來進一步封裝。

對於資料庫的建表、升級、降級等這些容易出錯的步驟,最好是不要讓使用者自己去手動寫這部分邏輯,方案使用編譯時註解來實現(Entitiy類和欄位屬性、版本號透過註解對應起來),在編譯期間自動生成SQLiteOpenHelper的實現類。

綜合以上兩部分基本實現了所有痛點操作不再需要呼叫者去關注(只需關注傳參和返回結果),於是將其獨立成一個資料庫模組,取名Sponsor( [ˈspɑːnsər] ),寓意一種分發器或排程器方案,目前已在團隊內部使用。

四、Sponsor呼叫示例

1、Entity定義:

//Queryable:表示一個可查詢的物件,有方法bool convert(Cursor cursor),將cursor轉換為Entitiy
//Insertable:表示一個可插入的物件,有方法ContentValues convert(),將Entitiy轉換為ContentValues
public class FooEntity implements Queryable, Insertable {
    /**
     * 資料庫自增id
     */
    private int id;
    /**
     * entitiy id
     */
    private String fooId;
    /**
     * entity內容
     */
    private String data;
  
    //其他屬性
  
   //getter()/setter()
}

2、介面定義,宣告增刪改查介面:

/**
 * 插入
 * @return 最後一個row Id
 */
@Insert(tableName = FooEntity.TABLE)
Call<Integer> insertEntities(List<FooEntity> entities);
/**
 * 查詢
 * @return 獲取的entitiy列表
 */
@Query("SELECT * FROM " + FooEntity.TABLE + " WHERE " + FooEntity.CREATE_TIME + " > "
        + Parameter1.NAME + " AND " + FooEntity.CREATE_TIME + " < " + Parameter2.NAME
        + " ORDER BY " + FooEntity.CREATE_TIME + " ASC LIMIT " + Parameter3.NAME)
Call<List<FooEntity>> queryEntitiesByRange(@Parameter1 long start, @Parameter2 long end, @Parameter3 int limit);
/**
 * 刪除
 * @return 刪除記錄的條數
 */
@Delete(tableName = FooEntity.TABLE, whereClause = FooEntity.ID + " >= "
        + Parameter1.NAME + " AND " + FooEntity.ID + " <= " + Parameter2.NAME)
Call<Integer> deleteByIdRange(@Parameter1 int startId, @Parameter2 int endId);

3、建立FooService例項:

Sponsor sponsor = new Sponsor.Builder(this)
        .allowMainThreadQueries() //是否執行在主執行緒操作,預設不允許
        //.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //rxjava
        //.addCallAdapterFactory(Java8CallAdapterFactory.create()) //java8
        //.addCallAdapterFactory(LiveDataCallAdapterFactory.create()) //livedata
        .logger(new SponsorLogger()) //日誌輸出
        .build();
//呼叫create()方法建立FooService例項,實際上是返回了FooService的動態代理物件
FooService mFooService = sponsor.create(FooService.class);

4、插入Entitiy資料:

//構造Entity列表
List<FooEntity> entities = new ArrayList<>();
//add entities
//同步方式
//rowId為最終的自增id(同原生insert操作返回值)
//final int rowId = mFooService.insertEntities(entities).execute();
//非同步方式
mFooService.insertEntities(entities).enqueue(new Callback<Integer>() {
    @Override
    public void onResponse(Call<Integer> call, Integer rowId) {
        //success
    }
    @Override
    public void onFailure(Call<Integer> call, Throwable t) {
        //failed
    }
});

5、查詢引數指定資料庫記錄,並轉換為Entitiy物件列表:

List<FooEntity> entities;
//entities為查詢結果集合
entities = mFooService.queryEntitiesByRange(1, 200, 100).execute();

6、刪除引數指定資料庫記錄,返回總共刪除的記錄條數:

//cout為刪除的條數
int count = mFooService.deleteByIdRange(0, 100).execute();

注:

  1. 以上所有操作都支援根據具體的場景進行同步/非同步呼叫。
  2. 增、刪、改操作的Call<?>返回值引數(泛型引數)還可以直接指定為Throwable,如果內部異常可以透過它返回,成功則為空

五、核心實現點

基本原理仍是借鑑了Retrofit框架的實現,透過動態代理拿到Method物件的各種引數進行SQL拼湊,並透過Converter和Adapter適配執行結果,整體框架有如下幾module構成:

Android 原生 SQLite 資料庫的一次封裝實踐

  • sponsor:主體實現
  • sponsor_annotaiton:註解定義,包括執行時註解和編譯時註解
  • sponsor_compiler:資料庫建表、升級/降級等邏輯的Processor實現
  • sponsor_java8、sponsor_livedata、sponsor_rxjava2:適配幾種主流的呼叫方式

1、動態代理入口

public <T> T create(final Class<T> daoClass, final Class<? extends DatabaseHelper> helperClass) {
    final Object obj = Proxy.newProxyInstance(daoClass.getClassLoader(), new Class<?>[]{daoClass},
            new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Object.class) {
                return method.invoke(this, args);
            }
            DaoMethod<Object, Object> daoMethod =
                    (DaoMethod<Object, Object>) loadDaoMethod(method);
            final DatabaseHelper helper = loadDatabaseHelper(daoClass, helperClass);
            Call<Object> call = new RealCall<>(helper, mDispatcher, mAllowMainThreadQueries,
                    mLogger, daoMethod, args);
            return daoMethod.adapt(call);
        }
    });
    return (T) obj;
}

2、介面適配

Android 原生 SQLite 資料庫的一次封裝實踐

由於動態代理會返回介面的Method物件和引數列表args[],可以透過這兩個引數拿到上述標識的所有元素,具體方法如下所示:

獲取方法的註解: method.getAnnotations()
獲取形參列表:已傳過來
獲取引數註解和型別:method.getParameterAnnotations() method.getGenericParameterTypes()
獲取呼叫方式:method.getGenericReturnType()後,再呼叫Type.getRawType() //Call
獲取結果型別:method.getGenericReturnType()後,再呼叫Type.getActualTypeArguments() //List<FooEntitiy>

3、返回結果適配

private Converter<Response, ?> createQueryConverter(Type responseType, Class<?> rawType) {
    Converter<Response, ?> converter = null;
    if (Queryable.class.isAssignableFrom(rawType)) { //返回單個實體物件
        //其他處理邏輯
        converter = new QueryableConverter((Class<? extends Queryable>) responseType);
    } else if (rawType == List.class) { //返回一個實體列表
        //其他處理邏輯
        converter = new ListQueryableConverter((Class<? extends Queryable>) argumentsTypes[0]);
    } else if (rawType == Integer.class) { //相容 SELECT COUNT(*) FROM table的形式
        converter = new IntegerConverter();
    } else if (rawType == Long.class) {
        converter = new LongConverter();
    }
    return converter;
}

ListQueryableConverter實現,主要是遍歷Cursor構建返回結果列表:

static final class ListQueryableConverter implements Converter<Response, List<? extends Queryable>> {
    @Override
    public List<? extends Queryable> convert(Response value) throws IOException {
        List<Queryable> entities = null;
        Cursor cursor = value.getCursor();
        if (cursor != null && cursor.moveToFirst()) {
            entities = new ArrayList<>(cursor.getCount());
            try {
                do {
                    try {
                        //反射建立entitiy物件
                        Queryable queryable = convertClass.newInstance();
                        final boolean convert = queryable.convert(cursor);
                        if (convert) {
                            entities.add(queryable);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } while (cursor.moveToNext());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        /*
         * 避免返回null
         */
        if (entities == null) {
            entities = Collections.emptyList();
        }
        return entities;
    }
}

4、執行增刪改查操作

final class RealCall<T> implements Call<T> {
    @Override
    public T execute() {
        /**
         * 實際的增刪改查操作
         */
        Response response = perform();
        T value = null;
        try {
            value = mDaoMethod.toResponse(response);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //遊標關閉
            if (response != null) {
                Cursor cursor = response.getCursor();
                if (cursor != null) {
                    try {
                        cursor.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            //資料庫關閉
            if (mDatabaseHelper != null) {
                try {
                    mDatabaseHelper.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return value;
    }
    /**
     * 具體資料庫操作方法
     * @return
     */
    private Response perform() {
        switch (mDaoMethod.getAction()) {
            case Actions.QUERY: {
                //..
              Cursor cursor = query(String sql);
            }
            case Actions.DELETE: {
               //...
              int count =  delete(simple, sql, tableName, whereClause);
            }
            case Actions.INSERT: {
                //...
            }
            case Actions.UPDATE: {
                //...
            }
        }
        return null;
    }
    /**
     * 具體的查詢操作
     */
    private Cursor query(String sql) {
        //...
        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        final Cursor cursor = db.rawQuery(sql, null);
        //...
        return cursor;
    }
    /**
     * 具體的刪除操作
     */
    private int delete(boolean simple, String sql, String tableName, String whereClause) {
        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        int result = 0;
        try {
            db.beginTransaction();
            //...
            result = db.delete(tableName, whereClause, null);
          
            db.setTransactionSuccessful();
        } finally {
            try {
                db.endTransaction();
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        return result;
    }
}

六、效能測試對比

  • 測試手機:vivo X23
  • 安卓版本:Android 9
  • 處理器:驍龍670,2.0GHz,8核
  • 測試方法:每個對比項測試5組資料,每組5輪測試,然後取平均值(四捨五入)

Android 原生 SQLite 資料庫的一次封裝實踐

說明:

  1. 表中第4條測試(查出全部10w條資料)差異較大(相差79ms),其原因是原生介面的Entity物件是直接new出來的,而sponsor內部只能透過Entity的newInstance()介面去反射建立,導致了效能差距,但平均算下來,每newInstance()建立1000個物件才多了1ms,影響還是很小的。(嘗試使用Clone的方式最佳化,但效果仍不明顯)
  2. sponsor方式效能均略低於原生方式,原因是其需要動態拼湊SQL語句的效能消耗,但消耗極少。

七、在專案(SDK)中的應用實踐

該專案內部使用的資料庫是一個多庫多表的架構,資料庫操作(增刪改查、建表、升級/降級等)均是呼叫SQLiteOpenHelper原生介面寫的程式碼邏輯,導致相關操作需要寫很多的模板程式碼才能拿到最終結果,邏輯比較冗長;因此,在重構版本我們使用sponsor替換掉了這些原生呼叫,以此簡化這些繁瑣易出錯操作。目前執行良好,暫沒有發現明顯嚴重問題。

八、擴充套件知識&mdash;&mdash;泛型的型別擦除

關於型別擦除,感覺很多人都有一些誤區,特別是客戶端開發平時涉及較少,感覺都不太理解:

根據我們的常識都知道Java的泛型在執行時是型別擦除的,編譯後就不會有具體的型別資訊了(都是Object或者某個上界型別)。

那麼問題來了,既然型別都擦除了,那retrofit又是怎樣能在執行時拿到方法泛型引數型別(包括引數型別和返回型別)的呢?比如內部可以根據函式的返回型別將json轉為對應bean物件。

起先也很難理解,於是透過查詢資料、技術群交流、寫demo驗證後才基本弄明白,總結為一句話:型別擦除其實只是把泛型的形參擦除了(方便和1.5以下版本相容),原始的位元組碼中還是會保留類結構(類、方法、欄位)的泛型型別資訊,具體儲存在Signature區域,可以使用Type的子類介面在執行時獲取到泛型的型別資訊。

1、retrofit請求介面一般定義如下:

  Android 原生 SQLite 資料庫的一次封裝實踐

可以看到這個函式的返回型別和引數型別都帶有泛型引數。

2、反編譯這個apk,並用JD-GUI工具開啟可以找到對應方法如下:

Android 原生 SQLite 資料庫的一次封裝實踐

很多人看到這裡會覺得 泛型的型別資訊確實已經被完全清除了 。不過這個工具只是展示了簡單的類結構資訊(僅包含類、函式、欄位)而已,我們可以更進一步看一下該類對應的位元組碼來確認下,直接使用AS開啟apk,展開classes.dex找到對應類,右鍵->"Show ByteCode"檢視:

Android 原生 SQLite 資料庫的一次封裝實踐

可以看到在Signature區域儲存了這個方法的所有引數資訊,其中就有泛型的型別資訊。

任何類、介面、構造器方法或欄位的宣告如果包含了泛型型別,則會生成Signature屬性,為它記錄泛型簽名資訊,不過函式內的區域性變數泛型資訊將不會被記錄下來。

3、下面看一下Type介面的繼承關係,以及提供的介面功能:

Android 原生 SQLite 資料庫的一次封裝實踐

Class:最常見的型別,一個Class類的物件表示虛擬機器中的一個類或介面。

ParameterizedType:表示是引數化型別,如:List<String>、Map<Integer,String>這種帶有泛型的型別,常用方法有:

  1. Type getRawType()&mdash;&mdash;返回引數化型別中的原始型別,例如List<String>的原始型別為List。

  2. Type[] getActualTypeArguments()&mdash;&mdash;獲取引數化型別的型別變數或是實際型別列表,如Map<Integer, String>的實際泛型列表是Integer和String。

TypeVariable:表示的是型別變數,如List<T>中的T就是型別變數。

GenericArrayType:表示是陣列型別且組成元素是ParameterizedType或TypeVariable,例如List<T>或T[],常用方法有:

  1. Type getGenericComponentType()一個方法,它返回陣列的組成元素型別。

WildcardType:表示萬用字元型別,例如? extends Number 和 ? super Integer。常用方法有:

  1. Type[] getUpperBounds()&mdash;&mdash;返回型別變數的上邊界。
  2. Type[] getLowerBounds()&mdash;&mdash;返回型別變數的下邊界。

九、參考資料

  1. https://techblog.bozho.net/on-java-generics-and-erasure/

  2. https://blog.csdn.net/u011983531/article/details/80295479

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2709765/,如需轉載,請註明出處,否則將追究法律責任。

相關文章