Xiaolong的个人博客

每一天都值得被认真对待


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 爱过的人

Android端的彩票开奖查询系统

发表于 2017-09-30 | 分类于 个人开源

实现如下

假装插入了图片
初版历时半个多月

基础功能

  1. 开奖结果查询

    • 近期开奖查询
    • 历史开奖查询(最多五十期)
  2. 关注彩种

  3. 一些简单的趋势分析
  4. 号码预测(号码预测做的比较简单,直接算出每个号码的多期平均值,和期望平均值做对比。取均值。理论上应该是范围内的都是概率发生的,这一块其实可以加入奇偶频率,号码频率,和一些其他的条件来做预测,后面会继续做优化)
  5. 接口原因,能用到的接口只有四个。 自己编写了规则文档。还有一些其他必要的功能。
  6. 最后面想说的是这里其实更需要一个后台来爬取数据做动态更新。(打算写一个后台和爬虫来搞)

设计意图

设计意图来自身边的朋友,对这一块投注存在的需求。

随机组合出所有可能再做筛选,然后投注

起初有个朋友让我帮忙写一个体育彩票36选7三个数字固定,其余数字随机的可能结果,然后动了一下脑子想了一下,
这些数据有
(33 X 32 X 31 X 30)/(4 X 3 X 2 X 1) = 40920种,
如果整组号码随机组合的结果有
(36 X 35 X 34 X 32 X 31 X 30)/(7 X 6 X 5 X 4 X 3 X 2 X 1) = 8347680种
由此可见其中大奖概率是很低的

特等奖(7) 中奖概率为 概率公式打不出来((C7 7) / (C36 7)) = 1/8347680 (全复式共1注)

一等奖(6+1) (((C8 7) *(C28 0)-(C7 7))/(C36 7)) = 1/1192526(全复式共7注)

二等奖(6) 1/42590(全复式共中196注)

三等奖(5+1) 1/14197(全复式共中588注)

四等奖(5) 1/1052(全复式共中7938注)

五等奖(4+1) 1/631(全复式共中13230注)

六等奖(4) 1/73(全复式共中114660注)

无奖(3+1) 1/73(全复式共中114660注)

无奖(3) 1/12(全复式共中716625注)

无奖(2+1) 1/19(全复式共中429975注)

无奖(2) 1/4(全复式共中2063880注)

无奖(1+1) 1/12(全复式共中687960注)

无奖(1) 1/3(全复式共中2637180注)

无奖(0+1) 1/22(全复式共中376740注)

下面给出这类彩票的中奖概率计算公式

n个全是正选号码:image

n个正选加一个特别号码:image - image

想起了我大学的时候的概率统计,学概率统计的时候老师就说过了,赌博(指合法的彩票这类的),还有保险这个行业,基本上都能从概率学来计算,而且如果仔细算一下这里面的概率,就能发现这些都是非常暴利的行业。好吧,没研究过体彩的盈利手段,可能都是从税上面征的。而保险其实如果有经过大规模的数据分析的话,是可以知道赔保的概率很低,所以基本上买保险的人数乘以保险额会远远大于预估出来的全部需要出险的赔偿额的,剩下的都是利润利润。
如果从心理学上面来讲的话,买保险大家都是图个心安,买彩票则是赌。不禁感叹人类果然智慧无限,对这两类人群都做了细致的划分和业务推销..扯远了扯远了..

回归正题吧,可见如果要买到所有号码可能就血本无归了,基本是赢不了啦。

在聊下这个朋友,这个朋友后面需要我帮忙演算一下多期开奖结果的均值,也就是一万期,十万期…甚至几千万期之后的开奖均值,模拟演算了一下36选7一千万组数据的均值大概在129.495左右。

靠近均值附近中奖的概率高

理论上来讲既然均值是这个值,如果能通过数据证明大多数的开奖数据都会比较接近这个均值,就是分布概率会在均值附近,是不是就能证明往靠近均值的地方买彩票中奖率会高一些。
算一下最小值 1+2+3+4+5+6+7=28 最大值 30+31+32+33+34+35+36=241这些都只有一组,而越往中间,选择越多如果是 30的话就可以1+2+3+4+5+6+9/1+2+3+4+5+7+8 ..是有这个趋势的。不过从另外一个维度分析,正因为均值区域概率太大,在这一块的组合也多,也就是说在改区域块的中奖概率也跟这里面的组合数是一致的。最终结果还是每个开奖的概率都相等,成功把自己扳倒了…

如果每个号码的开奖概率相同,那么买出现概率最低的码是不是就能中奖

想起小时候,跟朋友玩猜大小游戏的时候如果小的次数开多了,他们就会一直猜大,甚至加注猜大,坐庄的我每次都被搞的慌的要死,虽然知道大小概率还是50%,但也没办法,因为确实是很邪门的一件事情。

在一千万组数据下来的时候会发现,每一个号码的开奖次数都是接近相等的,现在假设我有(1-10) 10个号码,10选1,一千期内出现的概率为 100次 102次 105 次 98次 95次 93次 112次 88次 97次 94次 96次,根据期数多了每个号码开奖概率会趋于相等的原理,如果一直买 88次的是不是就稳赢了,好吧,如果学过概率统计就知道独立事件是互不影响的。不要想太多。更何况36/7的组合想要不让最低号码全中奖一直到开奖概率平均也是能做到的。

需求分析

帮朋友做了这两个应用之后,开始研究这方面的需求,感觉现在的市场确实会有这方面的分析需求。
那我要做些什么功能来满足他们呢。

1.求平均,不管是35/7 ,22/5或是其他 做一个千万把求平均的功能(已完成,仅支持,不重复,无颜色随机)

2.随机摇号 做一个随机摇号的功能,当然生成数量有限 (已完成)

3.最重要的功能,开奖结果查询,将每一种彩票的开奖结果罗列出来。(已完成)

4.开奖结果分析,用折线图来展示开奖结果,比如说多期内同一个号码的开奖次数统计(已完成)

5.每一期的平均统计等 (已完成)

6.其他 (号码收藏后期会添加)

项目github地址

By Xiaolong:每一天都值得被认真对待!

AndroidPdf框架一览(一)

发表于 2017-08-09 | 分类于 Android第三方

1. 简介

本章会讲四种可以在Android端使用的pdf框架,并介绍其优势和劣势。

(1)android-pdfview

第一个当然是github上面star 最多android-pdfview,它是基于谷歌的一款开源PDF浏览框架VuDroid封装的Android端框架,
支持缩略图,缩放,页面枚举,默认起始页。还有pdf加载监听,pdf页面滑动监听。
功能很强大,不过很可惜,功能有限,作者也在15年停止了维护。

(2)pdfium

pdfium是谷歌开源的一款高性能的PDF渲染组件,用来作Chrome内核的pdf渲染。支持pdf加密文件打开,支持获取pdf文档信息,作者,标题,副标题,创建日期等…可以将指定的pdf页面渲染成bitmap,这个框架足以实现基本所有pdf定制化操作。美中不足的是框架比较大,对于移动端的包压缩是一个挑战

(3)mupdf

mupdf是一款轻量级的pdf浏览框架,基本上支持前面两者的功能,如果是文本的pdf文档还支持搜索,标注等功能。当之无愧的强大。虽说轻量,但编译出来的so库也不小。

(4)Android原生

Android在API19提供了android.graphics.pdf 这个pdf管理库,主要提供两个类pdfRender 和pdfDocument,pdfdocument 是将View转成pdf文件(require API19),pdfRender是将pdf文件绘制到bitmap上(require API21),这个就是原生提供的pdf浏览框架,但因为API要求很容易被拒之门外,不过随着API迭代,以后可能就无所谓了。

2.四者做一下对比

因为所有的框架都是将pdf转成图片做展示,所以定制化自己都能实现,那在这里这边对四者做一下对比。 这边的信息查看指pdf创建信息,大小指so库大小

框架名称 支持信息查看 支持文本检索 API要求 原始/打包后大小
VuDroid false false API16 or lower 19M->6.7M
pdfium true false API16 or lower 30M->15.3M
mupdf true true API16 or lower 70.2->36.6M
PdfRenderer false false API21 0

总结:mupdf功能最强大,pdfium次之,VuDroid和PdfRender都差不多。
但因为PdfRender有API限制。mupdf库有点大,所以大家根据需求来选择。

3.源码和Demo

VuDroid源码

VuDroidDemo下载地址

pdfium源码

pdfiumDemo下载地址
//写的并不是很好,正在看其他的开源框架思考更好的封装思路ing~~~
mupdf源码

mupdfDemo下载地址

PdfRendererWiki

PdfRenderer下载地址

By Xiaolong,每一天都值得被认真对待!

自省

发表于 2017-07-20 | 分类于 阶段性总结

想离职却又变心的自己

最近下班时间都在玩游戏,基本没怎么花时间反思最近的自己,现在做一下阶段总结。在这家公司待了三个多月了,最直观的感受是在这边基本上没人能带着你工作,手头的项目也都是我一个人在做,先后做了一个离线版的应用还有另外一个面向企业的应用的改版。在这期间也遇到了很多问题,一步一步克服了,但总的感觉最近的生活太安逸。之前一段时间还有想要离开这里去外地城市发展的想法,后来想想自己的技术尚浅,在加上已经换了两家公司了,决定还是先在这边多做学习。

代码与我

我觉得大多数程序员的梦想就是能写出很优雅的代码,能造出好用的轮子,我也是千千万万普通程序猿中的一员,也拥有着和他们一样的想法,我当初选择软件工程的原因是:我想成为一名很厉害的黑客,

因为在高中期间,无意中得到了一份易语言的邮件发送代码,然后就用这个做了好多盗QQ号软件,其实是很简单的,因为就一个用户界面让用户自愿输入帐号密码,然后把帐号密码发送到我的邮箱,那时候一天能盗几百个号,我记得最蠢的事情是我把帐号列表post在空间,然后有人问我为什么他的帐号在上面,想想真是尴尬。不过这些好玩的东西现在都做不了了。

只要有一台电脑在面前就能操控很多东西能做很多事情。事实是我太天真了。上了大学第一个学期的课的时候我的梦想就破灭了。哈哈,因为发现C++居然写的是控制台,不带图形界面的。我这个职业拉控件,写写忽悠人的软件的小朋友,眉头一皱,发现黑客并不是那么简单。大学总算过来了, 虽然算不上班级中的佼佼者,但是还是带着大多数同学完成了课程设计。
大一写的好像是黑杰克,控制台界面,PC跟玩家对战,PC毫无智商可言,233,只要牌小于15就会加牌,随机分牌,
大二软件工程用C#写的家庭财务管理系统(就一个GridView带统计功能),Java课程用Java写了俄罗斯方块。C#课程C#写了一个写字板的应用,跟着老师一步一步写的。自己写的并不多,自己也是刷过ACM做过各种好玩的算法的。

大二下的J2EE,用strut2+hibernate写了个博客系统。实现了基本增删改查操作。

大三:现代算法课写了护士排班,虽然我到现在也不明白,遗传算法和模拟退火算法的这些数据基数是怎么来的。

最开始我弄了一部分基准数据在里面,然后让遗传值无限接近这个基准,那其实我觉得算法应该是一次一次输入数据一次比一次更准确,因为有对前面数据的统计。所以我的算法是错误的,分数不高。坑爹的是我把这份代码送了班级的另一名同学,结果他拿了比我还高的分数。

大三:Andorid课程设计,五子棋人机对战版,那时候本来想写蓝牙对战,但是感觉难度蛮高,虽然也有demo实现,但是没有去做参考。人工智能:五子棋AI,一份网上参考的代码,打败了全班的小伙伴。带着我们小组成了第一名。基于极大极小搜索算法和剪枝优化策略下的五子棋算法,跟上面那个一样。

大四:大四的话基本没怎么做事情了,那时候设计模式课程设计拿的是比赛的项目,和小伙伴们参加了比赛,边比赛边创业。那时候的朵拍。拿了奖,入驻了众创空间,可能那就是离梦想最近的一次了。可是我们还是太年轻了。当然也是发现自身存在的不足和所需的技术实现难度,在毕业之前大家各自放弃了梦想,投入了工作。最后毕业设计,做的是一个移动端的任务发布平台,跟之前到位的功能有点像。用户可以在上面发任务,然后,接单的用户可以私下与其接洽,大致如此。提供地图标记和发布功能,提供聊天功能(WebSocket),提供用户有移动端和服务端。服务端用的是php一键式的~

写代码的日子总是这么有意思,其实我也仔细思考过写代码能做的最棒的事情是什么?就是,当有一天你已成家,有自己的孩子,你可以给你的孩子定制一些简单的小程序和游戏。我觉得这是一件超级幸福的事情,这就是写代码最幸福的追求吧。

