2016我的番剧

v1.0 2nd Edition

不知是从何时开始,开始渐渐习惯去电影院看电影。虽说大部分在电影院看的电影都感觉有些许遗憾,但是2016年电影给了我很多的触动,说优秀的作品鼓励人,事实上确实是这样的。而动画业界也闹出了各种各样的事,业界药丸的言论几乎充斥在每一个新番讨论的地方。确实喜欢看的番越来越少,年末回顾起来绞尽脑汁也仅仅想起刚刚看过的几部。而电视剧方面倒是给了我们很多的惊喜,无论美剧、英剧还是日剧。

推特的一些媒体信息

v1.0 1st Edition

其实逛推仅仅是非常简单的理由,1、关注开发者最新消息,2、关注设计师的最新消息,3、画师有新图了,4、新番游戏相关的咨询网站有新的消息,5、声优又晒图了。而这其中收图算是一大日常了,通过各种交错复杂的关注关系又会发现新的画师,顺带很多又是原画师作画监督什么的,相当有趣。

去骑车吧

这一切的契机是十一月份觉得应该去锻炼一下,于是和朋友去凤凰山骑车。当然那时是当地租的山地车,而且车子不怎么样,但是真的非常开心。之后摩拜单车开始渐渐出现在街头巷尾。在摩拜推出lite版新版的时候,我在楼下拍了一张全家福分别是mobike lite v2,mobike lite,mobike。

就像我之前写的那样,也许有了这些方便的公路自行车,骑上之后也许会走的更远吧。比较直观的感觉就是有个口味非常好的家乡菜馆,要走十几分钟有时候懒得去,现在却有些期待(笑)。

前一段时间从滨海之窗回来的时候在创业广场看到大量的各个厂家的自行车,由于滨海大道全程堵车于是找了辆自行车骑回来。比较令我惊讶的是平时感觉这段非常长的路,竟然如此轻松加愉快的骑车就过去了。

之后正好遇到ofo单车免费活动,于是有了真正的开始。

环南山

如上图,在桂庙路口挑了辆大轮版的ofo单车。直接下行至东滨路,转到蛇口沃尔玛,直达蛇口客运码头。这里不得不说沿途自行车道非常好,但是全程修路各种推车。然后是蛇口港下面路段工厂开始增多沿途灰尘非常多而且有诡异的气味,也正是如此本打算下行至左炮台结果走错路莫名进入赤湾六道。

随后本打算走兴海大到但是莫名的走到小南山隧道,看到有人步行在走于是也骑了过去。但是这是封闭路段,怀疑是不是上了高速。也正是这时候当速度起来之后觉得这单车感觉要散架一样,长时间骑这种车果然还是不行。之后又转回了东滨路,这时候觉得非常不尽兴所以走到东滨路尽头的阿里大楼附近。这时候天色开始暗淡下来,经过深圳湾体育馆一个非常不起眼的小路进入深圳湾观光栈道。然后夜幕降临一路走到跨海大桥。

在海上栈道拍了这个照片(手机的极限在有月亮的时候就像灯泡一样…),虽然海风非常强劲但是实夜景真的非常美。和之前不同的是观光栈道开始不允许走自行车,但是在外围竟然多出一条自行车道,夜幕中非常的漂亮。

随后在夜幕中找不到如何回体育馆,还在栈道上转了半天。多次确认终于在阴影中找到了草地中的路,就没什么悬念走滨海大道经过桂庙路回到桂庙路口。

粗略算起来全程大致40公里,加上几次走错路,路上车辆和行人过多无法愉快的骑之外,从开始到结束全程大约4个小时。说实话全程也只有深圳湾骑的比较愉快,其它地方行人过多。另外就是时间有点晚,到深圳湾天都彻底黑了,在蛇口港向下路段之前计划的也没有走,而且在当地远远望去觉得有几处非常不错的地方,但是在赤湾路绕来绕去就错过了非常可惜,希望将来有机会再去吧。

伴随着《长骑》的疯狂延期,之后看了几话《小自行车》确实被燃到了。随后到住处附近的运动自行车专卖店了解一下公路车,经过一番介绍觉得和网络上说的差不多,但是我从一开始就有个忧虑,这种车可以在市区复杂路段骑吗,不会碰到个台阶就爆胎吧。不然公路车岂不是浪费了,但是小轮车和山地车骑在自行车道骑几个小时,想想也觉得比较痛苦吧。

于是告别老板去了传说中比较舒适的道路,环华侨城。

环华侨城

在大新地铁站找了辆车一路骑到深圳大学,穿过深大北门上深南大道,经沙河西路进入白石路。白石路这个路段行人非常多,但是过了天虹之后路面非常开阔并且有两侧都有非常不错的自行车道。

然后不经意的发现原来到了欢乐海岸北面,发现世界真是小,但是随便换一个角度却开阔了自己的视野。

随后北上经过侨城东进入深南大道,达到侨城东地铁口。

然后发现在继续走就回去了,于是折返回侨香路北上至北环大道,并一路向西走到南山大道返回桂庙路口。

这其中最为惊讶的是我回来看一下地图才发现侨城东地铁站原来在深南大道上,这个地铁口旁边正是深南大道。就像这个图那样,我到这里的时候真的非常吃惊,这沿路的风景。

