安卓GreenDao 3.0使用和源码分析

(一)GreenDao简介

GreenDao是一个对象关系映射(ORM)的开源框架,目前最主流的安卓数据库操作框架。

  • 首先说说什么是对象关系映射ORM:Object Relational Mapping,是一种将对象层次结构映射成关系型结构的方法。
Object:即对象,java是一门面向对象的编程语言,对开发者而言,更习惯以对象的角度看待某个问题,或
           者说通过面向对象的方式处理某个问题,对java程序员来说更能理解。
Relational:关系,SQLite是一种关系型数据库,其关系模型就是指二维表格模型,因而一个关系型数据库
           就是由二维表及其之间的联系组成的一个数据组织,它是从数学理论发展而来的(百度百科的定
           义),这种关系型数据库与面向对象的思想是冲突的。开发人员需要时时去面对表单和数据库的
           操作,特别是当表结构复杂时,会在这些数据的处理上花费大量时间。
Mapping:映射(可以通过Map来理解),一种对应关系,用面向对象的方式来处理关系型结构的数据库。简单
           的理解是一张表按照统一规则映射成一个java实体类,对表的操作可以转换对成开发者更熟悉
           的对实体对象的操作。
  • 通过上面的简单分析,来说说ORM框架的优缺点:
优点:
     1,开发起来简单,ORM框架将我们的对象模型转化为SQL语句,只需要掌握一些api就能够操作数据库,
        不用亲自处理sql语句了(下面greendao和原生Sqlite开发案例可以对比)。
     2,当面对一个复杂的程序时,其内部较多的数据处理,sql语句大量的硬编码,会让代码显得混乱和不
        易维护,ORM框架能让结构更清晰。

缺点:
     1,虽然ORM框架开发起来简单,但是我们需要掌握的东西却更多了,框架需要去学习,SQL原生操作需
        要去掌握。
     2,在一些复杂的数据库操作(如多表关联查询)时,ORM语法会变得十分复杂。直接用SQL语句会更清晰
        ,更直接。

  • 给出GreenDao的ORM体现图:


    GreenDao的ORM体现.png

(二)GreenDao的简单使用

1,版本说明:作为演示,本文中会用到目前最新的版本greenDao3.2.2,更多的信息可以去github看看greenDao

2,在build.grade中配置插件信息并引入依赖包

  • 2.1,声明添加的脚本类型和设置脚本运行环境
apply plugin: 'org.greenrobot.greendao'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
    }
}
  • 2.2,添加依赖库
compile 'org.greenrobot:greendao:3.2.2'
  • 2.3,自定义greendao的版本和路径
greendao {
    schemaVersion 1
    daoPackage 'com.example.android_db_biz.greedao'
    targetGenDir 'src/main/java'
}
  • 2.4,编译项目,引入Greendao

最后给出完整的build.gradle文件,里面有详细说明,这里在编译时会出现一个问题,具体解决办法可以借鉴这篇博文greenDao3.2.2配置出现的问题,这里面需要下载的文件我会在结束时给出。

apply plugin: 'com.android.library'

//1,声明添加的插件类型
apply plugin: 'org.greenrobot.greendao'

//2,设置脚本的运行环境(如果用在app启动模块,直接加3,4步,在lib模块整个需要加进去)
buildscript {

    repositories {
        //3,支持java 依赖库管理(maven/ivy),用于项目的依赖。
        mavenCentral() // add repository
    }

    dependencies {
        //4,依赖包的定义。支持maven/ivy,远程,本地库,也支持单文件
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
    }
}

//6,自定义Greendao版本和生成路径
greendao {
    //数据库版本号,数据库修改后这里一定要记得修改,否则会报错no such table
    schemaVersion 1
    //通过gradle插件生成的数据库相关文件的包名,默认为你的entity所在的包名
    daoPackage 'com.example.android_db_biz.greendao'
    //这就是我们上面说到的自定义生成数据库文件的目录了,可以将生成的文件放到我们的java目录中
    ,而不是build中,这样就不用额外的设置资源目录了
    targetGenDir 'src/main/java'
}

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt')
            , 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    //5,添加依赖库
    compile 'org.greenrobot:greendao:3.2.2'
    testCompile 'junit:junit:4.12'
}

3,创建一个实体类,实体类添加@Entity注解

@Entity
public class Song {

    @Id(autoincrement = true)
    private Long id;

    private String songName;

    private Integer songId;

    private String songDesc;

    private String cover;

    private Integer singerCode;

    private String singerName;

    private String createTime;

    private String updateTime;
}

这里自增长的id类型一定是Long/long类型,否则会报如下错误:

