Android主流三方库源码分析(四、深入理解GreenDao源码)

4,880 阅读17分钟

前言

前两篇我们详细地分析了Android的网络底层框架OKHttp和封装框架Retrofit的核心源码,如果对OKHttp或Retrofit内部机制不了解的可以看看Android主流三方库源码分析(一、深入理解OKHttp源码)Android主流三方库源码分析(二、深入理解Retrofit源码),除了热门的网络库之外,我们还分析了使用最广泛的图片加载框架Glide的加载流程,大家读完这篇源码分析实力会有不少提升,有兴趣可以看看Android主流三方库源码分析(三、深入理解Glide源码)。本篇,我们将会来对目前Android数据库框架中性能最好的GreenDao来进行较为深入地讲解。

一、基本使用流程

1、导入GreenDao的代码生成插件和库

// 项目下的build.gradle
buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.0'
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1' 
    }
}

// app模块下的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao'

...

dependencies {
    ...
    compile 'org.greenrobot:greendao:3.2.0' 
}

2、创建一个实体类,这里为HistoryData

@Entity
public class HistoryData {

    @Id(autoincrement = true)
    private Long id;

    private long date;

    private String data;
}

3、选择ReBuild Project,HistoryData会被自动添加Set/get方法,并生成整个项目的DaoMaster、DaoSession类,以及与该实体HistoryData对应的HistoryDataDao。

image

@Entity
public class HistoryData {

    @Id(autoincrement = true)
    private Long id;

    private long date;

    private String data;

    @Generated(hash = 1371145256)
    public HistoryData(Long id, long date, String data) {
        this.id = id;
        this.date = date;
        this.data = data;
    }

    @Generated(hash = 422767273)
    public HistoryData() {
    }

    public Long getId() {
        return this.id;
    }

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

    public long getDate() {
        return this.date;
    }

    public void setDate(long date) {
        this.date = date;
    }

    public String getData() {
        return this.data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

这里点明一下这几个类的作用:

  • DaoMaster:所有Dao类的主人,负责整个库的运行,内部的静态抽象子类DevOpenHelper继承并重写了Android的SqliteOpenHelper。
  • DaoSession:作为一个会话层的角色,用于生成相应的Dao对象、Dao对象的注册,操作Dao的具体对象。
  • xxDao(HistoryDataDao):生成的Dao对象,用于进行具体的数据库操作。

4、获取并使用相应的Dao对象进行增删改查操作

DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, Constants.DB_NAME);
SQLiteDatabase database = devOpenHelper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(database);
mDaoSession = daoMaster.newSession();
HistoryDataDao historyDataDao = daoSession.getHistoryDataDao();

// 省略创建historyData的代码
...

// 增
historyDataDao.insert(historyData);

// 删
historyDataDao.delete(historyData);

// 改
historyDataDao.update(historyData);

// 查
List<HistoryData> historyDataList = historyDataDao.loadAll();

本篇文章将会以上述使用流程来对GreenDao的源码进行逐步分析,最后会分析下GreenDao中一些优秀的特性,让读者朋友们对GreenDao的理解有更一步的加深。

二、GreenDao使用流程分析

1、创建数据库帮助类对象DaoMaster.DevOpenHelper

DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, Constants.DB_NAME);

创建GreenDao内部实现的数据库帮助类对象devOpenHelper,核心源码如下:

public class DaoMaster extends AbstractDaoMaster {

    ...

    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);
        }
    }
    
    public static class DevOpenHelper extends OpenHelper {
    
        ...
        
        @Override
        public void onUpgrade(Database db, int oldVersion, int newVersion) {
            Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables");
            dropAllTables(db, true);
            onCreate(db);
        }
    }
}

DevOpenHelper自身实现了更新的逻辑,这里是弃置了所有的表,并且调用了OpenHelper实现的onCreate方法用于创建所有的表,其中DevOpenHelper继承于OpenHelper,而OpenHelper自身又继承于DatabaseOpenHelper,那么,这个DatabaseOpenHelper这个类的作用是什么呢?