此路段在白石路真的非常好,之后北环一段也可以,但是北环各种岔道阻断并且空气不好。最为关键的是全程车辆多,在公路上汽车感觉非常危险。而在绿道/自行车道上又经常遇到道路不好,被各种隔断的问题,依然不够尽兴。

最后

也许就如同很多朋友说的那样,重新爱上这个城市。在平时的活动范围之后走出两三条街,似乎就是完全陌生的环境一样,又有时甚至对着地图看了半天才恍然大悟。原来自己身边有这样的地方,滚滚车流之外原来还有这样美丽的风景。

去外面走走吧,说不定会发现意外的美好,重新发现和爱上这个城市。

从零开始的机器学习(前言)

最前

本文并不包括实际的算法说明,仅仅是这个系列的开端。

前言

前一段时间终于开始闲了下来,本来想到周边愉快的玩耍一番好好休息一下。但是朋友拜托做一个文本匹配和提取的工作,在小样本数据上初看都是些很规则的数据。但是在做模板测试的时候发现完全符合预期格式的占75%,基本符合的占24%,剩下的完全不匹配。之后就不停的修改模板和匹配规则想要适合所有格式,最后虽然结果是达到了,但是真的非常难看。然后才是真正的开始。

这其中比较严重的是本来非常严肃的文档,但是各种用措辞和句式却非常的随意。经过多次测试之后找了个最容易的实现–查表,但是随后却反而越来越在意,经过多次代码重构之后觉得用简单的文字和格式匹配几乎是无法实现的。于是我想起了之前写过的一些分类算法,然后结合一些自然语言处理工具处理那些格式混乱措辞随意甚至包括错字的文本。

十六年十月番扫番报告

v2.0 1st Edition

历来十月都是很强势的档期,很多话题作品都出自十月。

每当季节交替我都会觉得很没有精神,忙碌一天回家感觉昏昏欲睡,今年的十月更是严重。因此在十月番开始之初有大量的弃番,当时追的感觉只有两三部的样子,但是现在回过神来做统计的时候才发现不知不觉已经看了十几部。

第一阵营为一开始就比较关注并一直追下来的,《夏目》、《吹响吧,上低音号》、《少女编号》、《无畏魔女》。而这其中在精神最不好的时候实际上只追了《夏目》和《少女编号》,《无畏魔女》更多的是因为情怀了。而现在仔细想一下觉得十月档期阵容真的是非常强大,几乎是照顾到方方面面。那么接下来从一般大众番组开始介绍我心中绚丽的十月新番。

Retrofit 适配器与转换器

v1.0 1st Edition

本文主要说明Retrofit中关于retrofit2.Converterretrofit2.CallAdapter的相关内容,不可避免的会牵扯到一部分okhttp3RxJava的内容。

在使用Retrofit的时候首先会对其使用动态代理的方式做http请求的方式非常喜欢,这里个人觉得一是因为用接口做模板非常简洁清晰,其次也正是因为这种原因隐藏了http请求的繁琐步骤,同时也减少了不必要的出错。

其次对于默认的数据转换感觉非常神奇,仅仅是声明一个泛型就可以把http返回转换成对象。并且其拥有非常好的扩展性,特别是对RxJava的支持在第一次用的时候令人印象深刻。

那么接下来开始读代码,如果不关心okhttp的实现那么看Retrofit的源代码,会觉得非常的简洁并且文档注释写的非常详细。

Android Retrofit

v1.0 2nd Edition

自从Android将SDK中的HttpClient去除之后,我开始找替代的HTTP请求类库,推荐的也比较多,其中我对于okhttp比较感兴趣。当时也没有深究只是把它当做Httpclient替代品来用,后来看到越来越多的地方开始谈论Retrofit。渐渐的开始对其有一定的了解并尝试使用,在我开始使用的时候其版本已经到了Retrofit2,它也变得更加强大和易用。

Retrofit2给我最直观的感觉是漂亮。

似乎是在我刚开始学程序设计的时候我看到一句话,Java代码一定要优雅。之前看到Httpclient在做请求时写的乱糟糟的代码,我曾经做过多次的重构,而每次重构之后虽然解决了问题但是依旧谈不上优雅。之后看到Retrofit用代理的方式做请求,真的是非常喜欢。

Android MVP模式

v2.0 2nd Edition

这是一个关于一些新的编程思想和框架的使用的一系列文章,目标是融合目前比较热的MVP模式、RxJava框架和Retrofit库写出一个简单的例子,也算是一个总结吧。

前言

Android MVP模式很早就听过,也大概了解了一下。当时觉得从理论上来说这个思想确实是很好的,但是由于没有实际使用经验,这个东西给人的第一感觉是不知道从项目组织上要怎么做。很多的例子过于简单把项目结构按照MVP分目录,之后也看过一些关于如何在实际项目中使用MVP的文章。这里有几种关于MVP的使用观点。

  1. 基本的模式,按照MVP的思想整整齐齐的将各个类接口放在不同的目录里。这种方式做过Java后台网站的朋友也许会比较熟悉,在Android端其实我是比较反感这类做法的。一切服务于业务,而业务体现在功能上。目前我比较赞同的是按照功能构建项目。
  2. 顶级功能模块分类,下级MVP分类。据说某些大厂的大型项目是这种方式。关于MVP说的最多的是什么?至少我看到的是P层的功能切换,这实际上也是面向接口的好处与便捷之处。
  3. 融合的方式。MVP终究只是一个编程思想,作为一个思想,有必要把之前的项目逻辑全部推倒重构吗?我觉得这完全是多此一举。