Error:Execution failed for task ':android-db-biz:greendao'.
> Can't add field `Variable(type=VariableType(name=int, isPrimitive=true
, originalName=int, typeArguments=null), name=id)` for entity Song due 
to: AUTOINCREMENT is only available to primary key properties of 
type long/Long

4,build(Build->Make Project)项目
会自动生成一些数据库相关类,这些类在build.gradle里设置的目录下,而且实体类里面也会自动生成get/set方法

image.png

这里有多少个@Entity注释的实体类就会生成多少个相关的XXXDao类,XXXDao类里提供对实体类对应的表单的CRUD的操作方法,即ORM里提供以面向对象的方式来处理关系型数据库,不需要我们去写sql语句。

5,greenDao的简单使用

  • 首先获取操作类DaoSession,默认数据库表存储在内存里,看看下面的DaoSessionManager :
public class DaoSessionManager {

    private final String DB_NAME = "android.db";
    private DaoMaster daoMaster;
    private DaoSession daoSession;

    private DaoSessionManager() {
    }

    public static DaoSessionManager mInstance = new DaoSessionManager();

    public static DaoSessionManager getInstace() {

        return mInstance;
    }

    public DaoMaster getDaoMaster(Context mContext) {

        DaoMaster.DevOpenHelper mHelper = new DaoMaster
                                   .DevOpenHelper(mContext, DB_NAME, null);
        daoMaster = new DaoMaster(mHelper.getWritableDatabase());
        return daoMaster;
    }

    public DaoSession getDaoSession(Context mContext) {

        if (daoSession == null) {

            if (daoMaster == null) {
                getDaoMaster(mContext);
            }
            daoSession = daoMaster.newSession();
        }
        return daoSession;
    }
}
  • 通过DaoSession获取上面实体类Song对应的表单操作类SongDao,并进行简单的CURD操作,具体复杂的SQL操作(如多表关联),这里就不说明了:
//获取Song这张表的操作类SongDao
DaoSession daoSession = DaoSessionManager.getInstace()
                                         .getDaoSession(getApplicationContext());
SongDao songDao = daoSession.getSongDao();

//创建一个对象
Song song = new Song();
song.setSingerCode(111);


//增加
songDao.insert(song);

//改
song.setSingerName("miss08");
songDao.update(song);

//查
Song query = songDao.queryBuilder().where(SongDao.Properties.SingerCode.eq(111))
        .list().get(0);

//删
songDao.delete(song);

可以看出,在进行增删改查的操作并不需要我们去写相应的sql语句,只需要调用songDao的相关API就行。

6,数据库设置存储为本地路径

  • 我现在调试用的小米手机,无法root所以没办法看到data/data目录下的数据情况,所有现在把数据库表单存储在手机本地目录下,方便查看数据库数据,对上面的DaoSessionManager的getDaoMaster方法进行修改(记得添加文件的读写权限)
//本地存储目录
private final String DB_PATH = "AndroidDevelopment/nc/miss08/database";

public DaoMaster getDaoMaster(Context mContext, final String path) {

    DaoMaster.DevOpenHelper mHelper = new VersionChangeHelper
        (new ContextWrapper(mContext) {

        @Override
        public SQLiteDatabase openOrCreateDatabase(String name, int mode, 
                                     SQLiteDatabase.CursorFactory factory) {
            return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
        }

        @Override
        public SQLiteDatabase openOrCreateDatabase(String name, int mode, 
        SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {
            return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
        }

        @Override
        public File getDatabasePath(String name) {
            File file = FileUtils.buildDataBasePath(path, name);
            return file != null ? file : super.getDatabasePath(name);
        }
    }, DB_NAME);
    daoMaster = new DaoMaster(mHelper.getWritableDatabase());
    return daoMaster;
}
  • 修改后,运行项目,在指定目录找到数据库文件android.db:


    greendao的数据库文件.png
  • 执行一次insert操作后,打开数据库查看Song表数据

sqlite数据库.png

7,数据库的版本升级

  • 在上面基础上,不更新数据库的表单,只增加数据库的版本号,会发现数据库里所有数据被清空,下面通过源码分析下。


    只修改版本信息.png

    在基本数据库操作里,Sqlite的数据库更新是在SQLiteOpenHelper里的onUpgrade里进行的,在GreenDao框架里肯定会继承它来定制自己框架的需求,在DaoMaster里我们找到了GreenDao用来处理版本升级的类DevOpenHelper

    /** WARNING: Drops all table on Upgrade! Use only during development. */
    public static class DevOpenHelper extends OpenHelper {
         
        ...
        @Override       //数据库版本升级会触发这个方法
        public void onUpgrade(Database db, int oldVersion, int newVersion) {
            dropAllTables(db, true);
            onCreate(db);
        }
    }

在其onUpgrade方法里会通过dropAllTables方法删除项目里所有的数据库,通过所有表单操作类XXXDao来删除所有表单。

    /** Drops underlying database table using DAOs. */
    public static void dropAllTables(Database db, boolean ifExists) {
       //项目里所有实体类操作Dao,都会在此删除表单,当前项目里只有一张表
        SongDao.dropTable(db, ifExists);
    }

    //找到SongDao的dropTable方法,就是执行一条删除表单的sql语句
    public static void dropTable(Database db, boolean ifExists) {
        String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"SONG\"";
        db.execSQL(sql);
    }

删除完成后,调用onCreate(db)方法创建所有表单,onCreate方法存在于DaoMaster里的OpenHelper类里,它是上面DevOpenHelper的父类

    public static abstract class OpenHelper extends DatabaseOpenHelper {
        ...
        @Override    //数据库创建时触发的方法
        public void onCreate(Database db) {
            Log.i("greenDAO"
            , "Creating tables for schema version " + SCHEMA_VERSION);
            createAllTables(db, false);
        }
    }

    //所有Dao都在此创建
    public static void createAllTables(Database db, boolean ifNotExists) {
       
        SongDao.createTable(db, ifNotExists);
    }

故,GreenDao默认在版本升级时会删除所有表单然后再创建,如果用户想自己控制版本升级的情况,就需要自己实现OpenHelper。

  • 项目每次编译运行时,DaoMaster里的内容都会恢复成默认状态,所以不要在DaoMaster的DevOpenHelper里进行业务操作。

  • 数据库升级需要用户自定义DaoMaster.OpenHelper,在其onUpgrade实现方法里进行版本对比更新。这里借用一个开源解决办法MigrationHelper类,来完成数据库的升级,有现成的我们就直接拿来用,里面代码也挺清晰的,如果不想用也可以自己通过DaoMaster来处理,或者更直接的使用sql语句来处理。

  • MigrationHelper在我的demo里与greenDao不兼容,故直接把源码拿来用,其github目录为MigrationHelper解决greenDao版本升级,大家可以直接去这里找符合你greenDao的版本,这里先给出用法,后面会进行分析。

通过MigrationHelper进行版本升级
  • 1,还是结合上面的例子,我们删除之前的本地数据库目录,并在之前的表单基础上再添加一张歌单表MenuInfo,这样项目中就存在两张表。
@Entity
public class MenuInfo {

    @Id(autoincrement = true)
    private Long id;

    private String menuCode;
    
    private String menuName;

    private String createTime;

    private String updateTime;
}
  • 2,build项目,在两张表里都添加两条数据。此时数据库的schemaVersion版本号为1


    menuInfo表创建.png

    song表创建.png
  • 3,如果我们在下个版本需要对MenuInfo的字段做在线更改,增加一个menuDesc字段,改为如下:

public class MenuInfo {

    @Id(autoincrement = true)
    private Long id;

    private String menuCode;
    
    private String menuName;

    //增加的字段
    private String menuDesc;

    private String createTime;

    private String updateTime;
}
  • 4,版本升级,自定义DaoMaster.OpenHelper,然后在build.gradle里将schemaVersion版本改为2
public class VersionChangeHelper extends DaoMaster.DevOpenHelper {

    public VersionChangeHelper(Context context, String name) {
        this(context, name, null);
    }

    public VersionChangeHelper(Context context, String name, SQLiteDatabase
        .CursorFactory factory) {
        super(context, name, factory);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        super.onUpgrade(db, oldVersion, newVersion);
        Log.e("miss08", "oldVersion = " + oldVersion + "newVersion = " + newVersion);
        MigrationHelper.migrate(db, new MigrationHelper.ReCreateAllTableListener() {

            @Override
            public void onCreateAllTables(Database db, boolean ifNotExists) {
                Log.e("dongyiming", "onCreateAllTables");
                //DaoMaster.createAllTables(db, ifNotExists);
            }

            @Override
            public void onDropAllTables(Database db, boolean ifExists) {
                Log.e("dongyiming", "onDropAllTables");
                //DaoMaster.dropAllTables(db, ifExists);
            }
        }, MenuInfoDao.class);
        }
    }
}

同时修改其引用的地方DaoSessionManager的getDaoMaster方法:
DevOpenHelper mHelper = new VersionChangeHelper(new ContextWrapper(mContext)
  • 5,运行项目,查看日志和数据库如下:
版本改变后,VersionChangeHelper的update方法被调用

10-08 18:22:07.797 22536-22536/com.example.pver.androiddevelopment E/miss08: 
oldVersion = 1_newVersion = 2

表单信息为:

menuInfo表修改数据.png

song表没有更新.png

发现两张表都是空表,这是因为我们在上面onUpgrade方法里super.onUpgrade(db, oldVersion, newVersion)会先执行父类DaoMaster.DevOpenHelper里的删除所有表单数据的方法,这个上面有分析,现在我们去掉super.onUpgrade后,再假设重复上面的操作,就只有我们做过更新的MenuInfo表为空,其他表单还是有原始的数据。

menuInfo表修改数据.png
song表没有更新.png
MigrationHelper的简单分析
  • 先看看MigrationHelper的migrate方法,它有三个重载方法,我们使用的那个方法有三个参数,分别为database,一个ReCreateAllTableListener回调,一个继承AbstractDao的class对象的可变参数类型。
    public static void migrate(SQLiteDatabase db
                         , Class<? extends AbstractDao<?, ?>>... daoClasses) {
        printLog("【The Old Database Version】" + db.getVersion());
        Database database = new StandardDatabase(db);
        migrate(database, daoClasses);
    }

    public static void migrate(Database database
                         , ReCreateAllTableListener listener
                         , Class<? extends AbstractDao<?, ?>>... daoClasses) {
        weakListener = new WeakReference<>(listener);
        migrate(database, daoClasses);
    }

AbstractDao作为所有表单操作类(如MenuInfoDao)的父类,项目中有多少表单在当前版本进行了修改,我们调用migrate方法,就需要传多少进去。

MigrationHelper.migrate(db,listener,xxDao.class,xxDao.class,xxDao.class...);

也可调用没有listener的migrate方法,即不用回调给我们来处理数据库,其使用一样:

MigrationHelper.migrate(db,xxDao.class,xxDao.class,xxDao.class...);

  • 在上面带listener的migrate方法里,首先用弱引用指向listener,后调用migrate另一个重载方法,传入database和修改的表单,方法如下。
    public static void migrate(Database database
             , Class<? extends AbstractDao<?, ?>>... daoClasses) {
        //轮询daoClasses,看对应的表单是否存在,并创建临时表
        generateTempTables(database, daoClasses);
        //获取接口对象,在最初调用时会new WeakReference(),所以这里的listener不为空 
        ReCreateAllTableListener listener = weakListener.get();
        if (listener != null) {
            //执行回调
            listener.onDropAllTables(database, true);
            printLog("【Drop all table by listener】");
            listener.onCreateAllTables(database, false);
            printLog("【Create all table by listener】");
        } else {
            dropAllTables(database, true, daoClasses);
            createAllTables(database, false, daoClasses);
        }
        printLog("【Restore data】start");
        restoreData(database, daoClasses);
        printLog("【Restore data】complete");
    }
  • 当 listener = null 时,会删除传入的Dao对应的那张表单,然后又重新创建这张表单,如果有新的数据就会存入更新后的表单里。

  • 当 listener != null 时,会通过listener回调给用户自己处理,更加的灵活。

  • 看看listener = null时删除和重新创建的方法,不管是创建还是删除最终会调用如下方法:

    private static void reflectMethod(Database db, String methodName, boolean 
    isExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
        if (daoClasses.length < 1) {
            return;
        }
        try {
            for (Class cls : daoClasses) {
                Method method = cls.getDeclaredMethod(methodName, Database.class
                , boolean.class);
                method.invoke(null, db, isExists);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

reflectMethod方法里,会对daoClasses进行轮询,通过反射获取我们我们传入的daoClasses(本例中只有一个为MenuInfoDao)里的名为methodName的方法对象,然后通过invoke执行MenuInfoDao类中的该方法,展示我们需要调用的两个方法dropTable和createTable。


    //传入的false
    /** Creates the underlying database table. */
    public static void createTable(Database db, boolean ifNotExists) {
        String constraint = ifNotExists? "IF NOT EXISTS ": "";
        db.execSQL("CREATE TABLE " + constraint + "\"MENU_INFO\" (" + //
                "\"_id\" INTEGER PRIMARY KEY AUTOINCREMENT ," + // 0: id
                "\"MENU_CODE\" TEXT," + // 1: menuCode
                "\"MENU_NAME\" TEXT," + // 2: menuName
                "\"MENU_DESC\" TEXT," + // 3: menuDesc
                "\"CREATE_TIME\" TEXT," + // 4: createTime
                "\"UPDATE_TIME\" TEXT);"); // 5: updateTime
    }

    //传入的true
    /** Drops the underlying database table. */
    public static void dropTable(Database db, boolean ifExists) {
        String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") 
                     + "\"MENU_INFO\"";
        db.execSQL(sql);
    }

这里会销毁之前的那张表然后重新创建一张新表,所以表里原有数据是不会存在的,这里只有传入的XXXDao.class做删除和重建的工作,其他表单是不会受影响的,有回调的就得看用户自己的需求了。

上面的例子演示了MigrationHelper对一个/多个表字段的更新(增删都一样),现在我们来演示在线增加一张/多张表。
  • 首先在项目里添加一张表Singer,然后build项目,现在整个项目中就存在三张表
@Entity
public class Singer {

    @Id(autoincrement = true)
    private Long id;

    private String singerCode;
    
    private String singerName;

}
  • 数据库版本加1,修改VersionChangeHelper方法如下,这里演示没有listener回调的方法,然后运行项目
@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
    super.onUpgrade(db, oldVersion, newVersion);
    Log.e("miss08", "oldVersion = " + oldVersion + "_newVersion = " + newVersion);

    MigrationHelper.migrate(db, SingerDao.class);
}
  • 项目运行时会出现一个问题,MigrationHelper里migrate对listener的非空判断,这里修改一下然后重新运行
public static void migrate(Database database
              , Class<? extends AbstractDao<?, ?>>... daoClasses) {
    printLog("【Generate temp table】start");
    generateTempTables(database, daoClasses);
    printLog("【Generate temp table】complete");
    //添加非空判断
    ReCreateAllTableListener listener = null;
    if (weakListener != null) {
        listener = weakListener.get();
    }
    if (listener != null) {
        ...省略
    }
}
  • 项目运行成功后,发现确实出现了三张表,这里只展示第三张表:


    数据库里所有表单.png

    singer表创建.png
最后说说更新版本时删除表操作
  • 实现方式挺多的,提供几种建议(前提是自己的helper里不要有super父类更新方法),这里我只用第一种方案试过并成功,其他几种就不演示了。
1,不使用MigrationHelper直接通过SingerDao里的dropTable(db, true)方法完成
2,不使用MigrationHelper直接通过sql语句完成
3,使用MigrationHelper,对里面代码简单修改下,通过reflectMethod来处理传入的daoClasses

(三)GreenDao源码的简单分析

说明:

要查看具体源码,可以到github下载greenDao,里面有它的源码和操作demo。

  • 1,用户可操作的几个类的关系图如下:


    GreenDao类图.png
  • 2,DaoGenerator:通过给定的模板Schema和路径生成相应的实体类和Dao相关的类。Schema的关系图如下:
Schema.png

Schema的操作方式如下,包含了数据库的相关信息:

//版本和包名
Schema schema = new Schema(1, "org.greenrobot.greendao.daotest2");
Entity keepEntity = schema2.addEntity("KeepEntity");
keepEntity .addIdProperty();
keepEntity .addStringProperty("count");
keepEntity .addStringProperty("select");
keepEntity .addStringProperty("sum");
keepEntity .addStringProperty("avg");
keepEntity .addStringProperty("join");

再来看看DaoGenerator里生成实体类和Daos的方法generateAll

/** Generates all entities and DAOs for the given schema. */
public void generateAll(Schema schema, String outDir, String outDirEntity
, String outDirTest) throws Exception {

    List<Entity> entities = schema.getEntities();
    for (Entity entity : entities) {
        generate(templateDao, outDirFile, entity.getJavaPackageDao()
           , entity.getClassNameDao(), schema, entity);
    }
    generate(templateDaoMaster, outDirFile, schema.getDefaultJavaPackageDao(),
            schema.getPrefix() + "DaoMaster", schema, null);
    generate(templateDaoSession, outDirFile, schema.getDefaultJavaPackageDao(),
            schema.getPrefix() + "DaoSession", schema, null);
}
  • 3,DaoMaster:插件生成的daos的最顶层,可以看它的注释(如下)

Master of DAO (schema version 16): knows all DAOs.

包含项目所有表单的创建工作createAllTables和升级处理(先dropAllTables再createAllTables)

public static void createAllTables(Database db, boolean ifNotExists) {
    SingerDao.createTable(db, ifNotExists);
    MenuInfoDao.createTable(db, ifNotExists);
    SongDao.createTable(db, ifNotExists);
}

public static void dropAllTables(Database db, boolean ifExists) {
    SingerDao.dropTable(db, ifExists);
    MenuInfoDao.dropTable(db, ifExists);
    SongDao.dropTable(db, ifExists);
}

还有DaoSession的创建方法,创建时会传入daoConfigMap,这个daoConfigMap是项目所有表单操作类XXXDao的class对象缓存,它是在创建DaoMaster时创建和存入class的。

public DaoSession newSession() {
    return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}

看看DaoMaster的构造方法,通过registerDaoClass方法把当前项目里所有的XXXDao的class对象缓存起来(这里不是实例对象,是class对象,一般用来搞反射)。

public DaoMaster(SQLiteDatabase db) {
    this(new StandardDatabase(db));
}

public DaoMaster(Database db) {
    super(db, SCHEMA_VERSION);
    registerDaoClass(SingerDao.class);
    registerDaoClass(MenuInfoDao.class);
    registerDaoClass(SongDao.class);
}

在DaoMaster的父类中AbstractDaoMaster缓存起来,
protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;

protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
    DaoConfig daoConfig = new DaoConfig(db, daoClass);
    daoConfigMap.put(daoClass, daoConfig);
}

registerDaoClass方法缓存数据前,会创建一个DaoConfig,DaoConfig里面存储了很多Dao的基本数据,如下

    public final Database db;
    public final String tablename;
    public final Property[] properties;     

    public final String[] allColumns;
    public final String[] pkColumns;
    public final String[] nonPkColumns;

我们再看看DaoConfig的这个构造方法,通过reflectProperties方法获取到Property数组。Property里的数据描述了映射到数据库里列的属性。用于创建查询构建器使用的对象(包含所有select查询用到的条件操作)。

public DaoConfig(Database db, Class<? extends AbstractDao<?, ?>> daoClass) {
    this.db = db;
    try {
        this.tablename = (String) daoClass.getField("TABLENAME").get(null);
        Property[] properties = reflectProperties(daoClass);

        allColumns = new String[properties.length];

        List<String> pkColumnList = new ArrayList<String>();
        List<String> nonPkColumnList = new ArrayList<String>();
        Property lastPkProperty = null;
        for (int i = 0; i < properties.length; i++) {
            Property property = properties[i];
            String name = property.columnName;
            allColumns[i] = name;
            if (property.primaryKey) {
                pkColumnList.add(name);
                lastPkProperty = property;
            } else {
                nonPkColumnList.add(name);
            }
        }
    }
}

reflectProperties这个方法里,通过反射获取AbstractDao类的内部类Properties里所有的静态字段和public字段,然后再通过field.get(null)获取所有字段的属性值,存储这些属性值。

private static Property[] reflectProperties(Class<? extends AbstractDao<?, ?>> 
daoClass) throws ClassNotFoundException, IllegalArgumentException
, IllegalAccessException {
    Class<?> propertiesClass = Class.forName(daoClass.getName() + "$Properties");
    Field[] fields = propertiesClass.getDeclaredFields();

    ArrayList<Property> propertyList = new ArrayList<Property>();
    final int modifierMask = Modifier.STATIC | Modifier.PUBLIC;
    for (Field field : fields) {
        if ((field.getModifiers() & modifierMask) == modifierMask) {
            Object fieldValue = field.get(null);
            if (fieldValue instanceof Property) {
                propertyList.add((Property) fieldValue);
            }
        }
    }
}

以SingerDao为例,看看其内部类的字段和属性:属性值就是一个Property对象,包含实体类的字段名,对应的数据库里的列名,以及类型

public class SingerDao extends AbstractDao<Singer, Long> {
    public static class Properties {
        public final static Property Id = new Property(0, Long.class
                              , "id", true, "_id");
        public final static Property SingerCode = new Property(1, String.class
                              , "singerCode", false, "SINGER_CODE");
        public final static Property SingerName = new Property(2, String.class
                              , "singerName", false, "SINGER_NAME");
        public final static Property SingerDesc = new Property(3, String.class
                              , "singerDesc", false, "SINGER_DESC");
    }
}
  • 4,DaoSession:在构造里创建出XXXDao的实例对象。
public DaoSession(Database db, IdentityScopeType type, Map<Class<?
        extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap) {
    super(db);
    singerDaoConfig = daoConfigMap.get(SingerDao.class).clone();
    singerDaoConfig.initIdentityScope(type);

    menuInfoDaoConfig = daoConfigMap.get(MenuInfoDao.class).clone();
    menuInfoDaoConfig.initIdentityScope(type);

    songDaoConfig = daoConfigMap.get(SongDao.class).clone();
    songDaoConfig.initIdentityScope(type);

    singerDao = new SingerDao(singerDaoConfig, this);
    menuInfoDao = new MenuInfoDao(menuInfoDaoConfig, this);
    songDao = new SongDao(songDaoConfig, this);

    registerDao(Singer.class, singerDao);
    registerDao(MenuInfo.class, menuInfoDao);
    registerDao(Song.class, songDao);
}

构造方法里,首先从传入的daoConfigMap里获取DaoConfig,通过原型模式创建DaoConfig对象,然后创建XXXDao的实例,DaoConfig对象做为Dao的构参传入,最后在DaoSession的父类里用Map<Class<?>, AbstractDao<?, ?>>集合缓存实例类和它的操作类对象。
接下来看看DaoSession的父类AbstractDaoSession,在AbstractDaoSession提供基本增删改查的方法,但是数据库真正的执行者确是AbstractDao,从上面的缓存里获取的AbstractDao。

    public <T> long insert(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
        return dao.insert(entity);
    }

    public <T> long insertOrReplace(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
        return dao.insertOrReplace(entity);
    }

    public <T> void update(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
        dao.update(entity);
    }

    public <T> void delete(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
        dao.delete(entity);
    }

    public <T, K> List<T> queryRaw(Class<T> entityClass, String where
                  , String... selectionArgs) {
        @SuppressWarnings("unchecked")
        AbstractDao<T, K> dao = (AbstractDao<T, K>) getDao(entityClass);
        return dao.queryRaw(where, selectionArgs);
    }

    public <T> QueryBuilder<T> queryBuilder(Class<T> entityClass) {
        @SuppressWarnings("unchecked")
        AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entityClass);
        return dao.queryBuilder();
    }

还有支持数据库的Rx方式(没用过,这里不分析)

    @Experimental
    public RxTransaction rxTx() {
        if (rxTxIo == null) {
            rxTxIo = new RxTransaction(this, Schedulers.io());
        }
        return rxTxIo;
    }

    @Experimental
    public RxTransaction rxTxPlain() {
        if (rxTxPlain == null) {
            rxTxPlain = new RxTransaction(this);
        }
        return rxTxPlain;
    }

接下来看看数据库的真实操作类XXXDao

  • 5,XXXDao:数据库提供的真实操作类:提供所有数据库的操作方式。这里就只通过查询语句来分析一下它的操作过程(以Singer表为例)。

GreenDao查询语句
singerDao.queryBuilder().where(Properties.SingerCode.eq("111")).orderAsc(Properties.SINGER_NAME).list();

对比下sqlite的查询的两种方式

1,db.rawQuery(sql)
   select * from SINGER where SINGER_CODE = ‘111’ order by SINGER_NAME asc;

2,db.query(...)
   query(String table, String[] columns, String selection,String[] selectionArgs
                    , String groupBy, String having,String orderBy, String limit)
   
   query("Singer",null,"SINGER_CODE = ?",new String[]{"111"},"null","null"
                    ,"SINGER_NAME asc")

下面分析GreenDao的查询,在XXXDao的父类AbstractDao里找到queryBuilder方法,该方法里会创建一个QueryBuilder对象,我们来看看QueryBuilder的描述:

Builds custom entity queries using constraints and parameters and without SQL 
(QueryBuilder creates SQL for you)

使用约束条件和参数来构造实体对象的查询,而不使用SQL语句(QueryBuilder 为我们生成SQL语句)

QueryBuilder的where方法,使用一个类WhereCondition来收集所有的条件,这个在下面的build方法里会被用到。

public QueryBuilder<T> where(WhereCondition cond, WhereCondition... condMore) {
    whereCollector.add(cond, condMore);
    return this;
}

接着看QueryBuilder类的orderAsc方法,实际会调用下面这个方法,分析见里面注释。

private void orderAscOrDesc(String ascOrDescWithLeadingSpace, Property... properties) 
{
    for (Property property : properties) {

        //获取StringBuilder对象拼接字符串,没有就创建,存在就append(",")
        checkOrderBuilder();
        //首先判断排序的字段属于Singer表里字段,然后拼装该columnName
        append(orderBuilder, property);
        //字段是String类型并且stringOrderCollation不为空,
        if (String.class.equals(property.type) && stringOrderCollation != null) {
            //拼接stringOrderCollation =  " COLLATE NOCASE"
            orderBuilder.append(stringOrderCollation);
        }
        //拼装ASC
        orderBuilder.append(ascOrDescWithLeadingSpace);
    }
}

最终会创建一个orderBuilder,里面包含“SINGER_NAME asc”

最后看QueryBuilder类的list方法,list方法里先执行build方法,然后执行build方法返回对象的list方法。

public List<T> list() {
    return build().list();
}

先找到build方法,build里会做很多拼接工作。

public Query<T> build() {

    //创建一个StringBuilder对象,拼接工作下面分析
    StringBuilder builder = createSelectBuilder();

    //判断是否有LIMIT和OFFSET条件,如果有则拼接上
    int limitPosition = checkAddLimit(builder);
    int offsetPosition = checkAddOffset(builder);

    String sql = builder.toString();
    checkLog(sql);

    //返回一个Query对象,查询操作返回的结果(实体类或游标),values是position的值
    return Query.create(dao, sql, values.toArray(), limitPosition, offsetPosition);
}

重点看看上面build方法里执行的createSelectBuilder方法,他会拼接返回完整的用于查询条件的select语句

private StringBuilder createSelectBuilder() {

    //创建一个StringBuilder,拼接“SELECT FROM tablename”
    String select = SqlUtils.createSqlSelect(dao.getTablename(), tablePrefix
    , dao.getAllColumns(), distinct);
    StringBuilder builder = new StringBuilder(select);

    //如果上面的whereCollector条件集合有值,拼接“WHERE”和条件
    appendJoinsAndWheres(builder, tablePrefix);

    //orderBuilder如果存在则拼接“ORDER BY ”和orderBuilder,这个在上面分析过
    if (orderBuilder != null && orderBuilder.length() > 0) {
        builder.append(" ORDER BY ").append(orderBuilder);
    }

    //返回最终拼装的StringBuilder
    return builder;
}

再看最后的list方法调用,这个方法在类Query里,找到了我们最熟悉的Sqlite操作dao.getDatabase().rawQuery,查询传入我们上面拼接的sql语句和传入的参数值,得到游标,轮询就能返回结果。

public List<T> list() {
    checkThread();
    Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
    return daoAccess.loadAllAndCloseCursor(cursor);
}

最后说明

在分析完GreenDao的查询流程后,发现XXXDao对数据库的操作最终还是Sqlite里对sql语句的操作。体现了ORM面向对象操作和关系数据表操作的映射,所以要想熟练使用GreenDao对数据库的操作,必须得先掌握SQLite里对数据库的操作和常用的SQL语句的书写。

(四)Sqlite数据库的使用案例:

下面这个案例是之前的公司项目在迭代Orimlite框架前,对数据库的操作,对比下GreenDao的使用方式,这里就直接给出代码,就不做多余的分析,最终的demo里也会给出完整代码。

  • 1,创建基类
public class Singer {

    private int id;

    private String singerCode;

    private String singerName;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getSingerCode() {
        return singerCode;
    }

    public void setSingerCode(String singerCode) {
        this.singerCode = singerCode;
    }

    public String getSingerName() {
        return singerName;
    }

    public void setSingerName(String singerName) {
        this.singerName = singerName;
    }
}
  • 2,自定义SqliteOpenHelper,设置版本号,数据库名称,完成初始化表格的创建和更新的操作
public class DBHelper extends SQLiteOpenHelper {

    //数据库名
    private static final String DB_NAME = "android2.db";
    //版本号
    private static final int VERSION_CODE = 1;
    //创建table的sql语句
    private static final String SQL_CREATE_TABLE = String.format
                    ("create table %s ( " +
                    "%s integer primary key autoincrement" +
                    ",%s text" +
                    ",%s text)"
            , ISingerRepository.TABLE
            , ISingerRepository.ID
            , ISingerRepository.SINGER_CODE
            , ISingerRepository.SINGER_NAME);

    public DBHelper(Context mContext) {
        super(mContext, DB_NAME, null, VERSION_CODE);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        //增加一列
        String sql = String.format("alter table %s add %s text", ISingerRepository
              .TABLE, ISingerRepository.SINGER_DESC);
        db.execSQL(sql);
    }
}
  • 3,把表单存储到sd卡里,需要自定义ContextWrapper,在构建helper时传入这个context即可
public class DataBaseContext extends ContextWrapper {

    private static final String DB_PATH = "AndroidDevelopment/nc/miss08/database";

    public DataBaseContext(Context base) {
        super(base);
    }

    @Override
    public SQLiteDatabase openOrCreateDatabase(String name, int mode
                            , SQLiteDatabase.CursorFactory factory) {
        return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
    }

    @Override
    public SQLiteDatabase openOrCreateDatabase(String name, int mode
    , SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {
        return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
    }

    @Override
    public File getDatabasePath(String name) {

        File file = FileUtils.buildDataBasePath(DB_PATH, name);
        return file != null ? file : super.getDatabasePath(name);
    }
}
  • 4,把数据库操作的一些公共方法放到一个BaseRepository中
public class BaseRepository<T> {

    public final DBHelper dbHelper;

    public BaseRepository(Context mContext) {
        DataBaseContext context = new DataBaseContext(mContext);
        this.dbHelper = new DBHelper(context);
    }

    public long insert(String table, String nullColumnHack, ContentValues values) {
        long ret = 0L;
        SQLiteDatabase database = this.dbHelper.getWritableDatabase();
        database.beginTransaction();

        try {
            ret = database.insert(table, nullColumnHack, values);
            database.setTransactionSuccessful();
        } catch (RuntimeException var11) {
            Log.e("miss08", "exception : " + var11);
        } finally {
            database.endTransaction();
        }

        return ret;
    }

    public <T> List<T> query(String table, String[] columns, String selection
      , String[] selectionArgs, String groupBy, String having, String orderBy
      , Integer limit) {
        Object results = new ArrayList();
        Cursor cursor = null;

        try {
            if (limit != null) {
                cursor = this.dbHelper.getReadableDatabase().query(table, columns
              , selection , selectionArgs, groupBy, having, orderBy, limit + "");
            } else {
                cursor = this.dbHelper.getReadableDatabase().query(table, columns
                          , selection, selectionArgs, groupBy, having, orderBy);
            }

            results = this.queryResult(cursor);
        } catch (RuntimeException var15) {
            Log.e("miss08", "exception : " + var15);
        } finally {
            if (cursor != null) {
                cursor.close();
            }

        }

        return (List) results;
    }

    public <T> List<T> query(String table, String[] columns, String selection
    , String[] selectionArgs, String groupBy, String having, String orderBy) {
        return this.query(table, columns, selection, selectionArgs, groupBy, having
       , orderBy, (Integer) null);
    }

    public <T> List<T> queryResult(Cursor cursor) {
        throw new RuntimeException("Please overwrite method.");
    }

    public int update(String table, ContentValues values, String whereClause
                           , String[] whereArgs) {
        int ret = 0;
        SQLiteDatabase database = this.dbHelper.getWritableDatabase();
        database.beginTransaction();

        try {
            ret = database.update(table, values, whereClause, whereArgs);
            database.setTransactionSuccessful();
        } catch (RuntimeException var11) {
            Log.e("miss08", "exception : " + var11);
        } finally {
            database.endTransaction();
        }

        return ret;
    }

    public int delete(String table, String whereClause, String[] whereArgs) {
        int ret = 0;
        SQLiteDatabase database = this.dbHelper.getWritableDatabase();
        database.beginTransaction();

        try {
            ret = database.delete(table, whereClause, whereArgs);
            database.setTransactionSuccessful();
        } catch (RuntimeException var10) {
            Log.e("miss08", "exception : " + var10);
        } finally {
            database.endTransaction();
        }

        return ret;
    }
}
  • 5,创建Singer表的操作类SingerRepository
public class SingerRepository extends BaseRepository<Singer> 
                         implements ISingerRepository<Singer, Integer> {

    public SingerRepository(Context mContext) {
        super(mContext);
    }

    @Override
    public long add(Singer singer) {

        ContentValues cv = getContentValues(singer);
        long res = insert(TABLE, null, cv);
        return res;
    }

    @Override
    public int update(Singer singer) {
        ContentValues cv = getContentValues(singer);
        String whereClause = String.format("%s = ?", SINGER_CODE);
        update(TABLE, cv, whereClause, new String[]{singer.getSingerCode()});
        return 0;
    }

    @Override
    public Singer queryById(Integer id) {

        String sql = String.format("%s = ?", ID);
        List<Singer> singerList = query(TABLE, null, sql
                , new String[]{String.valueOf(id)}, null, null, null);
        if (singerList != null) {
            return singerList.get(0);
        }
        return null;
    }

    @Override
    public List<Singer> queryForAll() {
        return query(TABLE, null, null, null, null, null, null);
    }

    @Override
    public int delete(Integer id) {

        String sql = String.format("delete from %s where %s = ?", TABLE, ID);
        dbHelper.getWritableDatabase()
                .execSQL(sql, new String[]{String.valueOf(id)});
        return 0;
    }

    public ContentValues getContentValues(Singer singer) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(SINGER_CODE, singer.getSingerCode());
        contentValues.put(SINGER_NAME, singer.getSingerName());
        return contentValues;
    }

    //查询需要对cursor遍历,父类需要的方法
    public List<Singer> queryResult(Cursor cursor) {
        List<Singer> list = new ArrayList<>();
        while (cursor.moveToNext()) {
            Singer singer = new Singer();
            singer.setId(cursor.getInt(cursor.getColumnIndex(ID)));
            singer.setSingerName(
                       cursor.getString(cursor.getColumnIndex(SINGER_NAME)));
            singer.setSingerCode(
                       cursor.getString(cursor.getColumnIndex(SINGER_CODE)));
            list.add(singer);
        }
        return list;
    }
}
  • 6,在接口里统一Singer表的名称和字段以及方法
public interface ISingerRepository<T, ID> {

    public static final String TABLE = "singer";
    public static final String COLUMN_PREFIX = TABLE.concat("_");
    public static final String ID = "_id";
    public static final String SINGER_CODE = COLUMN_PREFIX + "code";
    public static final String SINGER_NAME = COLUMN_PREFIX + "name";
    public static final String SINGER_DESC = COLUMN_PREFIX + "desc";

    //只给出简单的增删改查
    public long add(T var1);

    public int update(T var1);

    T queryById(ID var1);

    List<T> queryForAll();

    int delete(ID var1);
}
  • 7,在MainActivity里操作数据库
SingerRepository singerRepository = new SingerRepository(this);
Singer singer = new Singer();
singer.setSingerCode("112");
singer.setSingerName("张三");
singerRepository.add(singer);
  • 8,上面Demo自测表的创建成功,而且对表的所有操作方法都通过验证。

总结:

上面就是所有对GreenDao的简单分析,由于公司使用的是ORMLite框架,所以GreenDao里比较复杂的一些操作(多表关联/事物的处理/版本跳级更新/融合Rxjava等)这里都没演示,一是使用不熟练,二是目前确实没时间慢慢弄,文章里所有demo都是自己操作成功的,如果有什么其他问题和建议可留言。

其他:

本文所有demo的github地址(gradle-3.5-all在项目根目录下,解压使用):GreenDao和SQLite的使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 一、关于greenDAO greenDAO应该算是当前最火的数据库开源框架了,它是一个将对象映射到SQLite数据...
    当幸福来敲门58阅读 13,755评论 3 19
  • 序言 (写分享前的序言)大家好.很高兴能为大家分享Android之GreenDao笔记,也希望能和大家一起学习进步...
    王黎聪阅读 1,052评论 4 11
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • GreenDao 介绍:greenDAO是一个对象关系映射(ORM)的框架,能够提供一个接口通过操作对象的方式去操...
    小董666阅读 701评论 0 1