public abstract class DatabaseOpenHelper extends SQLiteOpenHelper {

    ...
    
    // 关注点1
    public Database getWritableDb() {
        return wrap(getWritableDatabase());
    }
    
    public Database getReadableDb() {
        return wrap(getReadableDatabase());
    }   
    
    protected Database wrap(SQLiteDatabase sqLiteDatabase) {
        return new StandardDatabase(sqLiteDatabase);
    }
    
    ...
    
    // 关注点2
    public Database getEncryptedWritableDb(String password) {
        EncryptedHelper encryptedHelper = checkEncryptedHelper();
        return encryptedHelper.wrap(encryptedHelper.getWritableDatabase(password));
    }
    
    public Database getEncryptedReadableDb(String password) {
        EncryptedHelper encryptedHelper = checkEncryptedHelper();
        return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password));
    }
    
    ...
    
    private class EncryptedHelper extends net.sqlcipher.database.SQLiteOpenHelper {
    
        ...
    
    
        protected Database wrap(net.sqlcipher.database.SQLiteDatabase     sqLiteDatabase) {
            return new EncryptedDatabase(sqLiteDatabase);
        }
    }
    

其实,DatabaseOpenHelper也是实现了SQLiteOpenHelper的一个帮助类,它内部可以获取到两种不同的数据库类型,一种是标准型的数据库StandardDatabase,另一种是加密型的数据库EncryptedDatabase,从以上源码可知,它们内部都通过wrap这样一个包装的方法,返回了对应的数据库类型,我们大致看一下StandardDatabase和EncryptedDatabase的内部实现。

public class StandardDatabase implements Database {

    // 这里的SQLiteDatabase是android.database.sqlite.SQLiteDatabase包下的
    private final SQLiteDatabase delegate;

    public StandardDatabase(SQLiteDatabase delegate) {
        this.delegate = delegate;
    }

    @Override
    public Cursor rawQuery(String sql, String[] selectionArgs) {
        return delegate.rawQuery(sql, selectionArgs);
    }

    @Override
    public void execSQL(String sql) throws SQLException {
        delegate.execSQL(sql);
    }

    ...
}

public class EncryptedDatabaseStatement implements DatabaseStatement     {

    // 这里的SQLiteStatement是net.sqlcipher.database.SQLiteStatement包下的
    private final SQLiteStatement delegate;

    public EncryptedDatabaseStatement(SQLiteStatement delegate) {
        this.delegate = delegate;
    }

    @Override
    public void execute() {
        delegate.execute();
    }
    
    ...
}

StandardDatabase和EncryptedDatabase这两个类内部都使用了代理模式给相同的接口添加了不同的具体实现,StandardDatabase自然是使用的Android包下的SQLiteDatabase,而EncryptedDatabaseStatement为了实现加密数据库的功能,则使用了一个叫做sqlcipher的数据库加密三方库,如果你项目下的数据库需要保存比较重要的数据,则可以使用getEncryptedWritableDb方法来代替getdWritableDb方法对数据库进行加密,这样,我们之后的数据库操作则会以代理模式的形式间接地使用sqlcipher提供的API去操作数据库

2、创建DaoMaster对象

SQLiteDatabase database = devOpenHelper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(database);

首先,DaoMaster作为所有Dao对象的主人,它内部肯定是需要一个SQLiteDatabase对象的,因此,先由DaoMaster的帮助类对象devOpenHelper的getWritableDatabase方法得到一个标准的数据库类对象database,再由此创建一个DaoMaster对象。

public class DaoMaster extends AbstractDaoMaster {

    ...

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

    public DaoMaster(Database db) {
        super(db, SCHEMA_VERSION);
        registerDaoClass(HistoryDataDao.class);
    }
    
    ...
}