那其实作为一个软件工程师职业,大多数人更想的是做出很好用的轮子,帮助很多初学的程序猿,让自己成为行业内知名的人物。

我觉得要做到这些,无异于就以下几点,首先要热爱自己的职业,其次是需要花时间在里面,搜罗各种资源让自己成长。在其次就是多思考,多创新。现在我也在朝这一目标往前走。

在前面一段时间我学习Andorid的资源都是来自网上,各种教学视频,各种各样的论坛博客,还有代码仓库。这边学一点,那边学一点,到最后将零零散散的知识拼凑到一起,勉勉强强能应付工作,但是遇到一些更深入的细节问题往往都不知道答案,很多时候我都知道,这里就是需要这么写,但是为什么需要这么写,自己从未去深入探究,
在这半年,我开始发现阅读一些有用的框架源码能发现一些很有意思的设计模式,还有一些优质的设计思想,我才发现自己当初真的算初学Java,入门的Andorid。

我觉得做一行开发,确实需要系统的去学习,我以前看的过郭霖的第一行代码,以为自己真正了解Android开发了,却没发现那本只是入门书籍。往里面还有很多路要走,现在我开始看中级进阶的书籍了。以前对开发都只是知道皮毛,没有深入去探究,现在终于可以通过学习把那些零散的点一点一点的拼凑起来,希望自己最终能实现自己的梦想吧。

自省

曾经我以为我和大多数刚毕业的开发者不一样,我比他们拥有更强的技术,但是后面我才发现其实都是一样的,我也是那个搬着轮子瞎开心的程序猿,并没有比别人厉害,也不能做到一蹴而就,发现写代码这东西除非用心,除非花时间,除非热爱,除非有天分,很少有人能在短时间成为技术大牛的。

总之,未来还长,边学边往前走吧。

By xiaolong:You have a dream,you got to protect it!

gradle学习笔记,常用命令,多渠道打包等

发表于 2017-07-07 | 分类于 Android项目构建

本文整理自:
http://stormzhang.com/devtools/2014/12/18/android-studio-tutorial4/
http://stormzhang.com/devtools/2015/01/05/android-studio-tutorial5/
http://stormzhang.com/devtools/2015/01/15/android-studio-tutorial6/

什么是Gradle?


Gradle是一种依赖管理工具,基于Groovy语言,面向Java应用为主,它抛弃了基于XML的各种繁琐配置,取而代之的是一种基于Groovy的内部领域特定(DSL)语言。

Gradle基本概念


项目目录:
index
从上到下依次分析

1. GradleDemo/app/build.gradle

这个文件是app文件夹下这个Module的gradle配置文件,也可以算是整个项目最主要的gradle配置文件,我们来看下这个文件的内容:

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
//声明是android程序
apply plugin: 'com.android.application'
android {
//SDK版本
compileSdkVersion 25
//build tools版本
buildToolsVersion "25.0.2"
defaultConfig {
// 应用包名
applicationId "com.standards.gradledemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug{
//debug版本
}
release {
//是否进行混淆
minifyEnabled false
//混淆文件配置
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// 编译libs目录下的所有jar包
compile fileTree(include: ['*.jar'], dir: 'libs')
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'
testCompile 'junit:junit:4.12'
// 编译devlib模块
compile project(':devlib')
}

说明:

  • buildToolsVersion这个需要你本地安装该版本才行,很多人导入新的第三方库,失败的原因之一是build version的版本不对,这个可以手动更改成你本地已有的版本或者打开 SDK Manager 去下载对应版本。

  • proguardFiles这部分有两段,前一部分代表系统默认的Android程序的混淆文件,该文件已经包含了基本的混淆声明,免去了我们很多事,这个文件的目录在 /tools/proguard/proguard-Android.txt , 后一部分是我们项目里的自定义的混淆文件,目录就在 app/proguard-rules.txt , 如果你用Studio 1.0创建的新项目默认生成的文件名是 proguard-rules.pro , 这个名字没关系,在这个文件里你可以声明一些第三方依赖的一些混淆规则,由于是开源项目,这里未进行混淆。最终混淆的结果是这两部分文件共同作用的。

  • 以上文件里的内容只是基本配置,其实还有很多自定义部分,如自动打包debug,release,beta等环境,签名,多渠道打包等,后续会单独拿出来讲解。

2. GradleDemo/devlib/build.gradle

每一个Module都需要有一个gradle配置文件,语法都是一样,唯一不同的是开头声明的是 apply plugin: ‘com.android.library’

3. GradleDemo/gradle

这个目录下有个 wrapper 文件夹,里面可以看到有两个文件,我们主要看下 gradle-wrapper.properties 这个文件的内容:

1
2
3
4
5
6
#Thu Jul 06 16:20:36 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

可以看到里面声明了gradle的目录与下载路径以及当前项目使用的gradle版本,这些默认的路径我们一般不会更改的,这个文件里指明的gradle版本不对也是很多导包不成功的原因之一。

4. GradleDemo/build.gradle

这个文件是整个项目的gradle基础配置文件,我们来看看这里面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

内容主要包含了两个方面:一个是声明仓库的源,这里可以看到是指明的jcenter(), 之前版本则是mavenCentral(), jcenter可以理解成是一个新的中央远程仓库,兼容maven中心仓库,而且性能更优。另一个是声明了android gradle plugin的版本,android studio 1.0正式版必须要求支持gradle plugin 1.0的版本。

5. GradleDemo/settings.gradle

这个文件是全局的项目配置文件,里面主要声明一些需要加入gradle的module,我们来看看9GAG该文件的内容:

1
include ':app', ':devlib'

文件中的 app, devlib 都是module,如果还有其他module都需要按照如上格式加进去。也可以Ctrl+Shift+Alt+S 进入项目配置里面可视化配置。

###Gradle命令详解

命令行Gradle编译的过程

1、切换到GradleDemo项目的根目录,执行 ./gradlew -v 来查看下项目所用的Gradle版本
如果你是第一次执行会去下载Gradle,下载成功后会看到如下信息

------------------------------------------------------------
Gradle 2.14.1
------------------------------------------------------------

Build time:   2016-07-18 06:38:37 UTC
Revision:     d9e2113d9fb05a5caabba61798bdb8dfdca83719

Groovy:       2.4.4
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_25 (Oracle Corporation 25.25-b02)
OS:           Windows 7 6.1 amd64

2、接着执行 ./gradlew clean执行这个命令去下载Gradle的一些依赖,下载成功并编译通过时会看到如下信息:

1
2
3
4
5
:clean UP-TO-DATE
:app:clean
:devlib:clean
BUILD SUCCESSFUL

紧接着在 GradleDemo/app/build/outputs/apk 目录下会看到类似于app-debug-unaligned.apk, app-release-unsigned.apk等,看名字应该能理解意思,unaligned代表没有进行zip优化的,unsigned代表没有签名的。然后就可以直接安装apk查看运行效果了。

Gradle常用命令

上面大家接触了一些命令如 ./gradlew -v ./gradlew clean ./gradlew build, 这里注意是./gradlew, ./代表当前目录,gradlew代表 gradle wrapper,意思是gradle的一层包装,大家可以理解为在这个项目本地就封装了gradle,即gradle wrapper, 在GraldeDemo/gradle/wrapper/gralde-wrapper.properties文件中声明了它指向的目录和版本。只要下载成功即可用grdlew wrapper的命令代替全局的gradle命令。

理解了gradle wrapper的概念,下面一些常用命令也就容易理解了。

./gradlew -v 版本号
./gradlew clean 清除9GAG/app目录下的build文件夹
./gradlew build 检查依赖并编译打包
这里注意的是 ./gradlew build 命令把debug、release环境的包都打出来,如果正式发布只需要打Release的包,该怎么办呢,下面介绍一个很有用的命令 assemble, 如

./gradlew assembleDebug 编译并打Debug包
./gradlew assembleRelease 编译并打Release的包
除此之外,assemble还可以和productFlavors结合使用,具体在下一篇多渠道打包进一步解释。

./gradlew installRelease Release模式打包并安装
./gradlew uninstallRelease 卸载Release模式包

###Gradle多渠道打包

友盟多渠道打包

1.以友盟统计为例,在AndroidManifest.xml里面会有这么一段:

1
2
3
<meta-data
android:name="UMENG_CHANNEL"
android:value="Channel_ID" />

里面的Channel_ID就是渠道标示。我们的目标就是在编译的时候这个值能够自动变化。

第一步,在AndroidManifest.xml里配置PlaceHolder

1
2
3
<meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}" />

第二步,在build.gradle设置productFlavors

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
android {
productFlavors {
xiaomi {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]
}
_360 {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "_360"]
}
baidu {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
}
wandoujia {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
}
}
}
或者批量修改
android {
productFlavors {
xiaomi {}
_360 {}
baidu {}
wandoujia {}
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}

很简单清晰有没有?直接执行 ./gradlew assembleRelease , 然后就可以静静的喝杯咖啡等待打包完成吧。

assemble结合Build Variants来创建task

  • ./gradlew assembleDebug
  • ./gradlew assembleRelease

除此之外 assemble 还能和 Product Flavor 结合创建新的任务,其实 assemble 是和 Build Variants 一起结合使用的,而 Build Variants = Build Type + Product Flavor , 举个例子大家就明白了:

如果我们想打包wandoujia渠道的release版本,执行如下命令就好了:

  • ./gradlew assembleWandoujiaRelease
    如果我们只打wandoujia渠道版本,则:

  • ./gradlew assembleWandoujia
    此命令会生成wandoujia渠道的Release和Debug版本

同理我想打全部Release版本:

  • ./gradlew assembleRelease
    这条命令会把Product Flavor下的所有渠道的Release版本都打出来。

总之,assemble 命令创建task有如下用法:

  • assemble: 允许直接构建一个Variant版本,例如assembleFlavor1Debug。
  • assemble: 允许构建指定Build Type的所有APK,例如assembleDebug将会构建Flavor1Debug和Flavor2Debug两个Variant版本。
  • assemble: 允许构建指定flavor的所有APK,例如assembleFlavor1将会构建Flavor1Debug和Flavor1Release两个Variant版本。

完整的gradle脚本

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
apply plugin: 'com.android.application'
def releaseTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
android {
compileSdkVersion 21
buildToolsVersion '21.1.2'
defaultConfig {
applicationId "com.boohee.*"
minSdkVersion 14
targetSdkVersion 21
versionCode 1
versionName "1.0"
// dex突破65535的限制
multiDexEnabled true
// 默认是umeng的渠道
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "umeng"]
}
lintOptions {
abortOnError false
}
signingConfigs {
debug {
// No debug config
}
release {
storeFile file("../yourapp.keystore")
storePassword "your password"
keyAlias "your alias"
keyPassword "your password"
}
}
buildTypes {
debug {
// 显示Log
buildConfigField "boolean", "LOG_DEBUG", "true"
versionNameSuffix "-debug"
minifyEnabled false
zipAlignEnabled false
shrinkResources false
signingConfig signingConfigs.debug
}
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
minifyEnabled true
zipAlignEnabled true
// 移除无用的resource文件
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
// 输出apk名称为boohee_v1.0_2015-01-15_wandoujia.apk
def fileName = "boohee_v${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
}
}
// 友盟多渠道打包
productFlavors {
wandoujia {}
_360 {}
baidu {}
xiaomi {}
tencent {}
taobao {}
...
}
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:21.0.3'
compile 'com.jakewharton:butterknife:6.0.0'
...
}

By Xiaolong,每一天都值得被认真对待!

Realm数据库的那些坑

发表于 2017-07-04 | 分类于 Android第三方

Realm算是移动端用的比较多的ORM框架了,当初选择的时候入了坑,现在只能在这里慢慢爬了,版本3.1.4。记录一些存在的坑。

Realm的缺点

id不能自增

解决方案:UUID自动生成,或者每次插入的时候获取最大的id往上+1.

实体类只能继承RealmObjet 或者实现RealmModel加@RealmClass注解。

只能直接继承不能间接继承。
我的遇到的问题如下,因为所有的数据库实体类都有一部分同样的字段。我就想把这些字段写到MyBaseObject基类中,再由基类来继承ReamObject。实体类直接继承基类。编译不过。
Error:(14, 8) 错误: Valid model classes must either extend RealmObject or implement RealmModel.

不能参照RealmObject自己实现RealmModel
翻看了一下ReamObject的源码发现它是对RealmModel的实现。天真的以为我也可以这么写一个基类来继承。依然编译不过。一样的错误。
这个RealmModel+注解的方式,简直是来逗我的

暂无解决方案。老老实实一个一个写!

查询的对象带有RealmProxy对象代理

带有代理的对象是可以直接进行Realm数据库操作的。
RealmProxy