深圳新海诚展

无意中听说在深圳中心书城有新海诚展,于是赶在最后一天去看了一下。

说起新海诚会想起什么呢?秒速五厘米?言叶之庭?我想大部分朋友会想起秒速五厘米吧,毕竟当时给人的冲击太过强烈,无论是作画还是剧情。其实这么多年过去了,秒速五厘米无论多么浓烈的感情还是会淡去的,留给我的更多的是一个意象。飘落的樱花、积水的倒影、叮叮当当的火车道口。

近期给我感触最深的其实还是新版的《她与她的猫》(观看地址)(我推荐别人的时候会戏称“香菜与猫”),而上一部作品(言叶之庭)的女主角CV也是花泽香菜,所以有时候我会想这是不是有某种含义。其实新番《她与她的猫》和新海诚关系不大,更像是一个挂名而已,剧情上为原作的前传,在最后结尾的时候回到了原作的时间点。

说实话写到这里非常难以下笔,我又回去把秒速五厘米三篇看了下。已经很久没有看这么“文艺向”的作品了,不知从何时起开始不敢看这类的作品,所有的思考方式都使用冷冰冰的逻辑推理。而表现出的最直观现象就是比较喜欢有因有果的推导式的思考,以前收藏的各类小说已经完全不想看了,即便是为了查资料也完全看不下去了。然而相关的技术文档却依然看的津津有味,所以事情变的糟糕起来,又要像学生时代那样“多愁善感”起来。

《秒速五厘米》说实话作画上确实密度惊人,但是故事其实非常一般,对故事的解读更是仁者见仁,智者见智。之所以产生如此大的反响,是因为这种事情很容易引起观众的共鸣,即便与故事中的经历不同也会或多或少的造成代入感。之前我的一个“运动系”同学有一次意外的聊到《秒速五厘米》的时候,他一反常态的唏嘘不止,一遍一遍的向我诉说。这个动画简直就是在说他自己,里面的情景和他的真实经历一模一样。

而对于我来说,是因为喜欢它的“静”。

v1.0 1st Edition

前言

Android ContentProvider属于Android框架中的几个核心组件,相信Android的一些入门书籍都会提起。ContentProvider的主要用途我认为是对一些结构化数据进行管理,而对于数据管理的实现也有非常多,因此在实际应用中要自行取舍。本文主要从以下几个方面对ContentProvider进行讲解,如果遇到TL;DR可直接跳转。

  1. ContentProvider概念
  2. ContentProvider使用
  3. 各种问题
  4. Loader/AsyncQuery
  5. Observer

一、ContentProvider概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class ContentProvider{

public boolean onCreate();

public String getType(Uri uri);

public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1);

public Uri insert(Uri uri, ContentValues contentValues);

public int delete(Uri uri, String s, String[] strings);

public int update(Uri uri, ContentValues contentValues, String s, String[] strings);
}

如果要实现自己的ContentProvider的话,需要继承android.content.ContentProvider抽象类,继承之后必须覆盖的抽象方法如上面的代码。这里的几个方法作用如下:

  • ContentProvider#onCreate():boolean:构造方法,由框架调用运行在UI线程中,因此不能做一些耗时操作,一般用作属性初始化(比如数据库初始化)。
  • ContentProvider#getType(Uri uri):String:返回的是Uri对对应的资源的MIME类型。
  • ContentProvider#query():Cursor:使用条件查询,最基本的也是最常用的方法,其返回一个数据库游标。
  • ContentProvider#insert():Url:插入一条数据,返回插入数据资源定位符。
  • ContentProvider#delete():int:使用给定的条件删除数据库信息,返回受影响的记录数。
  • ContentProvider#update():int:使用给定的条件更新数据库信息,返回受影响的记录数.

之后需要在AndroidManifest.xml中进行声明。如下:

1
2
3
4
5
<provider
android:authorities="com.suwish.wiki.provider"
android:name="com.suwish.provider.CoreProvider"
android:exported="true"
android:label="Core Provider"/>

这里需要特别注意两个地方,authorities的值为Provider对外暴露的Uri地址,其必须保证全系统为唯一性。即无论你应用内部还是整个Android系统中都不能出现两个authorities值相同的Provider声明,关于这个值的命名有什么标准我也没注意过,既然是全局唯一那么自然和包名有相似的规则吧。而这里的name属性则是真正的Provider类的位置,当然了authorities和name可以一致,但是问题是这个Uri是完全暴露出去的如果包名比较长不好看写起来麻烦是一回事,总觉得是比较敏感的东西被暴露了。所以这里我觉得还是使用功能和逻辑命名比较好,如果你只有一个功能单一的Provider那我觉得直接按照包名就好了。

// TODO multiprocess

关于数据存储的选择,应用内数据存储对于简单的数据格式来说SharedPreferences无疑是最好的选择。但是其存在一个明显的问题无法跨进程使用,当单个应用使用多个进程实例,此时虽然可以保证Application对象的唯一性,但是其会进行多次初始化。即便全局唯一实例的SharedPreferences对多次实例化做了判断,但是其方法调用结果依旧会在不同进程间存在数据混乱。