在DaoMaster的构造方法中,它首先执行了super(db, SCHEMA_VERSION)方法,即它的父类AbstractDaoMaster的构造方法。

public abstract class AbstractDaoMaster {

    ...

    public AbstractDaoMaster(Database db, int schemaVersion) {
        this.db = db;
        this.schemaVersion = schemaVersion;

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

在AbstractDaoMaster对象的构造方法中,除了记录当前的数据库对象db和版本schemaVersion之外,还创建了一个类型为HashMap<Class>, DaoConfig>()的daoConfigMap对象用于保存每一个DAO对应的数据配置对象DaoConfig,并且Daoconfig对象存储了对应的Dao对象所必需的数据。最后,在DaoMaster的构造方法中使用了registerDaoClass(HistoryDataDao.class)方法将HistoryDataDao类对象进行了注册,实际上,就是为HistoryDataDao这个Dao对象创建了相应的DaoConfig对象并将它放入daoConfigMap对象中保存起来。

3、创建DaoSession对象

mDaoSession = daoMaster.newSession();

在DaoMaster对象中使用了newSession方法新建了一个DaoSession对象。

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

在DaoSeesion的构造方法中,又做了哪些事情呢?

public class DaoSession extends AbstractDaoSession {

    ...

    public DaoSession(Database db, IdentityScopeType type, Map<Class<?     extends AbstractDao<?, ?>>, DaoConfig>
            daoConfigMap) {
        super(db);

        historyDataDaoConfig = daoConfigMap.get(HistoryDataDao.class).clone();
        historyDataDaoConfig.initIdentityScope(type);

        historyDataDao = new HistoryDataDao(historyDataDaoConfig, this);

        registerDao(HistoryData.class, historyDataDao);
    }
    
    ...
}

首先,调用了父类AbstractDaoSession的构造方法。

public class AbstractDaoSession {

    ...

    public AbstractDaoSession(Database db) {
        this.db = db;
        this.entityToDao = new HashMap<Class<?>, AbstractDao<?, ?>>();
    }
    
    protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
        entityToDao.put(entityClass, dao);
    }
    
    ...
}

在AbstractDaoSession构造方法里面创建了一个实体与Dao对象的映射集合。接下来,在DaoSession的构造方法中还做了2件事:

  • 1、创建每一个Dao对应的DaoConfig对象,这里是historyDataDaoConfig,并且根据IdentityScopeType的类型初始化创建一个相应的IdentityScope,根据type的不同,它有两种类型,分别是IdentityScopeObjectIdentityScopeLong,它的作用是根据主键缓存对应的实体数据。当主键是数字类型的时候,如long/Long、int/Integer、short/Short、byte/Byte,则使用IdentityScopeLong缓存实体数据,当主键不是数字类型的时候,则使用IdentityScopeObject缓存实体数据。
  • 2、根据DaoSession对象和每一个Dao对应的DaoConfig对象,创建与之对应的historyDataDao对象,由于这个项目只创建了一个实体类HistoryData,因此这里只有一个Dao对象historyDataDao,然后就是注册Dao对象,其实就是将实体和对应的Dao对象放入entityToDao这个映射集合中保存起来了。

4、插入源码分析

HistoryDataDao historyDataDao = daoSession.getHistoryDataDao();

// 增
historyDataDao.insert(historyData);

这里首先在会话层DaoSession中获取了我们要操作的Dao对象HistoryDataDao,然后插入了一个我们预先创建好的historyData实体对象。其中HistoryDataDao继承了AbstractDao<HistoryData, Long> 。

public class HistoryDataDao extends AbstractDao<HistoryData, Long> {
    ...
}

那么,这个AbstractDao是干什么的呢?

public abstract class AbstractDao<T, K> {

    ...
    
    public List<T> loadAll() {
        Cursor cursor = db.rawQuery(statements.getSelectAll(), null);
        return loadAllAndCloseCursor(cursor);
    }
    