实体会多一个ColumnInfo和ProxyState 的对象实例。。ProxyState里面又包含了该RealmProxy对象。
结构如下:
层级嵌套结构
可以看出这是一种递归的存储方式。至于java为什么不溢出,因为它的model直接指向了自己的地址。
问题:做遍历操作会导致ANR和内存溢出
解决方案:

  1. 类似于这种递归的方式如果需要做遍历操作的话。比如说转Json或者一些其他的操作的时候不要忘了忽略掉递归的对象,否则分分钟就内存溢出ANR了。比如Gson忽略某个字段参考
  2. 也可以直接把对象copyfromRealm,拿到真正的对象
    1
    Realm.getDefaultInstance().copyFromRealm(object);

数据库版本更新的那些坑

数据库版本更新介绍

通常来讲数据库更新有三个需要关注的地方。

  • 版本号 .schemaVersion(3)
  • 新版本信息 .migration()
  • 删库更新 .deleteRealmIfMigrationNeeded()
    代码如下:
    1
    2
    3
    4
    5
    6
    7
    Realm.init(this);
    RealmConfiguration config = new RealmConfiguration.Builder()
    .schemaVersion(3)
    .migration(new Migration())
    .deleteRealmIfMigrationNeeded()
    .build();
    Realm.setDefaultConfiguration(config);

版本号就需要每次有做更新schemaVersion往上增加
3 . 4 . 5 …
deleteRealmIfMigrationNeeded() 这句话慎用。

官方说明:

设置这将改变如何处理迁移异常的行为。磁盘上的Realm将被清除并重新创建
加了这句话,如果出现版本号不一致,或者更新出了某些问题。
就会清除掉所有的数据。
migration实现就不做过多介绍了,可以参考这个。

坑——数据库更新后不能回退

如果版本更新做了错误的表字段更新,你想要回退。
有两个解决方案(1) 删除掉应用重新安装。(数据会全部丢失)
(2)更新一个版本,把修改还原回去。(工作量大,但是数据不会丢。)

坑——基本类型字段添加

addField(“xxxfield”,long.class);
一定要用基本类型的class,如long.class,int.class,byte.class 不能用Long.class,Byte.class
这个坑将导致上面的坑。解决方案就看上面看上面吧。

realm对于这种经常用数据库存储的应用,真的不好用!!!

坑——Rx流操作

最近想把Realm用Rx流改写一下,然后又发现了他的坑了。

不支持线程切换

在Rx流中我们经常会做线程变换操作,然后Realm并不支持其他非创建线程中访问Realm对象。

线程切换
也就是说,你在哪个线程创建的Realm对象只能在哪个线程中使用。mmp,好吧,谢谢你的异常提醒,但我用的不是很爽。

不是很爽解决方案:把Realm对象创建在子线程
在IO线程中创建一个Realm对象毕竟数据库操作是个耗时操作。

1
Realm.getInstance(new RealmConfiguration.Builder().build());

之后就只能在IO线程中做操作了。别忘了~~ 2333

Rx操作

嗯,一个Rx流的查询操作,莫名其妙给我一直发数据流,相当于无限的查询。后面才发现是因为我的另外一个操作也是有对数据库的查询操作的。 不知道Realm内部怎么实现。就导致这两个的subscribe一直重复走。

解决方案:没有解决方案,建议不要用Rx流。。。。啊啊啊MMP啊 我回不去了!

迎接坑吧!!
realm对于这种经常用数据库存储的应用,真的不好用!!!

By Xiaolong:You have a dream,you got to protect it!

代码命名规范

发表于 2017-05-07 | 分类于 Android规范

前言

代码命名规范在开发过程中极其重要,记得进第一家公司的时候,刚进入就丢了一份java的代码规范文档给我。写了个小demo给当时的主管,然后因为命名规范问题被叫去谈话了,由此可见公司的开发人员都非常重视代码命名。因为很多时候一个应用的开发工作并不是你一个人完成的,你可能会有一个小伙伴与你同时开发,也可能你开发完成后会有其他的小伙伴接替你的工作。如果这个时候别人看到你乱七八糟的代码不知道从哪里下手估计心里早就把你骂了千百遍。换位思考,如果你是接班人,或者和你合作的小伙伴代码写的乱七八糟的。想要用他封装的某个功能,结果毛都没看懂。心里肯定要骂娘呀。
作为一个伟大的程序员候选人,我们的目标是创造好用且优雅的代码。
啊哈哈,那首先走出第一步。代码命名和注释。

包名

首先说下Android包名的命名规则。

Andorid的包名一般采用域名的反转,单词全小写。域名为www.example.com的包名为com.example,省略www。

包名的开始是一个顶级域名,比如com,cn,org等,包名使用.做为分隔符。第二位一般是二级域名,也可以根据不同机构各自的命名。
后面的命名可以用部门,项目等进行区分(也可以没有),例如:

com.example.project
在项目内可以根据功能不同,按照模块划分不同的包名,com.example.project.user就是表示用户模块。

也可以根据层级的不同而划分不同的包名,比如:com.example.prokect.activity,就是Acitivity相关的包。

当然也可以在不同层级里面再按照模块划分包名,比如:com.example.project.activity.user,表示和用户有关的Activity。

总结,包名一般是以反转域名开始,后面跟有项目名称(缩写,也可以没有)。

后面可以采用的区分包名方式:

按照类功能 com.example.project.bean(实体类) com.example.project.network(存放网络层相关)

模块下区分层级 com.example.project.ui.user;(这边放user模块UI) com.example.project.ui.widger(一些自定义控件)

类和接口

类名是一个或多个单词组成,采用大驼峰命名,尽量要使类名简洁且利于描述,例如:SignInActivity,类名规则如下:

大驼峰命名
简洁而富有表达性
尽量不使用缩写(广泛使用的单词除外,比如URL,XML…)
多单词中采用 名词+动词的方式命名: LocationManage
对于缩写单词要全部大写比如:XMLManage
一个类如果继承了Android的组件,需在使用该组件的名称作为后缀,这样容易区分该类的作用,比如: SgnInActivity,UserInfoFragment,FileUploadService…

接口一般使用I开头,采用大驼峰命名规则,比如:IPullToRefresh。

变量

Android变量分为三种:成员变量,静态变量和常量。

成员变量

成员变量采用小驼峰命名规则,第一单词的首字母小写,其后的首字母大写。变量名一般不使用_和$开头。例如:

private Intent cropIntent;
变量名应简短且易于描述,选用规则尽量简单,易于记忆和联想。

非控件的类成员变量可以用m开头,比如 private UserInfo mUserInfo;

尽量避免单个字符的变量名,除非是用于一次性的临时变量,临时的整形变量一般命名为 i,j,k,m,n。字符型的变量一般使用c,d,e。

对于View变量的命名规则,如果View是一个单词的,采用第一个单词小写的方式+对应View的描述进行,例如:

private View viewUserInfo;
如果是两个单词组成的View,比如:TextView,一般采用缩写的方式,例如:

private TextView tvUserName;
一般情况下Button缩写为:btn。

静态变量

为了可以很方便的区分静态变量,静态变量的命名一般采用小写的s开头,后面单词的命名规则和成员变量保持一致,例如:

private static Map sCacheStrings;

常量

常量命名规则一般是所有的单词都是大写,中间使用_(下划线)分割,例如:

private static final float SCALE_RATE = 1.25f;
代码中不允许出现单独的字符串或数字常量,比如xx.equals(“1”),单独的字符串或数字不利于后期的维护。如果需要使用数据或字符,请按照他们的含义封装成静态常量,或者使用枚举,for语句除外。

方法

方法命名规则采用小驼峰命名法例如:onCreate(),onRun(),方法名一般采用动词或者动名词。
一般使用的方法名前缀。

getXX()返回某个值的方法
initXX() 初始化相关方法,比如初始化布局:initView()
checkXX()和isXX() 方法为boolean值的时候使用is或者check为前缀
saveXX() 保存数据
clearXX()和removeXX() 清除数据
updateXX() 更新数据
processXX() 对数据进行处理
dispalyXX() 显示某某信息
draswXX() 绘制数据或者效果
setXXX()配置某些信息

对于方法的其他一些规范:

方法的参数尽可能不超过4个,需要更多的参数的时候可以采用class的方法
方法参数中尽量少使用boolean,使用boolean传参不利于代码的阅读
方法尽量不超过15行,方法过长,说明当前方法业务逻辑过于复杂,需要进行方法拆分
一个方法只做一件事,
如果一个方法返回的是一个错误码,可以使用异常
不使用try catch 处理业务逻辑
尽可能不实用null,替代为异常或者使用空的变量,比如Collections.emptyList()

Layout

Layout的命名规则需要和使用他们的组件对应,方便查找和维护,比如我们在创建一个用户信息的UserInfoActivity,对应的Layout的命名就应该是activity_user_info.xml。

对应Andorid组件的Layout命名规则:

Activity -> activity_user_info.xml
Fragment -> fragment_sign_up.xml
Dialog -> dialog_change_password.xml
AdapterView Item -> item_user.xml
Layout只是一部分 -> partial_stats_bar.xml

string和color

项目中使用的string,和color的值原则上都是必须放在strings.xml和colors.xml中,不要放在Java代码中,这样的好处是可复用,提高维护性,减少非必要的代码。颜色是因为基本上颜色主题差不多如果有需要改颜色,只需要该一下colors的色值就可以了。string的优势是如果以后做国际化的话string的代码就不需要再次做修改了。如果项目很大这些都没做好想要改掉所有的中文是很麻烦的。

xml的资源命名,字母全部小写,多个单词之间使用_(下划线)分割.

建议color的命名中体现其ARGB值,比如:
color_blue_0000ff(看一下大致的颜色后面加上色值)
这样的写法对于代码提示更加的友好,有利于对照标注图查找颜色值。

id命名

建议使用驼峰式命名,使用名词或者名词词组,应该通过id的命名可以直接理解当前的View要实现的功能.在这里使用驼峰命名的优势是,打个下划线特别麻烦。而且你在类中要找到这个命名也很麻烦。比如 tvUserName = findViewById(R.id.tv_user_name);如果都使用驼峰命名,二者是一样的 tvUserName =findViewById(R.id.tvUserName);这样就可以使出copy paste 大招了!!!

例如:

@+id/tvUserName
id命名的第一个单词使用View的缩写,如果View只是一个单词,缩写就是当前单词。一般Button的缩写为:btn。

Drawable命名

Drawable的命名规则根据使用的控件来命名,控件的缩写在前面,后面使用表示其功能的一个或者多个单词,中间使用使用_下划线分割。

Action bar使用ab_,比如:abstacked.png
Button 使用btn

Dialgo 使用dialog
Divide 使用 divider

Icon 使用 ic
Menu 使用menu

Notification使用 notification
Tabs 使用tab

Drawable是有多个状态的,在命名中体现出状态的不同:

Normal 对应_normal结尾,比如btn_order_normal.9.png
Pressed 对应_pressed结尾
Focused 对应_focused结尾
Disabled 对应_disabled结尾
Selected 对应_selected结尾
其他资源文件的命名需要遵守Android的规范即可,比如arrays.xml数组文件,dimens.xml分辨的配置,style.xml样式的配置,资源文件的ID命名规则都是才是字母小写,使用下划线分割的原则。

暂时就想到这么多了,有漏的后面再做补充!!

By Xiaolong,每一天都值得被认真对待!

接口设计规范

发表于 2017-05-07 | 分类于 Android规范

接口通用信息

示例地址

http://....../app/getApp

接口控制器:

外网  http://110.110.1.20/app/

内网  http://192.168.1.22/app/

公共参数:

getApp接口名称

请求有以下两种方式

GET

POST

上传参数说明

UUID 设备号:

    每个手机的设备号是唯一的

上传参数的格式

{"key":"value","key1":value1,"key2":"value2",..........}

返回的数据规范

a、明文返回值,直接使用

b、部分字段加密的返回值通过DES.decryptDES()解密,需要key和vi进行解密,

    {
        "code":1,
        "msg":"xxxxxx",
        "data":{}
    }

返回数据说明

a、code 标识返回状态
   1    成功
  -1    失败
-101    token失效
b、msg 服务端返回的说明
c、data为空,返回 {}

返回示例

如:
{
    "code":1,

    "msg":"",

    "data":{

        "param1":"xxx",

        "list":[

            { 
                "aa":"xxx",
                "bb":1,
                "cc":"xxxxxx"
            },
            ...........
        ]
    }
}

如2:

{
    "code":1,
    "msg":"",
    "data":{}

}

基础模块

获取访问令牌

请求地址

http://192.168.1.20/app/getAccessToken

请求方式

POST

接口参数

名称           类型        是否必须    示例值                   描述
uuid          String        是    321234098546345634534526   设备号