二、ContentProvider使用

使用离不开场景,这里假设要实现一个类似Wiki功能的应用。基本功能就是向外提供数据查询,之前也说过对于结构化数据SQLite的存储是非常方便的,而结合了ContentProvider之后又实现了非常好的数据获取的解耦。相比直接使用SQLiteDatabase或外面在封装一层来说不知道高到哪里去了,当然了大部分场景中ContentProvider都是基于SQLite的,所以这又回到了一个本质的问题 — 数据库设计。

对于数据库设计个人觉得移动设备完全没有必要走和服务器一样的结构,特别是字段命名。很多手机数据库设计都是基于服务端API的返回格式来设计的,这里有一个我非常不能忍的问题就是字段。经过长时间迭代API经过频繁的变动之后,返回字段名很可能基本就不是英文字母了,各种不看文档觉得莫名其妙的名称,各种拼写错误在IDE中一个一个警告,每次看到都觉得莫名的烦躁。很多时候你还要各种get和set这种字段,但是看命名完全不知所以然和你本地逻辑没有一个良好的对应。所以都去见鬼吧,自己设计吧(这段就当没看到,划掉好了)。

数据库设计

这是一个卡牌游戏的卡面WIKI,为了简化期间这里设计一个卡牌的数据表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class CardInfo {

public static final String TABLE_NAME = "card_info";

public static final Uri CARD_CONTENT_URI = Uri.parse("content://" +
CoreProvider.AUTHORITY + "/" + TABLE_NAME);

private int id;
private int cardId;
private String name;
private int characterId;
private String attribute;
private String explain;

public static final String TABLE_CREATE = "create table " + TABLE_NAME + " (" +
Impl._ID + " integer primary key autoincrement , " +
Impl.COLUMS_CARD_ID + " integer ," +
Impl.COLUMN_NAME + " text, " +
Impl.COLUMN_CHARACTER_ID + " integer, " +
Impl.COLUMN_ATTRIBUTE + " text, " +
Impl.COLUMN_EXPLAIN + " text )";

public final static String[] COLUMNS = new String[]{
Impl._ID,
Impl.COLUMS_CARD_ID,
Impl.COLUMN_NAME,
Impl.COLUMN_CHARACTER_ID,
Impl.COLUMN_ATTRIBUTE,
Impl.COLUMN_EXPLAIN
};


public static class Impl implements BaseColumns{

public static final String COLUMN_CARD_ID = "_card_id";
public static final String COLUMN_NAME = "_name";
public static final String COLUMN_CHARACTER_ID = "_character_id";
public static final String COLUMN_ATTRIBUTE = "_attribute";
public static final String COLUMN_EXPLAIN = "_explain";

}

...
}

首先这是一个CardInfo类是一个简单的卡牌实体类,如果对数据库有稍许了解的话可以简单的想象一个对象为一条数据表中的记录。此对象的几个私有变量为卡牌的逻辑属性,其中定义的Impl类则为实际的数据表字段名。Impl继承BaseColumns的原因是需要获得其接口中的_ID属性,其另外的_COUNT字段这里没有用到。

此外CardInfo这里只是简单的几个属性用于和数据表中的字段进行对应,并且实际上这个类合并了其他的几个功能。比如本质上CardInfo是一个逻辑相关的类,如果真的用起来的话尽量把数据表定义内容分离出去,此外可能还有一些工具方法,如游标和对象/ContentValues之间的转换等。这都需要根据实际的逻辑、数据库和易用性等等进行综合考量决定。当然了这样做也只是适合在本应用中使用,如果向外提供数据接口那么就另说了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class DatabaseHelper extends SQLiteOpenHelper{

public static final String DATABASE_NAME = "cg_ss.db";

public static final int DATABASE_VERSION = 1;

public DatabaseHelper(Context context){
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CardInfo.TABLE_CREATE);
}

@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

}
}

接下里是数据库具体的操作类,主要作用看其中的几个方法就一目了然了。其构造方法进行数据库初始化设置,比如名字,版本号等。接下来如果指定的数据库不存在则调用onCreate方法,在此方法中对数据表进行初始化操作,如建表和插入初始化数据。如果数据库已存在而版本号不同这会调用onUpGrade方法对数据库进行升级,这里一般会对表结构和旧数据进行清理等(当然了直接删除旧表也不是不可以…)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CoreProvider extends ContentProvider{

public static final String AUTHORITY = "com.suwish.wiki.provider";

private DatabaseHelper databaseHelper;

private static UriMatcher uriMatcher;

static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, CardInfo.TABLE_NAME, 1);
}

@Override
public boolean onCreate() {
databaseHelper = new DatabaseHelper(getContext());
return true;
}

.....

Provider如果完善起来实际上是相当的繁琐的,毕竟要面对各种数据库操作请求和兼容Uri规范。所以这里进行逐一说明,就目前我的感觉来说使用Provider对数据库进行操作有两种思路,一种是直接使用SQL的操作思路,这点也是最直观最有效的,另一种就是使用android的Uri思路了。接下来分别使用SQL和Uri两种思维方式进行描述。