    ...
    
    public long insert(T entity) {
        return executeInsert(entity, statements.getInsertStatement(),     true);
    }
    
    ...
    
    public void delete(T entity) {
        assertSinglePk();
        K key = getKeyVerified(entity);
        deleteByKey(key);
    }
    
    ...

}

看到这里,根据程序员优秀的直觉,大家应该能猜到,AbstractDao是所有Dao对象的基类,它实现了实体数据的操作如增删改查。我们接着分析insert是如何实现的,在AbstractDao的insert方法中又调用了executeInsert这个方法。在这个方法中,第二个参里的statements是一个TableStatements对象,它是在AbstractDao初始化构造器时从DaoConfig对象中取出来的,是一个根据指定的表格创建SQL语句的一个帮助类。使用statements.getInsertStatement()则是获取了一个插入的语句。而第三个参数则是判断是否是主键的标志。

public class TableStatements {

    ...

    public DatabaseStatement getInsertStatement() {
        if (insertStatement == null) {
            String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns);
            DatabaseStatement newInsertStatement = db.compileStatement(sql);
            ...
        }
        return insertStatement;
    }

    ...
}

在TableStatements的getInsertStatement方法中,主要做了两件事:

  • 1、使用SqlUtils创建了插入的sql语句
  • 2、根据不同的数据库类型(标准数据库或加密数据库)将sql语句编译成当前数据库对应的语句

我们继续往下分析executeInsert的执行流程。

private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
    long rowId;
    if (db.isDbLockedByCurrentThread()) {
        rowId = insertInsideTx(entity, stmt);
    } else {
        db.beginTransaction();
        try {
            rowId = insertInsideTx(entity, stmt);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }
    if (setKeyAndAttach) {
        updateKeyAfterInsertAndAttach(entity, rowId, true);
    }
    return rowId;
}

这里首先是判断数据库是否被当前线程锁定,如果是,则直接插入数据,否则为了避免死锁,则开启一个数据库事务,再进行插入数据的操作。最后如果设置了主键,则在插入数据之后更新主键的值并将对应的实体缓存到相应的identityScope中,这一块的代码流程如下所示:

protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) {
    if (rowId != -1) {
        K key = updateKeyAfterInsert(entity, rowId);
        attachEntity(key, entity, lock);
    } else {
       ...
    }
}

protected final void attachEntity(K key, T entity, boolean lock) {
    attachEntity(entity);
    if (identityScope != null && key != null) {
        if (lock) {
            identityScope.put(key, entity);
        } else {
            identityScope.putNoLock(key, entity);
        }
    }
}

接着,我们还是继续追踪主线流程,在executeInsert这个方法中调用了insertInsideTx进行数据的插入。

private long insertInsideTx(T entity, DatabaseStatement stmt) {
    synchronized (stmt) {
        if (isStandardSQLite) {
            SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
            bindValues(rawStmt, entity);
            return rawStmt.executeInsert();
        } else {
            bindValues(stmt, entity);
            return stmt.executeInsert();
        }
    }
}

为了防止并发,这里使用了悲观锁保证了数据的一致性,在AbstractDao这个类中,大量使用了这种锁保证了它的线程安全性。接着,如果当前是标准数据库,则直接获取stmt这个DatabaseStatement类对应的原始语句进行实体字段属性的绑定和最后的执行插入操作。如果是加密数据库,则直接使用当前的加密数据库所属的插入语句进行实体字段属性的绑定和执行最后的插入操作。其中bindValues这个方法对应的实现类就是我们的HistoryDataDao类。

public class HistoryDataDao extends AbstractDao<HistoryData, Long> {

    ...

    @Override
    protected final void bindValues(DatabaseStatement stmt, HistoryData     entity) {
        stmt.clearBindings();

        Long id = entity.getId();
        if (id != null) {
            stmt.bindLong(1, id);
        }
        stmt.bindLong(2, entity.getDate());

        String data = entity.getData();
        if (data != null) {
            stmt.bindString(3, data);
        }
    }
    