响应参数

名称           类型        是否必须    示例值                   描述
acccessToken  String        是     访问令牌
expiresIn     String        是       28800                  过期时间, 单位为秒(可不做过期)
sessionKey    String        是     78......610              session key(32位字符)
sessionSecret String        是     2222......0              session密钥(32位字符)

返回示例

{
    "code":1,
    "msg":"",
    "data":{
        "accessToken":"AVjWf49ZrHPXsqA1hwSr7AcheUIb/oaRTR
        s0GzXxzsHBm79lcNLSNGjnisHaDBAWVM
        R8tR0xMQjhIdwkve8eNTs=",            //访问令牌
        "expiresIn":28800,                                 //Int, 过期时间, 单位为秒
        "sessionKey":"787d9f9beeeca3379cde65ff3354e610",   //session key
        "sessionSecret":"222baa398b38a499f267ed869215b130"  //session密钥
    }

}

code取值

 1成功

-1失败

账号模块

登录(4.10增加返回字段:classNum,grade)

请求地址

http://..../getLogin

请求方式

POST

接口参数

名称           类型        是否必须    示例值            描述

stuNo          String         是    SWE13001        学号
stuPsw         String         是    2341            密码

响应参数

名称             类型     是否必须     示例值           描述
displayName     String    是       张三            显示名称
authenUserId    String    是       11.......fb    登录用户ID
headerUrl       String    是       http://.x.png  头像
major           String    是       软件工程        专业
stuNo           String    是       SWE13001       学号
phone           String    是       13600000001    联系电话 
calssNum        String    是       软件工程一班     班级 
grade           String    是       2013级         年级

返回示例

{
    "code":1,
    "msg":"登录成功",
    "data":{
        "displayName":"张三",
        "headerUrl":"http://192.168.1.20/image/header.png"
        "authenUserId":"1120d8d6ea4a4850b65d0faa40d6dffb
        "major":"软件工程",
        "stuNo":"SWE13001",
        "phone":"13600000001",
        "calssNum":"软件工程一班",
        "phone":"2013级"
    }
 }

code取值

 1成功
-1失败

获取用户头像(已弃用)

请求地址

http://..../getUserHeader

请求方式

POST

接口参数

名称           类型        是否必须    示例值            描述
stuNo          String         是    SWE13001        学号

响应参数

名称        类型      是否必须    示例值             描述
header        String         是     http://xxx.jpg      头像地址

返回示例

{
    "code":1,
    "msg":"获取头像成功",
    "data":{
        "header":"http://xxx.jpg",
    }
 }

code取值

 1成功
-1失败

注册(已弃用)

请求地址

http://..../register

请求方式

POST

接口参数

名称           类型     是否必须    示例值            描述
stuNo          String      是     SWE13001        学号(学号要唯一)
stuPsw         String      是     2341            密码
data           String      是     xxxx            二进制数据
name           String      是     picture_0105    图片名称

响应参数

名称          类型    是否必须     示例值           描述
stuName      String      是     张三             显示名称
authenUserId String      是     11.......fb     登录用户ID
headerUrl    String      是     http://.x.png   头像
major        String      是     软件工程         专业
stuNo        String      是     SWE13001        学号
phone        String      是     联系电话         13600000001

返回示例

{
    "code":1,
    "msg":"注册成功",
    "data":{
        "displayName":"张三",
        "headerUrl":"http://192.168.1.20/image/header.png"
        "authenUserId":"1120d8d6ea4a4850b65d0faa40d6dffb
        "major":"软件工程",
        "stuNo":"SWE13001"
        "phone":"13600000001"
    }
 }

code取值

 1成功
-1失败

主页

获取今日课程列表

请求地址

http://..../getTodayCourse(4.10 state 增加未签到状态)

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述
authenUserId String    是    11...ffb   用户ID

响应参数

名称        类型        是否必须    示例值        描述
courseId   String       是     sdfe12        课程Id
name       String       是     软件工程       课程名称
date       String       是     20170122      上课日期
state      int          是      0            签到状态 0未开放签到 1正常
                                                    2旷课      3迟到  4请假 5.未签到
teacher    String       是     顾萍萍        教师名称
classPlace String       是     主一110       上课地点
classTime  int          是     0            节次  0一二节 1三四节 2午一午二
                                                    3五六节 4七八节 5 九十节

返回示例

{
    "code":1,
    "msg":"",
    "data":{
        "total":5,
        "list":[
            {
                "courseId":"sdfe12",
                "name":"软件工程",
                "date":"20170122",
                "state":0,
                "teacher":顾萍萍,
                "classPlace":"主一110",
                "classTime" : 0
            },
            ......
        ]
    }
 }

code取值

 1成功
-1失败

签到

请求地址

http://..../signIn

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述

authenUserId String    是    11...ffb   用户ID
courseId     String    是    xxxx      课程Id
date         String    是    20170328  日期
image        String    否    xxxx      二进制数据
classTime    int       是    节次       上课时间  0一二节 1三四节 2午一午二
                                                3五六节 4七八节 5九十节

返回示例

{
    "code":1,
    "msg":"签到成功",
    "data":{
    }
 }

code取值

 1成功
-1失败

获取当前用户全部课程(可以查询每门课的考勤结果)

请求地址

http://..../getAllCourseList

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述
authenUserId String    是    11...ffb   用户ID

响应参数

名称        类型        是否必须    示例值        描述
courseId   String       是     sdfe12        课程Id
name       String       是     软件工程       课程名称 
teacher    String       是     顾萍萍          任课教师名称

返回示例

{
    "code":1,
    "msg":"获取课程列表成功",
    "data":{
     "total":5,
        "list":[
               {
                "courseId":"sdfe12",
                "name":"软件工程",
                "teacher":"任课教师名称",
            },
            ......
        ]
    }
 }

code取值

 1成功
-1失败

考勤结果查询(4.10 state 增加未签到状态)

请求地址

http://..../checkResult

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述

authenUserId String    是    11...ffb   用户ID
startTime    String    是    20170222   起始时间
endTime      String    是    20170222   结束时间
courseId     String    否    “sd11002”  课程Id(为空代表查询全部课程)

响应参数

名称        类型        是否必须    示例值        描述
courseId   String       是     sdfe12        课程Id
name       String       是     软件工程       课程名称
date       String       是     20170122      上课日期
state      int          是      0            签到状态 0未开放签到 1正常
                                                    2旷课      3迟到  4请假 5未签到
teacher    String       是     顾萍萍        教师名称
classPlace String       是     主一110       上课地点
classTime  int          是     0            节次  0一二节 1三四节 2午一午二
                                                    3五六节 4七八节 5 九十节

返回示例

{
    "code":1,
    "msg":"获取考勤结果成功",
    "data":{
        "total":5,
        "list":[
           {
                "courseId":"sdfe12",
                "name":"软件工程",
                "date":"20170122",
                "state":0,
                "teacher":顾萍萍,
                "classPlace":"主一110",
                "classTime" : 0
            },
            ......
        ]

    }
 }

code取值

 1成功
-1失败

考勤结果列表(4.10 state 增加未签到状态)

请求地址

http://..../checkTruant

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述

authenUserId String    是    11...ffb   用户ID

响应参数

名称        类型        是否必须    示例值        描述

courseId   String       是     sdfe12        课程Id
name       String       是     软件工程       课程名称
date       String       是     20170122      上课日期
state      int          是      0            签到状态 0未开放签到 1正常
                                                    2旷课      3迟到  4请假 5.未签到
teacher    String       是     顾萍萍        教师名称
classPlace String       是     主一110       上课地点
classTime  int          是     0            节次  0一二节 1三四节 2午一午二
                                                    3五六节 4七八节 5 九十节

返回示例

{
    "code":1,
    "msg":"获取考勤结果成功",
    "data":{
        "total":5,
        "list":[
           {
                "courseId":"sdfe12",
                "name":"软件工程",
                "date":"20170122",
                "state":0,
                "teacher":顾萍萍,
                "classPlace":"主一110",
                "classTime" : 0
            },
            ......
        ]

    }
 }

code取值

 1成功
-1失败

获取本周课程列表

请求地址

http://..../getWeekCourse(4.10 state 增加未签到状态)

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述

authenUserId String    是    11...ffb   用户ID

响应参数

名称        类型        是否必须    示例值        描述

courseId   String       是     sdfe12        课程Id
name       String       是     软件工程       课程名称
date       String       是     20170122      上课日期
state      int          是      0            签到状态 0未开放签到 1正常
                                                    2旷课      3迟到  4请假 5.未签到
teacher    String       是     顾萍萍        教师名称
classPlace String       是     主一110       上课地点
classTime  int          是     0            节次  0一二节 1三四节 2午一午二
                                                    3五六节 4七八节 5 九十节

返回示例

{
    "code":1,
    "msg":"",
    "data":{
        "total":5,
        "list":[
            {
                "courseId":"sdfe12",
                "name":"软件工程",
                "date":"20170122",
                "state":0,
                "teacher":顾萍萍,
                "classPlace":"主一110",
                "classTime" : 0
            },
            ......
        ]
    }
 }

code取值

 1成功
-1失败

请假

请求地址

http://..../takeOff

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述
authenUserId String    是      11...ffb   用户ID
courseId     String    是      ddasd131   课程ID
date         String    是      20170328   请假日期
classTime    int       是         0       节次  0一二节 1三四节 2午一午二
                                                    3五六节 4七八节 5 九十节
data         String    是      xxxx       二进制数据
name         Sring     是      xxx.png    图片名称
reason       String    是      想睡觉      请假理由

返回示例

{
    "code":1,
    "msg":"请假成功",
    "data":{
    }
 }

code取值

 1成功
-1失败

图片上传(4.10弃用)

请求地址

http://..../uploadPicture

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述
authenUserId String  是    11...ffb      用户ID
data         String  是  xxxx           二进制数据
name         String  是 picture_0105    图片名称
type         int     是       0         0头像

返回示例

{
    "code":1,
    "msg":"头像上传成功!",
    "data":{
    }
 }

code取值

 1成功
-1失败

用户密码修改(4.10新增)

请求地址

http://..../pswModify

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述
authenUserId String  是    11...ffb      用户ID
oldpsw       String  是    10086         旧密码
newpsw       String  是    10087         新密码

返回示例

{
    "code":1,
    "msg":"密码修改成功!",
    "data":{
    }
 }

code取值

 1成功
-1失败

用户信息修改(4.10新增)

请求地址

http://..../userInfoModify

请求方式

POST

接口参数

名称          类型    是否必须    示例值     描述
authenUserId String  是    11...ffb      用户ID
phone        String  是    10086         手机号码

返回示例

{
    "code":1,
    "msg":"修改成功!",
    "data":{
    }
 }

code取值

 1成功
-1失败

By Xiaolong,每一天都值得被认真对待!

工作总结(二)

发表于 2017-05-07 | 分类于 阶段性总结

闲谈:

工作总结
又换了新工作了,讲一下离职原因吧
  1. 工作太闲(因为业务砍了很多,所以手头上的事情都很少,几个Android开发维护一个项目,基本上每天的上班时间都是是在逛论坛,写博客)
  2. 新的生活环境不适应(这个也算是一个大原因,以前是住岛内跟朋友合住,但是后面公司搬到岛外去了,跟着公司去岛外找了个房子,那边的房子比较潮湿,基本上每天都病怏怏的,特别难受。)
  3. 工资不涨(这是我的第二家公司,那时候已经在第一家基本熟悉了整个开发流程才到这家公司,那时候开的薪资特别低,说到底还是自己的原因啦,感觉每次去一家公司工资都比别人低。并不是技术比别人差,而是不会强势)

上一家公司是纯互联网公司,主营业务做的是旅游行业B2B的业务,公司规模还算可以。我是在快毕业那会进这家公司的,在这家公司工作的期间算是我成长最快的一个时期,在这家公司学到了好多东西。

从一些主流第三方框架学习到项目架构设计封装。一些项目功能的抽取封装。项目中有很多的设计模式,单例模式,观察者模式,责任链模式,建造者模式,装饰器模式,适配器模式。基本都能在这家公司原来的框架里看到。在这边看到了很多很有意思的代码。至少在我之前是没想到还有这么写的。这里其实应该感谢之前公司的老大,工作了十几年,他的技术特别强,对于封装还有架构的设计都有自己的思路。刚进去从熟悉代码,到代码规范,基本都对我指点了一番。在我离开之际他开始着手跨平台开发了。

很可惜不跟随他和公司一起往下走,但心里一直存有感激。(其实对于自己有过的每一份工作即使离开了心里多少都会有感激,毕竟这是成长过程中可贵的经历)

关于新公司工作的感受