首先进行数据初始化:

insert into card_info (_card_id, _name, _character_id, _attribute, _explain) values (100243, "Futaba Anzu", 134, "cute", "每 11 秒,有 40 ~ 60% 的几率使所有PERFECT音符恢复你 3 点生命,持续 4 ~ 6 秒。")

insert into card_info (_card_id, _name, _character_id, _attribute, _explain) values (200165, "Takagaki Kaede", 197, "cool", "每 11 秒,有 40 ~ 60% 的几率获得额外的 15% 的COMBO加成,持续 5 ~ 7.5 秒。")

insert into card_info (_card_id, _name, _character_id, _attribute, _explain) values (300125, "Jougasaki Mika", 267, "passion", "每 9 秒,有 40 ~ 60% 的几率使所有PERFECT/GREAT音符获得 17% 的分数加成,持续 4 ~ 6 秒。")

当然以上语句可以写在DatabaseHelper#onCreate方法中进行初始化。

使用SQL操作数据库

2.1 查询操作

标准的SQL简单来说就是对表中的字段进行操作,所以从这点上来说完全要匹配ContentProvider给出的几个方法,尽可能的使用拼装SQL的思路。

query(uri:Uri, projection:String[], selection:String, selectionArgs:String[], sortOrder:String ):Cursor:首先是查询方法,它有五个参数之多。但是对于了解SQL语句的朋来说,其实这是不够的,即便和SQLiteDatabase的查询方法相比也是不足的。这五个参数的意义分别为:

uri:Uri:Uri地址,这个东西实际是非常灵活的,同时也是唯一一个可以灵活处理的参数。
projection:String[]:列名数组,实际为查询结构需要返回的哪些列。
selection:String:选择列表,拿SQL来说它就是where后面的条件部分。
selectionArgs:String[]:参数列表,实际上是selection的预编译部分,如果直接全部写在selection中,则这部分可以省略。
sortOrder:String:结果集排序。

使用SQL语句的思路就是传过来表名、结果集列名、插叙条件和结果集排序条件,这是最基本的SQL查询语句,所以一些像什么多表连接查询在ContentProvider中就别想了,但是话又说回来如果你用对Provider的控制权那么又有什么办不到呢。你只需要把足够的条件想办法传进来,然后SQLiteDatabase拥有足够强大的功能去实现你想要的查询。

现在我需要查询_attribute(属性/阵营)为passion的卡,那么SQL语句直观来说可以这么写select * from card_info where _attribute = 'cute'(为了看起来美观我就不加分号了)结果如图。使用Provider的通用接口的话就感觉比较繁琐了。

1
2
3
4
5
6
7
8
ContentResolver resolver = getContentResolver();
String selection = CardInfo.Impl.COLUMN_ATTRIBUTE + " = '?' ";
String[] args = new String[]{"passion"};
Cursor cursor = resolver.query(CardInfo.CARD_CONTENT_URI, CardInfo.COLUMNS, selection, args, null);
while (cursor != null && cursor.moveToFirst()){
int cardId = cursor.getInt(cursor.getColumnIndexOrThrow(CardInfo.Impl.COLUMN_CARD_ID));
....
}

这里直接取游标的话写起来非常的繁琐,那么在这里把这一串写成一个工具方法。并且还有游标的开闭回收问题,这里就一起做了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class ProviderHelper {

private ProviderHelper(){}

public static int getInt(Cursor cursor, String column){
return cursor.getInt(cursor.getColumnIndexOrThrow(column));
}

public static String getString(Cursor cursor, String column){
return cursor.getString(cursor.getColumnIndexOrThrow(column));
}

public static void close(Cursor cursor){
if (cursor == null ) return;
try {
cursor.close();
}catch (Exception ignored){}
}
...
}

这里只有整型和字符串,简化期间就只写了两个get方法。另外提供一个close方法关闭游标,当然也可以实现其他的close方法用于关闭输入输出流等,这都是非常常用的功能。另外使用ignoredException变量名,可以在IDEA家族中规避掉控catch带来的警告,每次写完都觉得心情格外舒畅。那么使用工具方法之后上面的这个流程重写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ContentResolver resolver = context.getContentResolver();
String selection = CardInfo.Impl.COLUMN_ATTRIBUTE + " = '?'";
String[] args = new String[]{"passion"};
Cursor cursor = null;
try{
cursor = resolver.query(CardInfo.CARD_CONTENT_URI, CardInfo.COLUMNS, selection, args, null);
while (cursor != null && cursor.moveToNext()){
int cardId = ProviderHelper.getInt(cursor, CardInfo.Impl.COLUMN_CARD_ID);
String name = ProviderHelper.getString(cursor, CardInfo.Impl.COLUMN_NAME);
int characterId = ProviderHelper.getInt(cursor, CardInfo.Impl.COLUMN_CHARACTER_ID);
String attribute = ProviderHelper.getString(cursor, CardInfo.Impl.COLUMN_ATTRIBUTE);
String explain = ProviderHelper.getString(cursor, CardInfo.Impl.COLUMN_EXPLAIN);
}
}catch (Exception ex){
// TODO log
}finally {
ProviderHelper.close(cursor);
}