    @Override
    protected final void bindValues(SQLiteStatement stmt, HistoryData     entity) {
        stmt.clearBindings();

        Long id = entity.getId();
        if (id != null) {
            stmt.bindLong(1, id);
        }
        stmt.bindLong(2, entity.getDate());

        String data = entity.getData();
        if (data != null) {
            stmt.bindString(3, data);
        }
    }

    ...
}

可以看到,这里对HistoryData的所有字段使用对应的数据库语句进行了绑定操作。这里最后再提及一下,如果当前数据库是加密型时,则会使用最开始提及的DatabaseStatement的加密实现类EncryptedDatabaseStatement应用代理模式去使用sqlcipher这个加密型数据库的insert方法

5、查询源码分析

经过对插入源码的分析,我相信大家对GreenDao内部的机制已经有了一些自己的理解,由于删除和更新内部的流程比较简单,且与插入源码有异曲同工之妙,这里就不再赘述了。最后我们再分析下查询的源码,查询的流程调用链较长,所以将它的核心流程源码直接给出。

List<HistoryData> historyDataList = historyDataDao.loadAll();

public List<T> loadAll() {
    Cursor cursor = db.rawQuery(statements.getSelectAll(), null);
    return loadAllAndCloseCursor(cursor);
}

protected List<T> loadAllAndCloseCursor(Cursor cursor) {
    try {
        return loadAllFromCursor(cursor);
    } finally {
        cursor.close();
    }
}

protected List<T> loadAllFromCursor(Cursor cursor) {
    int count = cursor.getCount();
    ...
    boolean useFastCursor = false;
    if (cursor instanceof CrossProcessCursor) {
        window = ((CrossProcessCursor) cursor).getWindow();
        if (window != null) {  
            if (window.getNumRows() == count) {
                cursor = new FastCursor(window);
                useFastCursor = true;
            } else {
              ...
            }
        }
    }

    if (cursor.moveToFirst()) {
        ...
        try {
            if (!useFastCursor && window != null && identityScope != null) {
                loadAllUnlockOnWindowBounds(cursor, window, list);
            } else {
                do {
                    list.add(loadCurrent(cursor, 0, false));
                } while (cursor.moveToNext());
            }
        } finally {
            ...
        }
    }
    return list;
}

最终,loadAll方法将会调用到loadAllFromCursor这个方法,首先,如果当前的游标cursor是跨进程的cursor,并且cursor的行数没有偏差的话,则使用一个加快版的FastCursor对象进行游标遍历。接着,不管是执行loadAllUnlockOnWindowBounds这个方法还是直接加载当前的数据列表list.add(loadCurrent(cursor, 0, false)),最后都会调用到这行list.add(loadCurrent(cursor, 0, false))代码,很明显,loadCurrent方法就是加载数据的方法。

final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
    if (identityScopeLong != null) {
        ...
        T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key);
        if (entity != null) {
            return entity;
        } else {
            entity = readEntity(cursor, offset);
            attachEntity(entity);
            if (lock) {
                identityScopeLong.put2(key, entity);
            } else {
                identityScopeLong.put2NoLock(key, entity);
            }
            return entity;
        }
    } else if (identityScope != null) {
        ...
        T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key);
        if (entity != null) {
            return entity;
        } else {
            entity = readEntity(cursor, offset);
            attachEntity(key, entity, lock);
            return entity;
        }
    } else {
        ...
        T entity = readEntity(cursor, offset);
        attachEntity(entity);
        return entity;
    }
}

我们来理解下loadCurrent这个方法内部的执行策略。首先,如果有实体数据缓存identityScopeLong/identityScope,则先从缓存中取,如果缓存中没有,会使用该实体对应的Dao对象,这里的是HistoryDataDao,它在内部根据游标取出的数据新建了一个新的HistoryData实体对象返回。