关于入职,这段时间在各大招聘平台上找了不少工作,投了很多相对而言比较大的公司的简历。也面试了几家,给我的感觉是现在的Android开发供过于求。
企业开始各种筛选各种挑,也确实,现在Andorid开发的门槛特别低,随便会写点java,网上找找现成的源码。就可以开发出自己想要的功能了。一些框架也比以前好很多,网上都很多 retrofit,okhttp,MVP架构的项目demo,随便拿一个来用就可以实现大多数APP了。
所以我其实这份工作更考虑找一个framework层开发的工作。看了这家公司的资料,很多东西都是关于硬件方面的,我以为这份工作就是底层的工作了,那时候面试的时候,我现在的经理有跟我说蓝牙相关的,我以为是针对于蓝牙的二次开发。直接说可以了,但其实用到的是蓝牙接口的一些命令来实现与硬件做交互,实际上的核心还是偏向应用层开发。
由于软件部门较小,所以很多流程都不完善,没有原型文档,没有接口文档,UI特别少,刚进来的时候一脸懵逼。
比如说销售会直接跟我们提开发相关的功能,测试也基本都是销售和产品经理兼职测试。一个项目下来要做的东西特别杂,经常因为没有确定好需求,需要修修补补。但这样也有一个好处是开发流程的时间能节省不少,毕竟少了一个产品与销售沟通和出原型的过程,当然更大的一个好处是部门还小,这些可以自己去完善,制定规则,到时候发展空间也比较大。总之干一行爱一行吧。

谈一些工作中遇到的坑

公司有几份项目特别老,大概有三四年左右的历史了,里面的代码也不能说乱吧。就是命名规范简直不忍直视,能达得到我工作之前的水平了。我找一个功能的代码找了半天。
觉得命名规范这个东西还是需要每个人都去注意一下。
接下来打算整理一份命名规范的文档。
代码命名很重要!!!代码命名很重要!!!代码命名很重要!!!不要给自己和后人留坑!!

By xiaolong:You have a dream,you got to protect it!

MPAndroidChart绘制浅析

发表于 2017-03-31 | 分类于 Android源码解析

前言

一直在使用MPAndroidChart但对其内部机制却没有做多少了解,自己之前还修改过MPAndroidChart的源码,某次面试被问到,MPAndroidChart是怎样进行绘制的,瞬间一脸懵逼,回答了个大概,但是被看出其实不是很了解。算亡羊补牢吧,今天抽了点时间看了MPAndroidChart 3.0的源码部分。
直接进入正题吧。

Chart基类

这边顺便讲解一下Chart这个类,它是所有图表类的抽象基类,继承自ViewGroup,实现了图表接口ChartInterface(这个接口用来实现图表的大小,边界,和范围的获取。),Chart里面存放了一些公共的配置和一些共有的抽象方法,数据等。

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/**
* 这个类是图表类的基类,继承自ViewGroup,它可以让图表像View一样被使用。
* @author Philipp Jahoda
*/
@SuppressLint("NewApi")
public abstract class Chart<T extends ChartData<? extends IDataSet<? extends Entry>>> extends
ViewGroup
implements ChartInterface {
public static final String LOG_TAG = "MPAndroidChart";
/**
* 声明log是否开启(调试用)
*/
protected boolean mLogEnabled = false;
/**
* 图表数据
*/
protected T mData = null;
/**
* 触摸区域高亮
*/
protected boolean mHighLightPerTapEnabled = true;
/**
* 触摸事件完成后是否继续滚动(可以试试pieChart的旋转功能)
*/
private boolean mDragDecelerationEnabled = true;
/**
* 这个就是滚动的减慢速度,算摩擦系数,0就直接停了,1会一直转,所以他回自动变成0.999,
*/
private float mDragDecelerationFrictionCoef = 0.9f;
/**
* 数据格式化
*/
protected ValueFormatter mDefaultFormatter;
/**
* 图表描述画笔
*/
protected Paint mDescPaint;
/**
* 这个画笔是用来画无数据的情况
*/
protected Paint mInfoPaint;
/**
* 描述描述!!
*/
protected String mDescription = "Description";
/**
* X轴的label可以看折现图的横轴label
*/
protected XAxis mXAxis;
/**
* 手势开关
*/
protected boolean mTouchEnabled = true;
/**
* Legend是一个图例描述,里面是这个图例,位置等其他配置的信息。
*/
protected Legend mLegend;
/**
* 数据选中监听
*/
protected OnChartValueSelectedListener mSelectionListener;
/**
* 图表点击监听
*/
protected ChartTouchListener mChartTouchListener;
/**
* 空数据文本
*/
private String mNoDataText = "No chart data available.";
/**
* 手势监听
*/
private OnChartGestureListener mGestureListener;
/**
* 无数据描述
*/
private String mNoDataTextDescription;
/**
* 这个是图例绘制类
*/
protected LegendRenderer mLegendRenderer;
/**
* 数据绘制类基类实现
*/
protected DataRenderer mRenderer;
/**
* 区域高亮辅助基类。用来计算高亮区域并返回
*/
protected ChartHighlighter mHighlighter;
/**
* 边界约束管理
*/
protected ViewPortHandler mViewPortHandler;
/**
* 动画
*/
protected ChartAnimator mAnimator;
/**
* 边距
*/
private float mExtraTopOffset = 0.f,
mExtraRightOffset = 0.f,
mExtraBottomOffset = 0.f,
mExtraLeftOffset = 0.f;
/**
* 默认构造
*/
public Chart(Context context) {
super(context);
init();
}
/**
* constructor for initialization in xml
*/
public Chart(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* even more awesome constructor
*/
public Chart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* initialize all paints and stuff
* 初始化函数,负责一些对象的初始化
*/
protected void init() {
...略
}
/**
* 设置新数据,在这里有个notify,在数据添加完之后刷新图表
*/
public void setData(T data) {
// let the chart know there is new data
notifyDataSetChanged();
}
/**
* 清空所有数据并刷新
*/
public void clear() {
mData = null;
mIndicesToHighlight = null;
invalidate();
}
/**
* 上面是直接置空。可能是想回收,这里是清除值后刷新
*/
public void clearValues() {
mData.clearValues();
invalidate();
}
/**
* 判断data是否为空
*/
public boolean isEmpty() {
if (mData == null)
return true;
else {
if (mData.getYValCount() <= 0)
return true;
else
return false;
}
}
/**
* 这个方法可以让图表知道自己掌握的数据,并显示出来。
* 需要重新进行计算
*/
public abstract void notifyDataSetChanged();
/**
* 边距计算
*/
protected abstract void calculateOffsets();
/**
* 最大最小y值计算抽象方法
*/
protected abstract void calcMinMax();
/**
* 计算单位
*/
protected void calculateFormatter(float min, float max) {
...略
}
/**
* 边距计算
*/
private boolean mOffsetsCalculated = false;
protected Paint mDrawPaint;
/**
* 主要的绘制方法
*/
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
...略
}
/**
* 描述位置
*/
private PointF mDescriptionPosition;
/**
* 绘制描述
*/
protected void drawDescription(Canvas c) {
...略
}
/**高亮模块 支持点击高亮等各种效果。这边处理绘制还有计算*/
..略
/**下面是MarkView */
..略
/** 下面是动画处理 */
..略
/** 动画开放方法 */
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX,
EasingFunction easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateX(int durationMillis, EasingFunction easing) {
mAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, EasingFunction easing) {
mAnimator.animateY(durationMillis, easing);
}
/** 动画开放配置 */
public void animateXY(int durationMillisX, int durationMillisY, Easing.EasingOption easingX,
Easing.EasingOption easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateX(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateY(durationMillis, easing);
}
public void animateX(int durationMillis) {
mAnimator.animateX(durationMillis);
}
public void animateXY(int durationMillisX, int durationMillisY) {
mAnimator.animateXY(durationMillisX, durationMillisY);
}
/**在往下就是各种配置了 */
略略略
}

它是所有图表的基类,里面是一些基础的方法,包括数据,高亮,动画,描述,空数据等公用方法的实现和抽象。保存当前图表信息等…
继承结构如下

  1. Chart(图表类基类)
    1. BarLineChartBase(柱状图折线图抽象类)
      1. BubbleChart(气泡图)
      2. CandleStickChart(烛状图)
      3. CombinedChart(复合图表)
      4. BarChart(柱状图)
      5. LineChart(折线图)
      6. ScatterChart(散点图)
    2. PieRadarChartBase(饼状图雷达图抽象类)
      1. PieChart(饼状图)
      2. RadarChart(雷达图)

根据不同的图表做了不同的实现,比如说BarLineChartBase都有一个共同的属性是XY轴,在onDraw方法中对XY轴做了绘制,BarLineChartBase还支持缩放操作。PieRadarBase是没有XY轴且不支持缩放操作,但支持旋转。所以将这两个图单独抽象了一层。
onDraw算是共有的主要方法。因为数据绘制都是在onDraw方法中的canvas上面。

下面来讲解一下MPAndroidChart的绘制过程

MPAndroidChart绘制过程

所有的绘制都做了抽象在这边基本能重用的方法就做一层抽象。
我们来看一下最底层的抽象Renderer类
Chart的绘制是经由Renderer类之手,看一下Renderer类的实现。

Render

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
/**
* Abstract baseclass of all Renderers.
*
* @author Philipp Jahoda
*/
public abstract class Renderer {
/**
* 这个变量用来存放绘制区域还有偏移量等设置
*/
protected ViewPortHandler mViewPortHandler;
/**
* X轴需要绘制的最小值
*/
protected int mMinX = 0;
/**
* X轴需要绘制的最大值
*/
protected int mMaxX = 0;
/**
* 这边通过构造传入ViewPortHandler
*/
public Renderer(ViewPortHandler viewPortHandler) {
this.mViewPortHandler = viewPortHandler;
}
/**
* 这个方法用来计算当前的值是否在X的最小和最大之间
*/
protected boolean fitsBounds(float val, float min, float max) {
if (val < min || val > max)
return false;
else
return true;
}
/**
* 这个方法用来计算当前可以显示的X的大小,
*/
public void calcXBounds(BarLineScatterCandleBubbleDataProvider dataProvider, int xAxisModulus) {
int low = dataProvider.getLowestVisibleXIndex();
int high = dataProvider.getHighestVisibleXIndex();
int subLow = (low % xAxisModulus == 0) ? xAxisModulus : 0;
mMinX = Math.max((low / xAxisModulus) * (xAxisModulus) - subLow, 0);
mMaxX = Math.min((high / xAxisModulus) * (xAxisModulus) + xAxisModulus, (int) dataProvider.getXChartMax());
}
}

这个类里面的方法用来确定当前视图可显示的大小,所有的Render类都继承自它。包括AxisRenderer(轴和轴值绘制),LegendRender(图例绘制),DataRender(图表图形绘制)。
AxisRenderer,和LegendRender的实现都大同小异,DataRender属于图表绘制的抽象,因为图表的样式比较多,它扩展了一些可以供图表使用的方法,接下来主要拿DataRender来讲。

DataRender

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
/**
* 这个类是Renderer的子类,用来提供一些抽象的绘制方法。
*/
public abstract class DataRenderer extends Renderer {
/**
* 这个对象是用来设置图表动画的
*/
protected ChartAnimator mAnimator;
/**
* 初始化图表item绘制的画笔
*/
protected Paint mRenderPaint;
/**
* 绘制高亮区域
*/
protected Paint mHighlightPaint;
/**
* 这个没见用到
*/
protected Paint mDrawPaint;
/**
* 这个用来绘制图表的文本信息
*/
protected Paint mValuePaint;
/**
* 构造函数传入了动画对象ChartAnimator,控制绘制区域和偏移量的对象ViewPortHandler,里面做的是变量初始化操作
*/
public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(viewPortHandler);
略..
}
/**
* 这个方法返回了文本绘制的画笔对象
*/
public Paint getPaintValues() {
return mValuePaint;
}
/**
* 高亮画笔
*/
public Paint getPaintHighlight() {
return mHighlightPaint;
}
/**
* 绘制画笔
*/
public Paint getPaintRender() {
return mRenderPaint;
}
/**
* Applies the required styling (provided by the DataSet) to the value-paint
* object.
*
* @param set
*/
protected void applyValueTextStyle(IDataSet set) {
mValuePaint.setTypeface(set.getValueTypeface());
mValuePaint.setTextSize(set.getValueTextSize());
}
/**
* 初始化Buffers,buffer是用来进行尺寸变换的一个类,他和transformer类配合生成实际的尺寸
*/
public abstract void initBuffers();
/**
* 数据绘制抽象类
*/
public abstract void drawData(Canvas c);
/**
*数值绘制抽象类
*/
public abstract void drawValues(Canvas c);
/**
* 数值绘制
*/
public void drawValue(Canvas c, ValueFormatter formatter, float value, Entry entry, int dataSetIndex, float x, float y, int color) {
mValuePaint.setColor(color);
c.drawText(formatter.getFormattedValue(value, entry, dataSetIndex, mViewPortHandler), x, y, mValuePaint);
}
/**
* 这个是为LineChart 或者PieChart等设计的附加绘制。因为lineChart需要为每个点进行绘制,PieChart可能需要绘制中间圆形等。
* @param c
*/
public abstract void drawExtras(Canvas c);
/**
* 绘制高亮数据
*/
public abstract void drawHighlighted(Canvas c, Highlight[] indices);
}

