首页 > 数据库 >Android 原生 SQLite 数据库的一次封装实践

Android 原生 SQLite 数据库的一次封装实践

时间:2023-04-04 21:02:03浏览次数:54  
标签:SQLite 封装 entities 数据库 cursor 类型 泛型 new Android

作者: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 数据库的一次封装实践_SDK开发

通过上述相似性的对比并综合现有ORM框架来考虑切入口,首先想到的是使用注解:

主流Room使用的是编译时注解(更有利于性能),但在具体编码实现Processor过程中发现增删改查操作的出参和入参处理有点过于繁琐(参考Room实现),不太适用于本身就是一个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 数据库的一次封装实践_SDK开发_02

  • 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 数据库的一次封装实践_SDK开发_03

由于动态代理会返回接口的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 数据库的一次封装实践_泛型擦除_04

说明:

  1. 表中第4条测试(查出全部10w条数据)差异较大(相差79ms),其原因是原生接口的Entity对象是直接new出来的,而sponsor内部只能通过Entity的newInstance()接口去反射创建,导致了性能差距,但平均算下来,每newInstance()创建1000个对象才多了1ms,影响还是很小的。(尝试使用Clone的方式优化,但效果仍不明显)
  2. sponsor方式性能均略低于原生方式,原因是其需要动态拼凑SQL语句的性能消耗,但消耗极少。

七、在项目(SDK)中的应用实践

该项目内部使用的数据库是一个多库多表的架构,数据库操作(增删改查、建表、升级/降级等)均是调用SQLiteOpenHelper原生接口写的代码逻辑,导致相关操作需要写很多的模板代码才能拿到最终结果,逻辑比较冗长;因此,在重构版本我们使用sponsor替换掉了这些原生调用,以此简化这些繁琐易出错操作。目前运行良好,暂没有发现明显严重问题。

八、扩展知识——泛型的类型擦除

关于类型擦除,感觉很多人都有一些误区,特别是客户端开发平时涉及较少,感觉都不太理解:

根据我们的常识都知道Java的泛型在运行时是类型擦除的,编译后就不会有具体的类型信息了(都是Object或者某个上界类型)。

那么问题来了,既然类型都擦除了,那retrofit又是怎样能在运行时拿到方法泛型参数类型(包括参数类型和返回类型)的呢?比如内部可以根据函数的返回类型将json转为对应bean对象。

起先也很难理解,于是通过查找资料、技术群交流、写demo验证后才基本弄明白,总结为一句话:类型擦除其实只是把泛型的形参擦除了(方便和1.5以下版本兼容),原始的字节码中还是会保留类结构(类、方法、字段)的泛型类型信息,具体保存在Signature区域,可以使用Type的子类接口在运行时获取到泛型的类型信息。

1、retrofit请求接口一般定义如下:

 

Android 原生 SQLite 数据库的一次封装实践_注解_05

可以看到这个函数的返回类型和参数类型都带有泛型参数。

2、反编译这个apk,并用JD-GUI工具打开可以找到对应方法如下:

Android 原生 SQLite 数据库的一次封装实践_SQLite数据库_06

很多人看到这里会觉得泛型的类型信息确实已经被完全清除了。不过这个工具只是展示了简单的类结构信息(仅包含类、函数、字段)而已,我们可以更进一步看一下该类对应的字节码来确认下,直接使用AS打开apk,展开classes.dex找到对应类,右键->"Show ByteCode"查看:

Android 原生 SQLite 数据库的一次封装实践_注解_07

可以看到在Signature区域保存了这个方法的所有参数信息,其中就有泛型的类型信息。

任何类、接口、构造器方法或字段的声明如果包含了泛型类型,则会生成Signature属性,为它记录泛型签名信息,不过函数内的局部变量泛型信息将不会被记录下来。

3、下面看一下Type接口的继承关系,以及提供的接口功能:

Android 原生 SQLite 数据库的一次封装实践_注解_08

Class:最常见的类型,一个Class类的对象表示虚拟机中的一个类或接口。

ParameterizedType:表示是参数化类型,如:List<String>、Map<Integer,String>这种带有泛型的类型,常用方法有:

  1. Type getRawType()——返回参数化类型中的原始类型,例如List<String>的原始类型为List。
  2. Type[] getActualTypeArguments()——获取参数化类型的类型变量或是实际类型列表,如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()——返回类型变量的上边界。
  2. Type[] getLowerBounds()——返回类型变量的下边界。