@Override
public HistoryData readEntity(Cursor cursor, int offset) {
    HistoryData entity = new HistoryData( //
        cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id
        cursor.getLong(offset + 1), // date
        cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2) // data
    );
    return entity;
}

最后,如果是非identityScopeLong缓存类型,即是属于identityScope的情况下,则还会在identityScope中将上面获得的数据进行缓存。如果没有实体数据缓存的话,则直接调用readEntity组装数据返回即可。

注意:对于GreenDao缓存的特性,可能会出现没有拿到最新数据的bug,因此,如果遇到这种情况,可以使用DaoSession的clear方法删除缓存。

三、GreenDao是如何与ReactiveX结合?

首先,看下与rx结合的使用流程:

RxDao<HistoryData, Long> xxDao = daoSession.getHistoryDataDao().rx();
xxDao.insert(historyData)
        .observerOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<HistoryData>() {
            @Override
            public void call(HistoryData entity) {
                // insert success
            }
        });

在AbstractDao对象的.rx()方法中,创建了一个默认执行在io线程的rxDao对象。

@Experimental
public RxDao<T, K> rx() {
    if (rxDao == null) {
        rxDao = new RxDao<>(this, Schedulers.io());
    }
    return rxDao;
}

接着分析rxDao的insert方法。

@Experimental
public Observable<T> insert(final T entity) {
    return wrap(new Callable<T>() {
        @Override
        public T call() throws Exception {
            dao.insert(entity);
            return entity;
        }
    });
}

起实质作用的就是这个wrap方法了,在这个方法里面主要是调用了RxUtils.fromCallable(callable)这个方法。

@Internal
class RxBase {

    ...

    protected <R> Observable<R> wrap(Callable<R> callable) {
        return wrap(RxUtils.fromCallable(callable));
    }

    protected <R> Observable<R> wrap(Observable<R> observable) {
        if (scheduler != null) {
            return observable.subscribeOn(scheduler);
        } else {
            return observable;
        }
    }

    ...
}

在RxUtils的fromCallable这个方法内部,其实就是使用defer这个延迟操作符来进行被观察者事件的发送,主要目的就是为了确保Observable被订阅后才执行。最后,如果调度器scheduler存在的话,将通过外部的wrap方法将执行环境调度到io线程。

@Internal
class RxUtils {

    @Internal
    static <T> Observable<T> fromCallable(final Callable<T> callable) {
        return Observable.defer(new Func0<Observable<T>>() {

            @Override
            public Observable<T> call() {
                T result;
                try {
                    result = callable.call();
                } catch (Exception e) {
                    return Observable.error(e);
                }
                return Observable.just(result);
            }
        });
    }
}

四、总结

在分析完GreenDao的核心源码之后,我发现,GreenDao作为最好的数据库框架之一,是有一定道理的。首先,它通过使用自身的插件配套相应的freemarker模板生成所需的静态代码,避免了反射等消耗性能的操作。其次,它内部提供了实体数据的映射缓存机制,能够进一步加快查询速度。对于不同数据库对应的SQL语句,也使用了不同的DataBaseStatement实现类结合代理模式进行了封装,屏蔽了数据库操作等繁琐的细节。最后,它使用了sqlcipher提供了加密数据库的功能,在一定程度确保了安全性,同时,结合RxJava,我们便能更简洁地实现异步的数据库操作。GreenDao源码分析到这里就真的完结了,下一篇,笔者将会对RxJava的核心源码进行细致地讲解,以此能让大家对RxJava有一个更为深入的理解。

参考链接:

1、GreenDao V3.2.2源码

2、GreenDao源码分析

3、GreenDao源码分析

Contact Me

现如今,Android 行业人才已逐渐饱和化,但高级人才依旧很稀缺,我们经常遇到的情况是,100份简历里只有2、3个比较合适的候选人,大部分的人都是疲于业务,没有花时间来好好学习,或是完全不知道学什么来提高自己的技术。对于 Android 开发者来说,尽早建立起一个完整的 Android 知识框架,了解目前大厂高频出现的常考知识点,掌握面试技巧,是一件非常需要重视的事情。