DataRender里面的方法大致如下

  1. 图表的绘制抽象方法 (drawData)
  2. 数值绘制的抽象方法 (drawValues)
  3. 数值绘制方法(drawValue)
  4. 图表高亮抽象方法(drawHighlighted)
  5. 动画对象和画笔对象的初始化(构造函数)

看一下BarChartRender的实现

BarChartRender


public class BarChartRenderer extends DataRenderer {
    /**
     * BarDataProvider这个类中存放了所有的barData,还有一些类似于阴影,数值位置,高亮箭头等。
     */
    protected BarDataProvider mChart;

    /** the rect object that is used for drawing the bars
     *  这个是用来设置每条bar的大小。主要是用做高亮绘制
     */
    protected RectF mBarRect = new RectF();
    /**
     * 声明buffer的数组
     */
    protected BarBuffer[] mBarBuffers;
    /**
     * 阴影画笔
     */
    protected Paint mShadowPaint;
    /**
     * 边框画笔
     */
    protected Paint mBarBorderPaint;

    /**
     * 构造函数传入了BarDataProvider,动画类,显示位置控制类,初始化了绘制所需的画笔
     */
    public BarChartRenderer(BarDataProvider chart, ChartAnimator animator,
            ViewPortHandler viewPortHandler) {
        super(animator, viewPortHandler);
        this.mChart = chart;

        mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mHighlightPaint.setStyle(Paint.Style.FILL);
        mHighlightPaint.setColor(Color.rgb(0, 0, 0));
        // set alpha after color
        mHighlightPaint.setAlpha(120);

        mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mShadowPaint.setStyle(Paint.Style.FILL);

        mBarBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBarBorderPaint.setStyle(Paint.Style.STROKE);
    }

    /**
     * 初始化Buffer
     */
    @Override
    public void initBuffers() {

        BarData barData = mChart.getBarData();
        mBarBuffers = new BarBuffer[barData.getDataSetCount()];

        for (int i = 0; i < mBarBuffers.length; i++) {
            IBarDataSet set = barData.getDataSetByIndex(i);
            mBarBuffers[i] = new BarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1),
                    barData.getGroupSpace(),
                    barData.getDataSetCount(), set.isStacked());
        }
    }

    /**
     * 绘制图表
     * @param c
     */
    @Override
    public void drawData(Canvas c) {

        BarData barData = mChart.getBarData();

        for (int i = 0; i < barData.getDataSetCount(); i++) {

            IBarDataSet set = barData.getDataSetByIndex(i);

            if (set.isVisible() && set.getEntryCount() > 0) {
                drawDataSet(c, set, i);
            }
        }
    }

    /**
     * 根据每个dataset绘制图表
     */
    protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) {
       ...略
    }

    /**
     * 准备高亮的区域
     */
    protected void prepareBarHighlight(float x, float y1, float y2, float barspaceHalf,
            Transformer trans) {

        float barWidth = 0.5f;

        float left = x - barWidth + barspaceHalf;
        float right = x + barWidth - barspaceHalf;
        float top = y1;
        float bottom = y2;

        mBarRect.set(left, top, right, bottom);

        trans.rectValueToPixel(mBarRect, mAnimator.getPhaseY());
    }

    /**
     * 绘制数值  一系列的计算
     */
    @Override
    public void drawValues(Canvas c) {
         略...     }

    /**
     * 绘制高亮  一系列的计算
     */
    @Override
    public void drawHighlighted(Canvas c, Highlight[] indices) {
            略...
    }

    public float[] getTransformedValues(Transformer trans, IBarDataSet data,
            int dataSetIndex) {
        return trans.generateTransformedValuesBarChart(data, dataSetIndex,
                mChart.getBarData(),
                mAnimator.getPhaseY());
    }

    /**
     * 检查是否为可显示的值
     * @return
     */
    protected boolean passesCheck() {
        return mChart.getBarData().getYValCount() < mChart.getMaxVisibleCount()
                * mViewPortHandler.getScaleX();
    }

    /**
     * 绘制其他
     * @param c
     */
    @Override
    public void drawExtras(Canvas c) { }
}

可以看到里面就是图表项和文本绘制的具体实现了,主要方法是drawvalues方法,大致就是一些通过一些方法计算每条bar的值,然后进行绘制。

总结

在这边简单讲解一下设计的方法。因为所有的Chart都继承了ViewGroup,实现了View的onMeasure,onDraw,onLayout,onSizeChanged方法,所以它是可以像自定义控件一样来使用。
View的绘制都再Render中实现,不同图表实现了不同的Render,继承结构大概如下:

  1. Render基类
    1. AxisRender(轴绘制)
    2. DataRender(图表绘制抽象类)
      1. CombinedChartRender(复合图表绘制,这个类是3.0版本添加的,可以展示折线图,柱状图,散点图等混合)
      2. BubbleChartRenderer(气泡图绘制)
      3. BarChartRender(柱状图绘制)
      4. LineScatterCandleRadarRenderer(折线图,散点图,烛状图,雷达图抽象类)
        1. LineRadarRenderer(折线图,雷达图抽象)
          1. LineChartRender(折线图绘制类)
          2. RadarChartRender(雷达图绘制类)
        2. ScatterChartRender(散点图绘制)
        3. CandleStickChartRenderer(烛状图绘制)
    3. LegendRender(图例绘制)

在不同的图表构造中初始化不同的将mRender对象初始化成不同的图表的Render对象,这里传入的参数有

  1. 不同图表的DataProvider(DataProvider是一个接口,实现了获取Y轴方向(左或右),和获取数据的方法)。
  2. ChartAnimator这是一个动画类,执行动画效果
  3. ViewPortHandler 图表信息类,包括边距,大小,转换等级(缩放)
    之后这些Render类就根据自己的实现在canvas上面绘制东西了。

其他补充

由于很好奇它的点击事件是怎么实现的,这边也看了一下它的点击事件。
每个图表写了自己的TouchListener
在构造中需要传入的参数有

  1. 图表大小的矩阵(用来计算缩放等级,还有当前点击事件位置)
  2. 图表对象

之后根据Touch事件判断相应的手势或者点击,触摸事件作出反应(点击,手势缩放,移动等)。调用View的postInvalidate 方法通知刷新。

By Xiaolong,每一天都值得被认真对待!

EventBus的使用和源码解析

发表于 2017-03-25 | 分类于 Android源码解析

为什么要使用EventBus

在Android开发过程中经常有这样的场景–>栈顶Activity需要关闭非栈顶的Activity,或调用其中的某些方法。

案例:我有三个Activity,主界面Activity,个人中心Activity,登录界面Activity,主界面打开了个人中心页面,个人中心页面有个退出登录的功能,点击后退出登录关闭主界面Activity和当前Activity,打开登录页面.

这只是个简单的案例,当然关闭当前Activity和打开登录页面的Activity都是没什么难度的东西。

我们来讨论主界面关闭的一些实现方案。

  1. 得到主界面Activity的对象。这边可能需要某个静态方法来持有这个对象,以前见过一种写法是写一个ActivityManager来持有这些Activity对象,然后手动管理这些Activity。

  2. 发送一个广播通知主界面Activity.

  3. 在当前Activity销毁时传一个resultCode,主界面看到这个resultCode来判断是否是否要做关闭操作。

暂时能想到的就这三种实现方案,分析一下三种实现:

第三种实现方案并不适用于比如我现在是主界面–>个人主页–>个人中心,做第三种方案的话代码写到哭晕在厕所,并且第三种方案代码写起来会很复杂,页面可能还没关掉就把自己绕晕了(个人想法,不推荐!)

第二种实现方案:发送一个广播做通知,其实是一个蛮不错的选择,发送广播然后实现广播类做回调通知。但是如果现在需求是对一个Fragment做通知。这个时候广播就派不上用场了。毕竟广播只能再Activity中注册发送广播。

第一种实现方案:这种方案在以前还是蛮常见的,以前学Android的时候会看到一些大牛们,自己写一个ActivityManager用来存放Activity对象做生命周期管理。但是这样做其实是会有问题的。因为持有的是Activity的对象,这就导致了Activity只能手动销毁。每次写关闭页面的时候都要调用这个方法来销毁Activity,不然分分钟就OOM了。还有另外一个缺点就是需要遍历整个栈来确定存活的Activity对象。找到你要找的Activity,然后调用其中的方法。如果是要销毁则还要将它移除栈中。(心很累)

想想这些方法真的都好难用呀。
这个时候就需要来个上帝来管理这些东西了。EventBus一脸害羞的跳出来了。一种全局的观察者模式,你可以把你的愿望(Event)请求post给这位上帝,它提供多线程的处理方案,来让你的(愿望)在你指定的地方实现。

EventBus使用

在Github上可以看到gradle所需配置

1
compile 'org.greenrobot:eventbus:3.0.0'

在gradle文件做上如上配置就可以了

分三个步骤

  1. 定义一个作为通知的实体类Event;
  2. 在需要订阅的地方做注册和取消注册,写一个订阅方法(方法需要写注解 @Subscribe(threadMode = ThreadMode.XXX))
  3. 在任意地方调用EventBus.getDefault().post(Event());

简单代码如下:
在MainActivity注册和解除注册,并写出订阅方法

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
package com.example.cncn.myapplication;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//注册一个EventBus
EventBus.getDefault().register(this);
findViewById(R.id.tvClickView).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, UserInfoActivity.class));
}
});
}
@Subscribe
public void onMainEvent(Event event) {
//打印对象的地址信息
Log.d("MainActivity", "EventInfo" + event.toString() + "_" + event.eventInfo);
finish();
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d("MainActivity","我被关掉了");
//如果已经注册,千万别忘了取消注册
if (EventBus.getDefault().isRegistered(this))
EventBus.getDefault().unregister(this);
}
}

在UserInfoActivity发送通知

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
package com.example.cncn.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import org.greenrobot.eventbus.EventBus;
/**
* Created by xiaolong on 2017/3/24.
* Email:[email protected]
*/
public class UserInfoActivity extends AppCompatActivity {
private TextView tvClickView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvClickView = (TextView) findViewById(R.id.tvClickView);
tvClickView.setText("点我退出登录!");
tvClickView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(UserInfoActivity.this, LoginActivity.class));
Event event = new Event(110, "关闭页面吧!!");
Log.d("UserInfoActivity", event.toString());
EventBus.getDefault().post(event);
finish();
}
});
}
}

LoginActivity

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
package com.example.cncn.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
/**
* Created by xiaolong on 2017/3/24.
* Email:[email protected]
*/
public class LoginActivity extends AppCompatActivity {
private TextView tvClickView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvClickView = (TextView) findViewById(R.id.tvClickView);
tvClickView.setText("点我登录!");
tvClickView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(LoginActivity.this,MainActivity.class));
}
});
}
}

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.cncn.myapplication;
/**
* Created by xiaolong on 2017/3/24.
* Email:[email protected]
*/
public class Event {
public int eventCode;
public String eventInfo;
public Event(int eventCode, String eventInfo) {
this.eventCode = eventCode;
this.eventInfo = eventInfo;
}
}

运行结果

打印接锅

如此简单便捷的使用方式,确实是做消息通知的不二选择。不过在注册过EventBus的类中,千万别忘了在结束的时候取消注册,不然会抛运行时异常。因为你注册了某个对象却不知道它什么时候被销毁了,回调方法的时候肯定会空针,这种回调解除注册在某些注册类的生命周期短,而维护类生命周期长的情况下经常能使用到~~这么好用的东西还是需要去了解一下其内部实现的。而且EventBus内部实现其实并不复杂。

EventBus源码解析

注册EventBus的时候需要执行,EventBus.getDefault().register(this);
我们先从getDefault方法入手,看下里面做了些什么

1
2
3
4
5
6
7
8
9
10
11
/** Convenience singleton for apps using a process-wide EventBus instance. */
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}

大致代码如下,单例模式获取一个EventBus对象,保证所有的EventBus对象是同一个。

