zoukankan      html  css  js  c++  java
  • Android 原生 SQLite 数据库的一次封装实践

    本文首发于 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. 稳定可靠,无性能问题

    三、方案调研

    观察我们日常业务代码可以发现:一次数据库查询与一次网络请求在流程上是极为相似的,都是经过构造请求、发起请求、中间步骤、获取结果、处理结果等几个步骤。因此感觉可以将数据库操作以网络请求的方式进行抽象和封装,其详细对比如下表所示:

    通过上述相似性的对比并综合现有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构成:

    • 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、接口适配

    由于动态代理会返回接口的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轮测试,然后取平均值(四舍五入)

    说明:

    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请求接口一般定义如下:

     

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

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

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

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

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

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

    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/

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

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

    注:转载文章请先与微信号:Labs2020 联系。

  • 相关阅读:
    Atitit fms Strait (海峡) lst 数据列表目录1. 4大洋 12. 著名的海大约40个,总共约55个海 13. 海区列表 23.1. 、波利尼西亚(Polynesia,
    Atitit trave islands list 旅游资源列表岛屿目录1. 东南亚著名的旅游岛屿 21.1. Cjkv 日韩 冲绳 琉球 济州岛 北海道 21.2. 中国 涠洲岛 南澳
    Atitit Major island groups and archipelagos 主要的岛群和群岛目录资料目录1. 岛群 波利尼西亚(Polynesia, 美拉尼西亚(Melanesia,
    Atitit glb 3tie city lst 三线城市列表 数据目录1. 全球范围内约90个城市 三线 12. 世界性三线城市全球共
    Atitit glb 1tie 2tie city lst 一二线城市列表数据约50个一线城市Alpha ++ 阿尔法++,,London 伦敦,,New York 纽约,,Alpha +
    Attit 现代编程语言重要特性目录第一章 类型系统 基本三大类型 2第一节 字符串 数字 bool 2第二节 推断局部变量 2第三节 动态类型 2第二章 可读性与开发效率 简单性 2
    Atitit 未来数据库新特性展望目录1. 统一的翻页 21.1. 2 Easy Top-N
    使用Chrome DevTools(console ande elements panel)进行xpath/css/js定位
    chrome -console妙用之定位xpath/js/css
    表达式树之构建Lambda表达式
  • 原文地址:https://www.cnblogs.com/vivotech/p/13446397.html
Copyright © 2011-2022 走看看