去年,为了进入一线大厂去做更有挑战的事情,拿到更高的薪资,我提前准备了半年的时间,沉淀了一份 「两年磨一剑」 的体系化精品面试题,而后的半年,我都在不断地进行面试,总共面试了二三十家公司,每一场面试完之后,我都将对应的面试题和详细的答案进行了系统化的总结,并更新到了我的面试项目里,现在,在每一个模块之下,我都已经精心整理出了 超高频和高频的常考 知识点。

在我近一年的大厂实战面试复盘中逐渐对原本的内容进行了大幅度的优化,并且新增了很多新的内容。它可以说是一线互联网大厂的面试精华总结,同时后续还会包含如何写简历和面试技巧的内容,能够帮你省时省力地准备面试,大大降低找到一个好工作的难度。

这份面试项目不同于我 Github 上的 Awesome-Android-Interview 面试项目:github.com/JsonChao/Aw… 已经在 2 年前(2020年 10 月停止更新),内容稍显陈旧,里面也有不少点表述不严谨,总体含金量较低。而我今天要分享的这份面试题库,是我在这两年持续总结、细化、沉淀出来的体系化精品面试题,里面很多的核心题答案在面试的压力下,经过了反复的校正与升华,含金量极高。

在分享之前,有一点要注意的是,一定不要将资料泄露出去!细想一下就明白了:

1、如果暴露出去,拿到手的人比你更快掌握,更早进入大厂,拿到高薪,你进大厂的机会就会变小,毕竟现在好公司就那么多,一个萝卜一个坑。

2、两年前我公开分享的简陋版 Awesome-Android-Interview 面试题库现在还在被各个培训机构当做引流资料,加大了现在 Android 内卷。。

所以,这一点一定要切记。

现在,我已经在我的成长社群里修订好了 《体系化高频核心 Android 面试题库》 中的 ”计算机基础高频核心面试题“ 和 ”Java 和 kotlin 高频核心面试题“ 部分,后续还会为你带来我核心题库中的:

  • “Android基础 高频核心面试题”
  • “基础架构 高频核心面试题”
  • “跨平台 高频核心面试题”
  • “性能优化 高频核心面试题”
  • ”Framework 高频核心面试题“
  • ”NDK 高频核心面试题“

获取方法:点击此处查看

出身普通的人,如何真正改变命运?

这是我过去五、六年一直研究的命题。首先,是为自己研究,因为我是从小城镇出来的,通过持续不断地逆袭立足深圳。越是出身普通的人,就越需要有耐心,去进行系统性地全面提升,这方面,我有非常丰富的实践经验和方法论。因此,我开启了 “JsonChao” 的成长社群,希望和你一起完成系统性地蜕变。

星球目前有哪些服务?

  • 每周会提供一份让 个人增值,避免踩坑 的硬干货
  • 每日以文字或语音的形式分享我个人学习和实践中的 思考精华或复盘记录
  • 提供 每月 三 次成长、技术或面试指导的咨询服务。
  • 更多服务正在研发中...

超哥的知识星球适合谁?

  • 如果你希望持续提升自己,获得更高的薪资或是想加入大厂,那么超哥的知识星球会对你有很大的帮助。
  • 如果你既努力,又焦虑,特别适合加入超哥的知识星球,因为我经历过同样的阶段,而且最后找到了走出焦虑,靠近梦想的地方。
  • 如果你希望改变自己的生活状态,欢迎加入超哥的知识星球,和我一起每日迭代,持续精进。

星球如何定价?

365元每年

每天一元,给自己的成长持续加油💪

为了回馈 JsonChao 的 掘金 忠实用户,我申请了少量优惠券,先到者先得,错过再无。