注意游标操作时候的异常可不要忽略掉,要保留下日志以便分析问题。当然以上读游标的内容如果依旧觉得非常繁琐的话,完全可以再写一个工具方法用于游标到对象的转换,比如写一个CardInfo#fromCursor(cursor:Cursor)方法,然后把结果存入一个列表返回。到这里就可以很清楚的看到这种写反本质就是select ... where ... order结构,而参数列表和排序是可以忽略不计,但是就我而言args参数用起来远比直接拼凑字符串来的好,但是这使用预编译参数替换问号占位符的方法实际和Java中的JDBC还是有一定的差距的,这在下面的问题中说明。

2.2 修改

insert(uri:Uri, values:ContentValues):Uriupdate(uri:Uri, values:ContentValues, selection:String, selectionArgs:String[]):intdelete(uri:Uri, selection:String, selectionArgs:String[]):int。所谓增删改查CUDI,虽说查询是使用频率最高的操作,单修改的重要性依旧不言而喻。从某种程度上说updatedelete操作存在一定的相似度,比如都有限制条件,当然这些条件依旧是可以忽略的。

insertupdate又同时包含了android.content.ContentValues,而ContentValues内部实现目前到API 23(Android 6.0)为止都为HashMap<String, Object>,所以它本质就是一个Key-value格式的数据集合,key为列名value为列的值。这几个方法都是Uri提供表名ContentValues提供数据。

对于新增操作,拿最开始初始化数据库时的数据来说,可以写成以下的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Insert 新增记录
ContentValues values = new ContentValues();
values.put(CardInfo.Impl.COLUMN_CARD_ID, 100243);
values.put(CardInfo.Impl.COLUMN_NAME, "Futaba Anzu");
values.put(CardInfo.Impl.COLUMN_CHARACTER_ID, 134);
values.put(CardInfo.Impl.COLUMN_ATTRIBUTE, "cute");
values.put(CardInfo.Impl.COLUMN_EXPLAIN, "每 11 秒,有 40 ~ 60% 的几率使所有PERFECT音符恢复你 3 点生命,持续 4 ~ 6 秒。");

ContentResolver resolver = getContentResolver();
Uri uri = resolver.insert(CardInfo.CARD_CONTENT_URI, values);

//update 更新记录
values.clear();
values.put(CardInfo.Impl.COLUMN_EXPLAIN, "11秒毎、高確率で少しの間、PERFECTでライフ3回復");
String selection = CardInfo.Impl.COLUMN_CARD_ID + " = '?' ";
int rowCount = resolver.update(CardInfo.CARD_CONTENT_URI, values, selection, new String[]{String.valueOf(100243)});

//delete删除记录
selection = CardInfo.Impl.COLUMN_CARD_ID + " = '?' ";
rowCount = resolver.delete(CardInfo.CARD_CONTENT_URI, selection, new String[]{String.valueOf(100243)});

这里和直接使用SQL语句是一样的,不要使用CardInfo#Impl#_ID字段,因为这个字段在建表的时候已经声明过为自动增长ID,如果实际使用用无法确定是不是存在ID字段,那么最好在插入前删除一次,从而避免不必要的错误。

update/delete没有什么多说的,其存在一个where参数和查询一个思路。

使用Uri操作数据库

// TODO 补充

使用Uri的方式操作数据库我用的也比较少,毕竟如果自己写一个Provider要做这些确实非常繁琐,而且感觉不够灵活。但是话又说回来,如果这样做了之后用起来真是神清气爽,无论是视觉上还是调用起来真是一种享受(滑稽)。

这简要的介绍一下Android中的Uri。android的Uri就是一个资源定位符(Universal Resource Identifier),如果和URL做比较的话其最大的特色是“scheme”,说实话我第一次看到各种Uri前缀的时候真的一时间没反应过来,个人觉得把这部分看做URL中的协议部分后就比较直观了。

Uri的结构为:scheme://host:port/path的结构,但是不同的scheme其后的标示符也不同。比如文件为file:///、电话tel:xx、定位gps:xx等。同时host加上port部分也叫做authority,就是ContentProvider中的authority。

说到这里其实上一部分虽然给出一个CUDI的例子,但是其对应的ContentProider并没有实现细节。接下来在首先实现上一部分内容的功能后并提供有限的Uri操作支持,更多的支持可能在将来进行完善和补充(大概…)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public class CoreProvider extends ContentProvider{

...

private static final int CODE_CARD = 1;
private static final int CODE_CARD_ID = 2;
private static final int CODE_CHARACTER = 3;

static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, CardInfo.TABLE_NAME, CODE_CARD);
uriMatcher.addURI(AUTHORITY, CardInfo.TABLE_NAME + "/#", CODE_CARD_ID);
uriMatcher.addURI(AUTHORITY, CharacterInfo.TABLE_NAME, CODE_CHARACTER);
}
...

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String tableName = null;
int code = uriMatcher.match(uri);
switch (code){
case CODE_CARD:
tableName = CardInfo.TABLE_NAME;
break;
case CODE_CARD_ID:
tableName = CardInfo.TABLE_NAME;
break;
case CODE_CHARACTER:
tableName = CharacterInfo.TABLE_NAME;
break;
}
if (code == UriMatcher.NO_MATCH || TextUtils.isEmpty(tableName)) return null;
SQLiteDatabase database = databaseHelper.getReadableDatabase();
if (code == CODE_CARD_ID){
long id = ContentUris.parseId(uri);
selection = CardInfo.Impl._ID + " = ? ";
selectionArgs = new String[]{String.valueOf(id)};
}
return database.query(tableName, projection, selection, selectionArgs, null, null, sortOrder);
}