进入 EventBus构造看一下

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
/**
* Creates a new EventBus instance; each instance is a separate scope in which events are delivered. To use a
* central bus, consider {@link #getDefault()}.
*/
public EventBus() {
this(DEFAULT_BUILDER);
}
EventBus(EventBusBuilder builder) {
subscriptionsByEventType = new HashMap<>();
typesBySubscriber = new HashMap<>();
stickyEvents = new ConcurrentHashMap<>();
mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
backgroundPoster = new BackgroundPoster(this);
asyncPoster = new AsyncPoster(this);
indexCount = builder.subscriberInfoIndexes != null ? builder.subscriberInfoIndexes.size() : 0;
subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes,
builder.strictMethodVerification, builder.ignoreGeneratedIndex);
logSubscriberExceptions = builder.logSubscriberExceptions;
logNoSubscriberMessages = builder.logNoSubscriberMessages;
sendSubscriberExceptionEvent = builder.sendSubscriberExceptionEvent;
sendNoSubscriberEvent = builder.sendNoSubscriberEvent;
throwSubscriberException = builder.throwSubscriberException;
eventInheritance = builder.eventInheritance;
executorService = builder.executorService;
}

两个构造方法,一个是无参的public的方法,另外一个是内部方法,带参数的。
第一个方法默认调用有参数的构造方法,来生成一个EventBus对象。
这边DEFAULT_BUILDER声明如下

1
private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();

存放着一个默认的EventBusBuilder对象;
进入EventBusBuilder中看

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package org.greenrobot.eventbus;
import org.greenrobot.eventbus.meta.SubscriberInfoIndex;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Creates EventBus instances with custom parameters and also allows to install a custom default EventBus instance.
* Create a new builder using {@link EventBus#builder()}.
*/
public class EventBusBuilder {
private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
boolean logSubscriberExceptions = true;
boolean logNoSubscriberMessages = true;
boolean sendSubscriberExceptionEvent = true;
boolean sendNoSubscriberEvent = true;
boolean throwSubscriberException;
boolean eventInheritance = true;
boolean ignoreGeneratedIndex;
boolean strictMethodVerification;
ExecutorService executorService = DEFAULT_EXECUTOR_SERVICE;
List<Class<?>> skipMethodVerificationForClasses;
List<SubscriberInfoIndex> subscriberInfoIndexes;
EventBusBuilder() {
}
/** Default: true */
public EventBusBuilder logSubscriberExceptions(boolean logSubscriberExceptions) {
this.logSubscriberExceptions = logSubscriberExceptions;
return this;
}
/** Default: true */
public EventBusBuilder logNoSubscriberMessages(boolean logNoSubscriberMessages) {
this.logNoSubscriberMessages = logNoSubscriberMessages;
return this;
}
/** Default: true */
public EventBusBuilder sendSubscriberExceptionEvent(boolean sendSubscriberExceptionEvent) {
this.sendSubscriberExceptionEvent = sendSubscriberExceptionEvent;
return this;
}
/** Default: true */
public EventBusBuilder sendNoSubscriberEvent(boolean sendNoSubscriberEvent) {
this.sendNoSubscriberEvent = sendNoSubscriberEvent;
return this;
}
/**
* Fails if an subscriber throws an exception (default: false).
* <p/>
* Tip: Use this with BuildConfig.DEBUG to let the app crash in DEBUG mode (only). This way, you won't miss
* exceptions during development.
*/
public EventBusBuilder throwSubscriberException(boolean throwSubscriberException) {
this.throwSubscriberException = throwSubscriberException;
return this;
}
/**
* By default, EventBus considers the event class hierarchy (subscribers to super classes will be notified).
* Switching this feature off will improve posting of events. For simple event classes extending Object directly,
* we measured a speed up of 20% for event posting. For more complex event hierarchies, the speed up should be
* >20%.
* <p/>
* However, keep in mind that event posting usually consumes just a small proportion of CPU time inside an app,
* unless it is posting at high rates, e.g. hundreds/thousands of events per second.
*/
public EventBusBuilder eventInheritance(boolean eventInheritance) {
this.eventInheritance = eventInheritance;
return this;
}
/**
* Provide a custom thread pool to EventBus used for async and background event delivery. This is an advanced
* setting to that can break things: ensure the given ExecutorService won't get stuck to avoid undefined behavior.
*/
public EventBusBuilder executorService(ExecutorService executorService) {
this.executorService = executorService;
return this;
}
/**
* Method name verification is done for methods starting with onEvent to avoid typos; using this method you can
* exclude subscriber classes from this check. Also disables checks for method modifiers (public, not static nor
* abstract).
*/
public EventBusBuilder skipMethodVerificationFor(Class<?> clazz) {
if (skipMethodVerificationForClasses == null) {
skipMethodVerificationForClasses = new ArrayList<>();
}
skipMethodVerificationForClasses.add(clazz);
return this;
}
/** Forces the use of reflection even if there's a generated index (default: false). */
public EventBusBuilder ignoreGeneratedIndex(boolean ignoreGeneratedIndex) {
this.ignoreGeneratedIndex = ignoreGeneratedIndex;
return this;
}
/** Enables strict method verification (default: false). */
public EventBusBuilder strictMethodVerification(boolean strictMethodVerification) {
this.strictMethodVerification = strictMethodVerification;
return this;
}
/** Adds an index generated by EventBus' annotation preprocessor. */
public EventBusBuilder addIndex(SubscriberInfoIndex index) {
if(subscriberInfoIndexes == null) {
subscriberInfoIndexes = new ArrayList<>();
}
subscriberInfoIndexes.add(index);
return this;
}
/**
* Installs the default EventBus returned by {@link EventBus#getDefault()} using this builders' values. Must be
* done only once before the first usage of the default EventBus.
*
* @throws EventBusException if there's already a default EventBus instance in place
*/
public EventBus installDefaultEventBus() {
synchronized (EventBus.class) {
if (EventBus.defaultInstance != null) {
throw new EventBusException("Default instance already exists." +
" It may be only set once before it's used the first time to ensure consistent behavior.");
}
EventBus.defaultInstance = build();
return EventBus.defaultInstance;
}
}
/** Builds an EventBus based on the current configuration. */
public EventBus build() {
return new EventBus(this);
}
}

里面是一些EventBus的基础配置。打印异常信息,发送未被订阅的消息等。
特别需要注意的是 ignoreGeneratedIndex 这个变量是用来确定EventBus用什么方式来获取订阅方法。
true代表的是不优先使用索引,用反射的方式,false代表的是优先使用索引。默认是false。
这边有个addIndex方法是用来添加订阅方法索引的,这是3.0版本的新特性。
在EventBus带参数的构造函数里面初始化了 SubscriberMethodFinder 这个类,三个参数分别为索引列表,
是否严格方法定义,是否无视索引。

getDefault方法总结,该方法返回一个单例的EventBus对象,如果对象没被创建就创建一个默认的EventBus对象。

接下来从注册方法下手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Registers the given subscriber to receive events. Subscribers must call {@link #unregister(Object)} once they
* are no longer interested in receiving events.
* <p/>
* Subscribers have event handling methods that must be annotated by {@link Subscribe}.
* The {@link Subscribe} annotation also allows configuration like {@link
* ThreadMode} and priority.
*/
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

首先是通过一个findSubscriberMethods方法找到了一个订阅者中的所有订阅方法,返回一个 List,然后之后遍历所有的订阅方法,做订阅操作。进入到findSubscriberMethods看看如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}
if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
subscriberMethods = findUsingInfo(subscriberClass);
}
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass
+ " and its super classes have no public methods with the @Subscribe annotation");
} else {
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}

findSubscriberMethods这个方法声明SubscriberMethodFinder这个类中,类中声明了一个ConcurrentHashMap用来做SubscriberMethod的缓存。

回到findSubscriberMethods中,首先是通过类来当key,查找在当前缓存中是否存在这个类的订阅方法列表。有就直接return 缓存中的列表。

没有就进入下一步,这边有两种方式来获取类中的订阅方法通过ignoreGeneratedIndex来确定获取订阅方法的方式为true时会使用反射方法获取订阅者的事件处理函数,为false时会优先使用subscriber Index(订阅方法索引)生成的SubscriberInfo来获取订阅者的事件处理函数(减少反射耗时),默认false。(使用订阅方法索引需自己构建一个EventBus对象,将在后面提及,目前的使用并不能用到索引)

进入findUsingInfo这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
FindState findState = prepareFindState();//这个方法用来从对象池中获取一个FindState对象,如果对象池都没有可用对象的话新建一个。用来节省创建对象的花销
findState.initForSubscriber(subscriberClass);
while (findState.clazz != null) {
findState.subscriberInfo = getSubscriberInfo(findState);
if (findState.subscriberInfo != null) {
SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
for (SubscriberMethod subscriberMethod : array) {
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}
} else {
findUsingReflectionInSingleClass(findState);
}
findState.moveToSuperclass();
}
return getMethodsAndRelease(findState);
}
  1. 从FindState池——FIND_STATE_POOL中获取一个FindState对象。(SubscriberMethodFinder这个类中声明了一个FIND_STATE_POOL的对象池,这个用来存放空(并非NULL)的FIND_STATE对象,减少创建对象的花销)
  2. 该方法会将findState.clazz域中使用了@subscribe标注、方法中只有一个参数、且方法修饰符为public的方法,创建一个SubscriberMethod对象,并添加到findState的List集合中。
  3. 将findState.clazz域更新为clazz = clazz.getSuperclass(); 如果该超类名字以java. javax. android.开头则clazz变成null;不再往上寻找父类;
  4. 拷贝一份findState的List集合并返回,最后回收findState对象,回收的只是释放这个对象内部的变量的资源占用,但这个对象还是存在的,放回对象池中,下次可以再取出来用;

看一下如何用反射获取订阅方法。

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
private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass);
while (findState.clazz != null) {
findUsingReflectionInSingleClass(findState);
findState.moveToSuperclass();
}
return getMethodsAndRelease(findState);
}
private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
methods = findState.clazz.getMethods();
findState.skipSuperClasses = true;
}
for (Method method : methods) {
int modifiers = method.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class<?> eventType = parameterTypes[0];
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException("@Subscribe method " + methodName +
"must have exactly 1 parameter but has " + parameterTypes.length);
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException(methodName +
" is a illegal @Subscribe method: must be public, non-static, and non-abstract");
}
}
}
  1. 获取订阅类声明的所有方法; 然后对获取到的方法全部遍历一遍
  2. 获取方法的修饰符:即方法前面的public、private等关键字。
  3. 如果该类方法使用了@subscribe标注、方法中只有一个参数、且方法修饰符为public。findState.checkAdd(method, eventType) 如果之前没有存在过则返回true
  4. 判断@Subscribe标注中的threadMode对应的值,默认模式ThreadMode.POSTING
  5. 创建一个SubscriberMethod对象,该对象很简单就是保存有方法、方法参数类型、线程模式、订阅的优先级、sticky标志位。与Retrofit类似只是这里创建了一个SubscriberMethod对象。并将该对象添加到FindSate的List集合中。

回到注册方法,来看一下subscribe方法的实现

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
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
Class<?> eventType = subscriberMethod.eventType; //note1
Subscription newSubscription = new Subscription(subscriber, subscriberMethod); //note2
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType); //note3
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType);
}
}
int size = subscriptions.size(); //note4
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}
List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber); //note5
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
subscribedEvents.add(eventType);
if (subscriberMethod.sticky) { //note6
if (eventInheritance) {
Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
for (Map.Entry<Class<?>, Object> entry : entries) {
Class<?> candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) { //从stickyEvents获取对应的事件交给当前事件订阅者处理
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent); //该方法底层还是会执行postToSubscription方法
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}
  1. 获取方法参数类型;注意:使用@Subscribe标注的方法有且仅有一个参数
  2. 利用订阅者对象及其事件处理方法构建一个Subscription对象,该对象存储有Object、SubscriberMethod对象
  3. 从subscriptionsByEventType集合中获取当前事件对应的Subscription对象集合; 如果得到的集合为空则创建一个这样的集合,并将刚创建的Subscription对象添加进subscriptionsByEventType集合中;如果得到的集合不为空且刚创建的Subscription对象已经存在该集合中则抛出异常,即同一个对象不能注册两次!
  4. 将第二步创建的Subscription对象按照优先级存入Subscription对象集合中,该集合中的元素都是按照优先级从高到低存放.
  5. 以subscriber对象为键,从typesBySubscriber获取该对象对应的接收事件类型集合,没有则创建一个这样的集合,然后当前事件类型添加到该集合中,最后将整个集合添加进typesBySubscriber集合中;有则直接添加到接收事件类型集合中;
  6. 该值默认为false,除非在注册事件方法时使用了如下的标注@Subscribe(sticky = true);那么就会执行到这里。stickyEvent也是EventBus3.0的一大特点,该类事件一旦发送给EventBus,那么EventBus就会将它存入Map, Object> stickyEvents集合中,key事件类型,value事件实例;每个类型事件对应最新的实例。当订阅对象的某个事件处理方法使用了@Subscribe(sticky = true)标注,EventBus一旦对订阅者完成了注册任务之后,立即将stickyEvents集合中的匹配事件发送给该事件处理进行处理!

接下来看post方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void post(Object event) {
PostingThreadState postingState = currentPostingThreadState.get(); //note1
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event); //note2
if (!postingState.isPosting) {
postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
postingState.isPosting = true;
if (postingState.canceled) { throw new EventBusException("Internal error. Abort state was not reset"); }
try {
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);//note3
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}
  1. 获取当前线程对应的一个PostingThreadState()对象;回顾一下我们在EventBus中创建的如下域
1
2
3
private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
@Override protected PostingThreadState initialValue() { return new PostingThreadState(); } //当前推送线程状态
};