九、参考资料

  1. https://github.com/square/retrofit
  2. https://cs.android.com/androidx/platform/frameworks/support/+/android-room-release:room/compiler/src/main/kotlin/androidx/room/processor/
  3. https://techblog.bozho.net/on-java-generics-and-erasure/

更多内容敬请关注 vivo 互联网技术 微信公众号

Android 原生 SQLite 数据库的一次封装实践_泛型擦除_09

标签:SQLite,封装,entities,数据库,cursor,类型,泛型,new,Android
From: https://blog.51cto.com/u_14291117/6169530

相关文章

  • Android 加载图片占用内存分析
    作者:XuJie不同Android版本,对一张图片的内存处理方式是不一样的,使用不正确会导致OOM的发生,这篇文章带你梳理内存占用情况,选择适合你的图片加载模式,解决OOM问题。一、背景你知道吗一张5.48MB,宽高像素为4896*6528的24位的静态图片,放在Android工程目录下面的res/drawable-[density]/......
  • 使用logging封装日志
    自己封装的logging,封装日志的几个组件Logger记录器暴露了应用程序代码直接使用的接口。Handler处理器将日志记录(由记录器创建)发送到适当的目标。Filter过滤器提供了更细粒度的功能,用于确定要输出的日志记录。Formatter格式器指定最终输出中日志记录的样式。日志等级......
  • android四大组件
    Android开发的四大组件,本文主要分为一、Activity详解二、Service详解三、BroadcastReceiver详解四、ContentProvider详解外加一个重要组件intent的详解。一、Activity详解Activty的生命周期的也就是它所在进程的生命周期。 一个Activity的启动顺序:onCreate()——>onStart()——......
  • 【GiraKoo】重置Android Studio环境的几个方案
    【GiraKoo】重置AndroidStudio环境的几个方案AndroidStudio经常在编译时,发现一些奇奇怪怪的编译/运行问题。明明是很小的改动,但是出现了一些不相关的错误。搞不清楚究竟是什么原因导致的。这时候,就需要考虑重置AndroidStudio环境的几个方案。InvalidateCaches在"File"菜......
  • Android 手把手教您自定义ViewGroup(一)
    本文出自:【张鸿洋的博客】最近由于工作的变动,导致的博客的更新计划有点被打乱,希望可以尽快脉动回来~今天给大家带来一篇自定义ViewGroup的教程,说白了,就是教大家如何自定义ViewGroup,如果你对自定义ViewGroup还不是很了解,或者正想学习如何自定义,那么你可以好好看看这篇博客。1、......
  • 一手遮天 Android - view(媒体类): 截图
    项目地址https://github.com/webabcd/AndroidDemo作者webabcd一手遮天Android-view(媒体类):截图示例如下:/view/media/ScreenshotDemo1.kt/***截图*/packagecom.webabcd.androiddemo.view.mediaimportandroid.graphics.Bitmapimportandroid.graphics.Can......
  • 一手遮天 Android - view(媒体类): MediaPlayer(在 SurfaceView 上播放)
    项目地址https://github.com/webabcd/AndroidDemo作者webabcd一手遮天Android-view(媒体类):MediaPlayer(在SurfaceView上播放)示例如下:/view/media/MediaPlayerDemo1.kt/***MediaPlayer(在SurfaceView上播放)**注:无法对SurfaceView截图,如果需要对视频截图......
  • 高效的Android布局
    EfficientAndroidLayouts每一个视图最大的效率!查看/下载幻灯片介绍我已经做了大约七年的Android开发,首先在旅游App公司,然后Expedia,现在目前在Trello。这个演讲是关于高效的Android布局,当我写它,我发现是我真正感兴趣的不是那么多的性能方面的效率,但有作为一个开发者的抛砖......
  • android 权限
    1.AIDandroid系统沿用了Linux的UID/GID权限模型,但并没有使用传统的passws和group文件来存储用户和用户组的认证凭据,作为代替,Android定义了从名称到AndroidID(AID)的映射表。system/core/include/private/android_filesystem_config.h#defineAID_ROOT0/*traditional......
  • Android如何为某个APK开启代码混淆机制
    1.修改该模块的Android.mk文件,添加如下内容:LOCAL_PROGUARD_ENABLED:=customLOCAL_PROGUARD_FLAG_FILES:=proguard.flags2.编写一个文本文件,将其命名为proguard.flags,并将该文件放到与该模块的Android.mk相同的目录下;该文件开头部分内容需要填写:......