public Uri insert(Uri uri, ContentValues values) {
String tableName = null;
int code = uriMatcher.match(uri);
switch (code){
case CODE_CARD:
tableName = CardInfo.TABLE_NAME;
break;
case CODE_CHARACTER:
tableName = CardInfo.TABLE_NAME;
break;
}
if (code == UriMatcher.NO_MATCH || TextUtils.isEmpty(tableName)) return null;
SQLiteDatabase database = databaseHelper.getWritableDatabase();
long rowId = database.insert(tableName, null, values);
if (rowId <= 0) return null;
Uri result = ContentUris.withAppendedId(uri, rowId);
notifyChange(result);
return result;
}

public int delete(Uri uri, String selection, String[] selectionArgs) {
String tableName = null;
int code = uriMatcher.match(uri);
switch (code){
case CODE_CARD:
tableName = CardInfo.TABLE_NAME;
break;
case CODE_CHARACTER:
tableName = CharacterInfo.TABLE_NAME;
break;
case CODE_CARD_ID:
tableName = CardInfo.TABLE_NAME;
break;
}
if (code == UriMatcher.NO_MATCH || TextUtils.isEmpty(tableName)) return 0;
SQLiteDatabase database = databaseHelper.getWritableDatabase();
if (code == CODE_CARD_ID){
selection = CardInfo.Impl._ID + " = ? ";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
}
int rowCount = database.delete(tableName, selection, selectionArgs);//受影响的行数
if (rowCount > 0){
notifyChange(uri);
}
return rowCount;
}

public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
String tableName = null;
int code = uriMatcher.match(uri);
switch (code){
case CODE_CARD:
tableName = CardInfo.TABLE_NAME;
break;
case CODE_CHARACTER:
tableName = CharacterInfo.TABLE_NAME;
break;
case CODE_CARD_ID:
tableName = CharacterInfo.TABLE_NAME;
break;
}
if (code == UriMatcher.NO_MATCH || TextUtils.isEmpty(tableName)) return 0;
SQLiteDatabase database = databaseHelper.getWritableDatabase();
if (code == CODE_CARD_ID){
selection = CardInfo.Impl._ID + " = ? ";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
}
int rowCount = database.update(tableName, values, selection, selectionArgs);//受影响的行数
if (rowCount > 0){
notifyChange(uri);
}
return rowCount;
}
}

这里使用UriMatcher定义一些可接受的操作,首先是完全开放状态的CODE_CARD,其接受结尾为CardInfo.TABLE_NAME的所有Uri。何其相对的是CODE_CARD_ID,它接受CardInfo.TABLE_NAME后追加一个路径的Uri,由于这只接受追加的路径为ID属性,所以传入其他内容无法保障获取正确的结果。另外这里只使用一套操作码来用于CUDI操作,

query():Cursor

首先为了避免一些特殊情况可能造成的误解,这里增加一张角色信息表CharacterInfo,这个类中占有表名和Uri属性只是为了说明存在另一张表的情况,不做任何相关操作。

在使用UriMatcher获取操作码之后再进行下一步操作,这里为了简化操作在switch语句中获取相关操作对应的表明。这里分为两种操作情况,直接SQL语句的时候只需要获得表名然后将所有剩余参数全部传入database.query():Cursor方法(实际使用中可能还要防止SQL注入等)。另外就是直接使用Uri的方式,这种方式要考虑的比较多这里目前先给出一个最为简单情况,就是指提供ID查询。使用ContentUris.parseId(uri:Uri):long方法截取ID字段,随后将获得ID字段拼凑出selection和预编译的内容再次传入database.query():Cursor方法。

//TODO 更加通用的Uri操作

insert update datele

这里没有什么多说的,都是要么截取表名要么截取ID(此时的表名其实是由操作码判断出来的)。由于最终的数据库操作都是使用SQLiteDatabase来完成,所以任何形式本质上都是凑齐SQLiteDatabase几个相关方法的参数而已。任何外部的参数传递方式也只是手段不同,在ContentProvider中的使用都没有什么本质的区别。对于insert来说其返回一个Uri,这个Uri的返回是非常有必要的,不要为了觉得麻烦直接返回一个null,并且以为只要不抛出异常那必定是成功的,所以返回和不返回意义大部的(这样想其实是非常错误的)。在SQLiteDatabase#insert(...):long方法执行成功后会返回一个行号,此行号默认是一个从数字1开始步长为1的自增字段。同时默认情况下数据表设置的自增ID也是从数字1开始以1为步长自增,所以即便原表格旧记录被删除行号和ID依旧保持同样的增长,因此insert方法返回的ID我们可以等同于自己设置的数据表自增ID(这里就是Card_info#Impl#_ID)。于是返回的Uri这里追加插入成功的ID,即ContentUris.withAppendedId(uri, rowId),并向外发送新增数据成功的通知。这种通知可以为数据驱动类逻辑提供可靠保障,关于此类通知的详细说明参见下文的通知部分。

// TODO 批量