ThreadLocal类型的特点是当调用currentPostingThreadState.get()方法的时候,会返回当前线程所持有的一个 PostingThreadState对象;在不同的线程中执行同样一行代码它们得到的对象是不同的。PostingThreadState也很简单,就是定义了一堆数据,没有任何方法。下面就是它的所有源码

1
2
3
4
5
6
7
8
final static class PostingThreadState {
final List<Object> eventQueue = new ArrayList<Object>(); //待派送的事件队列
boolean isPosting; //当前PostingThreadState对象是否正在派送事件的标志位
boolean isMainThread; //当前PostingThreadState对象是否是工作在UI线程的标志位
Subscription subscription; //事件处理器
Object event; //待处理事件
boolean canceled; //是否取消事件派送的标志位
}
  1. 向PostingThreadState的事件队列中添加一个事件
  2. 从PostingThreadState的事件队列——eventQueue中移出一个事件,并调用postSingleEvent方法进行派送

postSingleEvent方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class<?> eventClass = event.getClass();
boolean subscriptionFound = false;
if (eventInheritance) {
List<Class<?>> eventTypes = lookupAllEventTypes(eventClass); //note2
int countTypes = eventTypes.size();
for (int h = 0; h < countTypes; h++) { //note3
Class<?> clazz = eventTypes.get(h);
subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
}
} else {
subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
}
if (!subscriptionFound) { //note4
if (logNoSubscriberMessages) {
Log.d(TAG, "No subscribers registered for event " + eventClass);
}
if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&eventClass != SubscriberExceptionEvent.class) {
post(new NoSubscriberEvent(this, event));
}
}
  1. eventInheritance该标志位默认为true,表示只要 满足订阅事件是该事件的父类或者实现了该事件同样接口或接口父类 中的任何一个条件的订阅者都会来处理该事件。
  2. 该方法从名字来看就是获取eventClass的所有父类往上都能追溯到Object、和所有实现的接口、以及接口父类;EventBus进行了优化采用了缓存机制Map, List>> eventTypesCache。
  3. 将eventClass所有的关联类都通过postSingleEventForEventType进行推送;下面跟着就会分析该方法
  4. 如果该事件没有推送成功,即没有事件处理器来处理这类事件;系统会尝试发送一个post(new NoSubscriberEvent(this, event))事件

postSingleEventForEventType方法

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
private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
//第一个是带处理的原始事件,第三个参数是原始事件的关联类
CopyOnWriteArrayList<Subscription> subscriptions;
synchronized (this) {
subscriptions = subscriptionsByEventType.get(eventClass);//note1
}
if (subscriptions != null && !subscriptions.isEmpty()) {
for (Subscription subscription : subscriptions) {
postingState.event = event;
postingState.subscription = subscription;
boolean aborted = false;
try {
postToSubscription(subscription, event, postingState.isMainThread); //note2
aborted = postingState.canceled;
} finally {
postingState.event = null;
postingState.subscription = null;
postingState.canceled = false;
}
if (aborted) {
break;
}
}
return true;
}
return false;
}
  1. 获取原始事件的关联类对应的所有Subscription对象
  2. 将上述Subscription对象集合进行遍历,使用postToSubscription方法处理原始事件

postToSubscription方法

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
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
//第一个参数是事件处理器、第二个参数是待处理事件、第三个为当前线程是否是UI线程的标志位
switch (subscription.subscriberMethod.threadMode) {
case POSTING: //note1
invokeSubscriber(subscription, event);
break;
case MAIN: //note2
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case BACKGROUND: //note3
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC: //note4
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}
  1. POSTING直接在当前线程执行,调用invokeSubscriber方法
  2. MAIN即UI线程执行:如果当前线程就是UI线程则直接调用invokeSubscriber方法,否则将任务交给mainThreadPoster——HandlerPoster(this, Looper.getMainLooper(), 10)对象异步执行,最终会调用invokeSubscriber(PendingPost pendingPost)方法;
  3. BACKGROUND背景线程执行:其实就在非UI线程中执行,如果当前线程是非UI线程则直接调用invokeSubscriber方法,否则将任务交给backgroundPoster——BackgroundPoster(this)对象异步执行,最终会调用invokeSubscriber(PendingPost pendingPost)方法;
  4. ASYNC:将任务交给 asyncPoster—— AsyncPoster(this)对象异步执行,最终会调用invokeSubscriber(PendingPost pendingPost)方法;
  5. 就是根据不同的Post模式,选择不同的方式反射执行方法。

接下来看取消注册的方法

1
2
3
4
5
6
7
8
9
10
11
public synchronized void unregister(Object subscriber) {
List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);//note1
if (subscribedTypes != null) {
for (Class<?> eventType : subscribedTypes) {
unsubscribeByEventType(subscriber, eventType); //
}
typesBySubscriber.remove(subscriber); //note3
} else {
Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}
}
  1. 从typesBySubscriber获取该对象接收的事件类型集合;
  2. 对得到的接收事件类型集合中的每个事件类型调用unsubscribeByEventType进行处理;跟着我们就分析该方法
  3. 该对象从typesBySubscriber集合中移除;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
List<Subscription> subscriptions = subscriptionsByEventType.get(eventType); //note1
if (subscriptions != null) {
int size = subscriptions.size();
for (int i = 0; i < size; i++) {
Subscription subscription = subscriptions.get(i);
if (subscription.subscriber == subscriber) { //note2
subscription.active = false;
subscriptions.remove(i);
i--;
size--;
}
}
}
}

1、从subscriptionsByEventType集合中获取该事件类型对应的Subscription集合
2、如果集合中的元素——Subscription的subscriber域等于目标subscriber,则将该Subscription从subscriptionsByEventType集合中移除出去;

源码部分就讲解到这里
由于对注解不是很了解,所以在通过索引获取订阅方法的时候我也是一脸蒙逼,因为索引列表是在构造函数中生成的。但是addIndex从来没被调用过。我就很好奇索引列表是怎么来的,后面才明白是另外有方法来添加订阅者索引列表,下面我们来看一下如何使用索引。

索引的好处

EventBus提供了一个扩展包 EventBusAnnotationProcessor 这个方法可以在编译的时候通过注解,自动生成订阅方法索引列表的类。由于是在编译阶段就执行的,这样做可以避免反射获取所需的耗时。缺点可能是会占用一小部分内存。

如何使用索引

使用索引需要在项目gradle下添加

1
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

如下

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
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

然后在app的gradle中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//apply 需添加
apply plugin: 'com.neenbedankt.android-apt'
//android中需添加
android {
apt{
arguments{
//路径加类名,用来确定存放的位置,可以自己定义
eventBusIndex "org.greenrobot.eventbus.EventBusTestIndex"
}
}
}
//dependencies需添加
dependencies {
provided 'org.greenrobot:eventbus-annotation-processor:3.0.1'
}

其中eventBusIndex 是路径加类名,用来确定存放的位置,可以自己定义。
配置完之后先Rebuild一把
这里写图片描述
可以看到在debug目录下生成了这个EventBusTestIndex这个类。

类中方法如下

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
package org.greenrobot.eventbus;
import org.greenrobot.eventbus.meta.SimpleSubscriberInfo;
import org.greenrobot.eventbus.meta.SubscriberMethodInfo;
import org.greenrobot.eventbus.meta.SubscriberInfo;
import org.greenrobot.eventbus.meta.SubscriberInfoIndex;
import org.greenrobot.eventbus.ThreadMode;
import java.util.HashMap;
import java.util.Map;
/** This class is generated by EventBus, do not edit. */
public class EventBusTestIndex implements SubscriberInfoIndex {
private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
static {
SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();
putIndex(new SimpleSubscriberInfo(com.cncn.tourenforce.base.BaseFuncActivity.class, true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("logoutEvent", com.cncn.tourenforce.bean.event.LogoutEvent.class),
}));
putIndex(new SimpleSubscriberInfo(com.cncn.tourenforce.ui.mine.AboutActivity.class, true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("downLoadSuccess", com.cncn.tourenforce.bean.event.UpdateVersionEvent.class),
}));
putIndex(new SimpleSubscriberInfo(com.cncn.tourenforce.ui.check.CheckReasonEditActivity.class, true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("checkSuccess", com.cncn.tourenforce.bean.event.CheckSuccessEvent.class),
}));
putIndex(new SimpleSubscriberInfo(com.cncn.tourenforce.ui.check.SignListFragment.class, true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("resetList", com.cncn.tourenforce.bean.event.SignSelectResetEvent.class),
}));
}
private static void putIndex(SubscriberInfo info) {
SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
}
@Override
public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
if (info != null) {
return info;
} else {
return null;
}
}
}

就是一个静态的Map对象用来存放所有订阅方法字典。然后提供一个get方法用来获取这个字典中的对象。

然后就是使用了

1
EventBus.builder().addIndex(subscriberClass -> new EventBusTestIndex().getSubscriberInfo(subscriberClass)).installDefaultEventBus();

这边再贴一遍installDefaultEventBus这个方法的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Installs the default EventBus returned by {@link EventBus#getDefault()} using this builders' values. Must be
* done only once before the first usage of the default EventBus.
*
* @throws EventBusException if there's already a default EventBus instance in place
*/
public EventBus installDefaultEventBus() {
synchronized (EventBus.class) {
if (EventBus.defaultInstance != null) {
throw new EventBusException("Default instance already exists." +
" It may be only set once before it's used the first time to ensure consistent behavior.");
}
EventBus.defaultInstance = build();
return EventBus.defaultInstance;
}
}

可以看出这个方法用来自定义一个EventBusBuider对象配置到EventBus中去,并替换掉defaultInstance。
注意:这里需要在Default EventBus 对象还没生成的时候执行这句话。如果已经有默认的EventBus对象存在了再installDefaultEventBus会报错的。

所以我们需要在APP启动的时候把这个方法加进去,新建一个App类继承Application

1
2
3
4
5
6
7
8
9
10
11
12
13
import android.app.Application;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.EventBusTestIndex;
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
EventBus.builder().addIndex(subscriberClass -> new EventBusTestIndex().getSubscriberInfo(subscriberClass)).installDefaultEventBus();
}
}

AndroidManifest中别忘了把Application改成当前App

1
2
3
4
5
6
7
<application
android:name=".base.App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/CustomAppTheme">
</application>

之后的方法获取就都是通过索引获取了。

总结

没有精确去分析所有的源码。这边大概做一下总结,整个流程大概如下。
1. 注册EventBus,EventBus会获取当前类中的订阅方法,包括方法的参数,类型,优先级等信息。在获取的方法中,经常会有缓存。如果在缓存中没有才去调用获取方法。获取订阅方法有两种方式,一种是反射,反射获取全部方法,找到加了@Subscribe注解并有一个参数的方法,另外一种是通过订阅方法索引来得到订阅方法。反射的效率较低,所以一般都是用订阅方法索引来获取,有些情况下找不到这个方法。它还是会走反射的途径。注册过后这个订阅方法就被存起来了。等待接受消息。(这边值得说一下在3.0以前是没有注解方法的那时候都是通过反射来获取和执行方法。而且方法必须以onEvent开头,分别为onEventMainThead,onEventBackground,onEvent,onEventAsync)
3. postEvent,会把这个Event加入消息队列,然后通过Event的类型信息来得到它的订阅方法,然后根据相应的订阅方式反射调用订阅方法。没有选择的Post一般都会在UI线程执行。如果当前post不是UI线程这边会用Handle的机制来让方法运行在UI线程。
4. 解除注册,将该对象从对象缓存列表中移除,获取当前对象的订阅列表,然后将其从订阅列表移除

By Xiaolong,每一天都值得被认真对待!

1234
小龙

小龙

程序猿

36 日志
14 分类
23 标签
GitHub CSDN
© 2019 小龙
由 Hexo 强力驱动
主题 - NexT.Pisces