2.3 Notification,回调通知

1
2
3
4
5
6
7
private void notifyChange(Uri uri){
Context context = getContext();
if (context == null) return;
ContentResolver resolver = context.getContentResolver();
if (resolver == null) return;
resolver.notifyChange(uri, null);
}

(真是惊了,现在各种@Nullable各种报可能出现空指针警告)

现在我们增加一条回调通知供相应的观察值使用。如上文的方法本质是调用ContentResolver#notifyChange(uri:Uri,observer:ContentObserver),而观察者的注册是通过ContentResolver#registerContentObserver(uri:Uri,notifyForDescendents:boolean,observer:ContentObserver)

通知一方可以采取全表通知的形式如CardInfo#CARD_CONTENT_URI,也可采取记录式的如ContentUris.withAppendedId(uri, rowId)。第一种我们在收到通知后仅仅知道表格数据发生了变化,但是如果需要获得变化的数据还需要做更多的操作。而第二种方法增加了ID属性,我们在获得通知的时候就可以知道是哪条记录发生了变化ContentUris.parseId(uri)。这时候我们甚至可以直接使用此Uri调用查询方法获得变化记录的详细情况,这对于UI界面来说相当于局部刷新。

三、问题

  1. rawSQL
  2. 预编译

四、Loader/AsyncQuery

在使用ContentProvider时由于我们无法得知数据的实际来源和数据操作的复杂度,如可能是读磁盘甚至是读网络,亦或有比较耗时的计算,所以我们无法保障数据返回的时间。因此就有必要使用异步查询来将耗时操作切换至后台线程,并在执行完成后切回到UI线程。对于直接使用Thread/Runnable - Handler这种情况实际上考虑的事情非常多,最关键的是写出来不优雅,用起来难受。

这里介绍两种使用ContentProvider放方式,分别是Loader/LoaderManager和AsyncQuery。

4.1 Loader

LoaderLoaderManager我记得在第一次看的时候还是比较吃惊的,最主要的是生命周期管理和字段的线程切换。但是后来我越来越觉得这个东西越来越不顺眼,这点有一下几个原因:1、API设计的不是很直接明了,文档写的也不怎么样第一次用总觉得比较莫名。2、繁琐,感觉要写一堆东西。但是话又说回来了,好像都比较繁琐的样子… 3、感觉有点重…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class LoaderActivity extands AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>{

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportLoaderManager().initLoader(0, null, this);
}

public Loader<Cursor> onCreateLoader(int id, Bundle args){
String selection = CardInfo.Impl.COLUMN_ATTRIBUTE + " = '?'";
String[] selectionArgs = new String[]{"passion"};
CursorLoader loader = new CursorLoader(this, CardInfo.CARD_CONTENT_URI, CardInfo.COLUMNS, selection, selectionArgs, null);
return loader;
}
public void onLoadFinished(Loader<Cursor> loader, D data){}
public void onLoaderReset(Loader<Cursor> loader){}

}

如果继承AppCompatActivity那么LoaderCallbacks就要使用support v4中的版本否则直接使用即可,getSupportLoaderManager()同理。这里我想说的是LoaderCallbacks是一个借口,我有时候是非常不写换借口的。因为借口设计之初的目的就是多继承,正因为如此多继承经常被滥用。经常觉得借口好呀,不用强制再写个类,这样导致了想Activity这样的类在不知不觉中implements关键字后面跟了一大串借口。进一步演变之后就变成了各种放些堆积在Activity中,越来越烦躁。对于ContentProvider来说直接在onCreateLoader构造一个CursorLoader即可,用法和ContentResolver非常相似,这里也没什么多说的。

4.2 AsyncQuery

AsyncQuery即AsyncQueryHandler,需要注意的是其本身是继承android.os.Handler的。其源码相对来说是比较少的,其内部使用android.os.HandlerThread来构建的线程并创建一个WorkerHandler用来异步执行数据操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LoaderHandler extends AsyncQueryHandler{

public LoaderHandler(ContentResolver cr) {
super(cr);
}
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {}

protected void onInsertComplete(int token, Object cookie, Uri uri) {}

protected void onUpdateComplete(int token, Object cookie, int result) {}

protected void onDeleteComplete(int token, Object cookie, int result) {}
}

调用的话它提供了startQuery()startInsertstartUpdatestartDelete几个入口方法,返回分别在对应的xxComplete方法中。而对于同类操作方法参数中的token用来做区分,而cookie则作为一个附加对象跟随整个操作过程。

我想说的是其提供的几个方法条理非常的清晰一目了然,即便你第一次用,在IDE里面代码提示点出来方法或者展开Override/Implement也能大概对这个类的用法猜出来个123,再点开它的源代码大致浏览一下,妥妥的。

Loader的时候说的一样,接口在和UI在一起用的时候很容易将代码逻辑过多的丢到UI中去,也许这是一个不自觉地的过程,但是当意识到UI中逻辑太混乱的时候又有多少人愿意仔细梳理一遍,各种重构呢?就比如各种xxListenerxxCallback等等,如果这个接口是自己设计的话那可能问题就更多了,所以从这方面来说我相对于接口我更倾向于抽象类的原因,因为Java的单继承特性,对于一个抽象类在使用之处你就会去考虑他的实现怎么做才更好。