reoger的记录

--以后的你会感激现在那么努力的自己

0%

HDFS(Hadoop Distributed File System)是Hadoop分布式计算中的数据存储系统,是基于流数据模式访问和处理超大文件的需求而开发的。

基本概念

下面介绍几个hdfs文件系统中几个常见的概念:

  1. Block
    HDFS中的存储单元是每个数据块block,HDFS默认的最基本的存储单位是64M的数据块。
  2. NameNode
    元数据节点,该节点用来管理文件系统中的命名空间。
  3. DataNode
    数据节点,是HDFS真正存储数据的地方。
  4. Secondary NameNode
    从元数据节点,从元数据节点并不是NameNode出现问题时候的备用节点,它的主要功能是周期性的将NameNode中的namespace image和edit log合并,以防log文件过大。
  5. edit log
    修改日志,用于记录hdfs中文件的相关操作日志。

命令操作

我们可以通过hadoop命令来实现文件的基本操作,包括添创建、删除目录,上传下载文件操作。

创建目录

我们可以通过下面的命令来创建一个input目录。
hadoop fs -mkdir hdfs://localhost:9000/input
这里的hdfs://localhost:9000表示的是本地的hdfs系统,如果是本地的hdsf系统,我们其实可以省略,我们上面的命令可以简写成:
hadoop fs -mkdir /input
当然,如果是远程的hadfs系统,我们就需要标注hdfs地址了,例如hdfs://http://172.22.66.245:9099,想知道hdfs开放的是那个端口,直接直接访问http://localhost:50070/dfshealth.html#tab-overview查看开放端口。
综上,我们在远程hdfs系统上创建的一个名为input目录的命令如下:
hadoop fs -mkdir hdfs://172.22.66.245:9090/input

上传文件

上传文件的也很简单,基本的格式为hadoop fs -put localFile hdfsFileDir
其中的localFile代表要上传的文件,hdfsFileDir代表要上传的到hdfs文件系统的目录。
例如,我要将D盘根目录下的keseh.txt上传到hdsf文件系统的input目录下,完整的命令如下:
hadoop fs -put D:\keshe.txt hdfs://localhost:9000/input
当然,因为是本地的hdfs系统,故hdfs可以省略,即可以简写成:
hadoop fs -put D:\keshe.txt /input
当然远程hdfs系统,这个hdfs就不能省略了,示例命令如下:
hadoop fs -put D:\keshe.txt hdfs://172.22.66.245:9090/input

下载文件

下载文件的基本格式为hadoop fs -get hdfsFile localFileDir,其中的hdfsFile表示的是hdfs系统中的文件,localFileDir表示要下载的到本地的目录。
这里就直接给出示例命令了.
从本地的hadfs系统获取,hadoop fs -get hdfs://localhost:9000/input/keshe.txt F:\

远程的hdfs系统获取:hadoop fs -get hdfs://172.22.66.245:9090/input/keshe.txt F:\

删除文件

删除文件的格式为hadoop fs -rm hdfsFile或者是hadoop fs -rm -r hdfsFile
每次可以删除多个文件或者目录。例如,删除远程hdfs文件的命令为:
hadoop fs -rm hdfs://172.22.66.245:9090/input/keshe.txt

基本命令

以上的操作都比较简单,在简单记录一下hadoop中的基本命令。

命令 格式 作用 实例
ls hadoop fs -ls /< hdfs dir> 列出hadfs文件系统中某个目录下的文件 hadoop fs -ls hdfs://localhost:9000/input
put hadoop fs -put < local file > < hdfs dir > 上传文件或者目录到hdfs系统中指定目录中 hadoop fs -put D:\keshe.txt hdfs://localhost:9000/input
get hadoop fs -get < hdfs file > < local file or dir> 下载hdfs文件系统中的文件或者目录 hadoop fs -get hdfsFile localFileDir
moveFromLocal hadoop fs -moveFromLocal < local src > … < hdfs dst > 类似于将本地文件剪切到hdfs文件系统中 hadoop fs -moveFromLocal D:\keshe.txt hdfs://localhost:9000/input
copyFromLocal hadoop fs -copyFromLocal < local src > … < hdfs dst > 类似于将文件从本地复制到hdfs文件系统中 hadoop fs -copyFromLocal D:\keshe.txt hdfs://localhost:9000/input
mkdir hadoop fs -mkdir < hdfs path> 在hdfs文件系统中创建目录 hadoop fs -mkdir hdfs://localhost:9000/input
rm hadoop fs -rm < hdfs file > … 删除hdfs文件目录中的文件 hadoop fs -rm hdfs://localhost:9000/input/keshe.txt
cp hadoop fs -cp < hdfs file > < hdfs file > 复制hdfs文件或者福文件夹 hadoop fs -cp hdfs://localhost:9000/input/keshe.txt hdfs://localhost:9000/input/user
mv hadoop fs -mv < hdfs file > < hdfs file > 移动文件,也可以当着重命名来使用 hadoop fs -cp hdfs://localhost:9000/input/keshe.txt hdfs://localhost:9000/haha.txt
count hadoop fs -count < hdfs path > 统计hdfs对应路径下的相关信息,显示为目录个数,文件个数,文件总计大小,输入路径 hadoop fs -count hdfs://localhost:9000/input
du hadoop fs -du < hdsf path> 显示hdfs对应路径下每个文件夹和文件的大小 hadoop fs -du hdfs://localhost:9000/input
text hadoop fs -text < hdsf file> 将文本文件或某些格式的非文本文件通过文本格式输出 hadoop fs -text hdfs://localhost:9000/input/haha.txt

暂时先记录这么多了,下面我们用代码实现基本的文件操作。

用代码实现

在真是实现之前,是需要我们提前好代码开发环境的,至于如何搭建,参照前面上一篇博客,这里不再展开。

查看所有目录下的文件

我们先来看如何查看hdfs文件指定目录下所有的文件信息,即相当于ls命令的。
下面就是主要实现,我们住需要在main方法中调用就可以打印出hdfs文件系统中input目录下所有的文件和目录信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void  ListItem(){
try {
Configuration config = new Configuration();
Path dfs = new Path("hdfs://172.22.66.245:9090/input");
FileSystem fileSystem = dfs.getFileSystem(config);
FileStatus[] status = fileSystem.listStatus(dfs);
for (int i = 0; i < status.length; i++) {
System.out.println(status[i].getPath().toString());
}
} catch (IOException e) {
e.printStackTrace();
}
}

上传文件

代码很简单,直接上代码了,upFile方法实现了将本地D盘根目录下的2017-09-25_093127.jpg图片上传到hdfs文件系统中的input文件夹下。

1
2
3
4
5
6
7
8
9
10
11
private static void upFile(){
try{
Configuration conf = new Configuration();
Path path = new Path("hdfs://172.22.66.245:9090/input");
Path localPath = new Path("D:\\2017-09-25_093127.jpg");
FileSystem fs = path.getFileSystem(conf);
fs.copyFromLocalFile(localPath,path);
} catch (IOException e) {
e.printStackTrace();
}
}

下载文件

下载文件比较坑的一点就是,我们需要先创建一个这样的文件,然后以流的形式传入到填充到这个文件中去,例如下面的示例代码就是将hdfs文件系统中的haha.txt文件的内容写到D盘根目录下的hello.txt文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void downFile(){
try{
Configuration conf = new Configuration();
Path path = new Path("hdfs://172.22.66.245:9090/input/haha.txt");
FileSystem fs = path.getFileSystem(conf);
FSDataInputStream open = fs.open(path);
OutputStream output = new FileOutputStream("D://hello.txt");
IOUtils.copyBytes(open,output,4096,true);

} catch (IOException e) {
e.printStackTrace();
}
}

参考链接

  1. hadoop HDFS常用文件操作命令
  2. HDFS中的基础概念

在参考了众多的参考资料之后,终于搭建搭建起了了intellij-IDEA开发hadoop环境,下面就记录一下开发环境的建立过程,以免日后遗忘。

开发工具和基础环境准备

首先,本次采用的开发工具选用的是intellij-IDEA,在进行本次开发之前需要将intellij-IDEA下载并安装好,并配置好java的JDK。下面是我本次配置开发环境之前的基础环境,本次采用的是hadoop 2.6.0的版本。

1
2
3
4
5
IntelliJ IDEA 2017.2.4
Build #IC-172.4155.36, built on September 12, 2017
JRE: 1.8.0_152-release-915-b11 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0

在下载好hadoop之后,我们便可以开始本次环境的配置了。

创建项目

这一步非常的简单,我们用IDEA创建一下普通的java项目即可。


创建项目完成之后是这个样子的。

添加示例代码

我们先不管三七二十一,在我们的项目中,先copy下面三个类,用来检测环境是否搭建好。
第一个类WordCountMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;
import java.util.StringTokenizer;

public class WordCountMapper extends Mapper<LongWritable,Text,Text,IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();

@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()){
word.set(itr.nextToken());
context.write(word,one);
}
}
}

第二个类:WordCountReducer

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

import org.apache.hadoop.io.IntWritable;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;


import java.io.IOException;


public class WordCountReducer extends Reducer<Text,IntWritable,Text,IntWritable> {

private IntWritable result = new IntWritable();

@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for(IntWritable val:values){
sum += val.get();
}

result.set(sum);
context.write(key,result);
}
}

第三个测试类:test

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
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.log4j.chainsaw.Main;

import java.io.IOException;

public class test {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// write your code here

Configuration configuration = new Configuration();

if(args.length!=2){
System.err.println("Usage:wordcount <input><output>");
System.exit(2);
}

Job job = new Job(configuration,"word count");

job.setJarByClass(Main.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));

System.exit(job.waitForCompletion(true)?0:1);
}
}

把代码copy完成之后,项目结构应该是这样子的:

可以看到,ide提示我们代码还有错,这是因为我们环境还没有搭建完成,所以报错,我们接下来继续下一步。

导入hadoop相关的jar包

我们首先需要知道我们要导入的jar包的具体位置: \hadoop-2.6.0\share\hadoop,即在hadoop根目录下的share文件夹中的hadoop文件夹中。
然后在IDEA中打开project Structure选项,快捷键ctrl+alt+shift+S。选择modules,如图:

选择右边的加号-> jars 如图:

选择hadoop的jar,具体位置我们前面已经说过了,将hadoop文件夹中所有的文件都选中,按住shift即可多选:

jar导入完成之后,应该是这个样子的:

然后在Project Settings中选择Artifacts,如图:

选择加号->JAR -> Emptry创建一个空的jar。选择完成后如图:


我们将其取名为hadoop,并选择output Layout下面的加号->Module Output,并勾选include in prokecy builde,完成后如图所示:

完成上面的操作后,发现我们前面copy的代码已经不在报错了,说明jar包已经成功导入到我们的项目中来了。最后我们需要配置我们的运行环境。

配置运行环境

首先选择edit Configurtions,如图;

新建application,具体做法,点击右上角的加号->选择application。新建界面如图所示:


我们需要填的只有三处,首先我们可以为他取个名字,在Name那么输入自定义的名义,然后再Main class那里填写:org.apache.hadoop.util.RunJar,选择progream arguments右边的展开按钮,填写此项的值,在这里需要填写三个参数,第一个就是我们前面创建jar的具体位置,我们可以在前面常见的artifacts中查看,示例D:\intellijIDEA\hadoopTest\out\artifacts\hadoop\hadoop.jar;第二个参数是我们要运行类的名字(需要加上包名)示例com.hut.Test;第三个是输入文件的位置,示例input/;第四个是输入文件的名字示例``output/;
参考配置如下:

1
2
3
4
D:\intellijIDEA\hadoopTest\out\artifacts\hadoop\hadoop.jar
com.hut.Test
input/
output/

这里需要根据自己项目的实际情况来进行配置,仅供参考。

运行检测

根据我们前面的配置,我们需要在项目的根目录下新建一个input文件夹。我们可以在这个文件夹中添加一个txt的文件,并随意的写入一些数据。
然后点击运行,成功运行后,会发现我们项目根目录下新添加了一个output文件夹,在part-r-0000文件中,就记录了我们在input文件夹中文件单词出现的次数。如图:

值得注意的是,每次成功运行,都需要将output文件夹删除后才能继续下一次的运行。

到此,我们的环境已经配置好了,接下来就可以开开心的学习hadoop的api使用了。

参考链接

不知不觉,四年的下来,还是收集整理了不少的网站,在此统一整理一下,以免以后遗忘。

UI素材类

在这里记录的是比较常用的图片网站

精美图片

以下网站是我认为比较好的图片资源网站,更多的图片资源网站,可以参考知乎的讨论。下面就介绍几个我认为比较好的图片素材网站(更多的是免费)

Pixabay.com

Pixabay有可在任何地方使用的免费图片和视频

https://pixabay.com/

Pixabay是一个充满活力的创意社区,分享免费的图片和视频。所有的内容都是在Creative Commons CC0下发布的,这使得它们可以安全的使用,而无需为创作者署名——即使是出于商业目的。

强力推荐,图片又多有好看!重要的是还不要钱,貌似也不用梯子。

Pexels

Pexels 是一个和pixabay 类似的免费高清图片网站。https://www.pexels.com/
打开它的主页,中间一个输入框,下面会精选出搜索最热门的关键词、图片,还有免费提供的博主专栏。
你还可以将自己的图片上传至Pexels,分享给更多的人使用,你可以根据颜色搜索,它同时也提供了电脑客户端。
你可以个人和商业引用,甚至可以重新分发,它遵循CC0 协议。
Pexels界面

摄图网

1373737张高清图片免费下载
http://699pic.com/
100%正版,可商用,免费下载,精品更新;
摄图网上的图片还是比较多的,而且质量也比较可以,可以算的上国内比较好的图片资源网站;
摄图网

Shutterstock

Shutterstock https://www.shutterstock.com/ 是一家全球技术公司,创建了一个最大、最具活力的双向市场供创意专业人士授权内容,这些内容包括图片、视频和音乐,以及推动创意过程的创新工具。
公司已扩展其产品组合,现在包括 Bigstock(以价值为导向的库存媒体机构)、PremiumBeat(特选免版税音乐库)、Rex Features(为全球媒体提供最好的编辑图片源)和 WebDAM(基于云的企业数字资产管理平台),同时创建了 Offset(高端图片精选)。
Shutterstock

Everypixel

Everypixel https://everypixel.com/是一个 强大的搜索引擎索引 51付费和免费图片的网站,允许用户通过搜索 海量数据库的秒照片。它汇集多家网络素材源,并且利用AI 技术自动移除那些不太好看的内容。
它还有很多筛选功能,你可以选择图片来源的网站,图片类型,还可以筛选不同颜色。
最重要的是还可以筛选免费的照片。

千图网

千图网http://www.58pic.com/经过4年的素材积累和数据沉淀,千图网搭建了一个以创意作品库为核心,围绕着设计人群及泛办公人群提供优质创意服务的共享平台。服务内容涵盖了平面广告、电商淘宝、装饰装修、网页UI、产品工业7大主流设计服务,以及PPT模板、Excel模板、文库模板、高清配图、视频音频5大办公人群常用服务,共计60多项细分服务。
千图网

别样网

别样网http://www.ssyer.com/是一个国内的图片分享网站,和pixabay一样都是免版权和免授权使用的。图片大部分都经过后期处理,风景、人物、小清新各种风格应有尽有。

优点:图片清晰度高,富有美感,网站加载速度快。

缺点:图片数量偏少,没有搜索功能。

别样网

海量图标

iconfont

阿里妈妈MUX倾力打造的矢量图标管理、交流平台http://iconfont.cn/
设计师将图标上传到Iconfont平台,用户可以自定义下载多种格式的icon,平台也可将图标转换为字体,便于前端工程师自由调整与调用。
iconfont

Icons8

Icons8 https://zh.icons8.com/ 是一个提供免费iOS、Windows、Android的平面化设计图案为主的搜索引擎,目前提供近4万个素材资源,数量非常丰富,同时网站也提供各种格式,各种尺寸和配色,让使用者也能自定义制作。
Icons8

easyIcon

提供超过四十六万个PNG、ICO、ICNS格式图标搜索、图标下载服务。http://www.easyicon.net/
easyIcon

material design icon

相信android开发者于这个网站会比较熟悉,没错,他就是material icons。https://material.io/icons/
material

iconmonstr

有时候我们的设计仅需要简简单单的勾勒创作即可,不需要繁杂的效果与艳丽的颜色搭配!今天给大家介绍这么一个朴实、朴素的图标素材网站 iconmonstr ,该网站的图标素材提供的格式分为PNG、SVG格式。http://iconmonstr.com
iconmonstr界面

fontawesome

一套绝佳的图标字体库和CSS框架,为您提供可缩放的矢量图标,您可以使用CSS所提供的所有特性对它们进行更改,包括:大小、颜色、阴影或者其它任何支持的效果。http://www.fontawesome.com.cn/
fontawesome

ionicons

iOnicons是一个提供免费ICON图标素材的站点,同时还提供CDN加速服务,该图标集采用 MIT 许可证,用sass来开发,前端网页设计师们可在商业应用中使用,设计出最佳的网页模版。http://ionicons.com/

ss

创新设计

lofter

LOFTER汇聚了数百万的摄影、胶片玩家,绘画及动漫爱好者,并不断衍生出更多的兴趣圈子,无论是设计、
艺术、科技、时尚、旅行、读书、电影评论都有精彩的人与内容不断产出。http://www.lofter.com/
lofter

Ui中国

UI中国http://www.ui.cn/,前身为iconfans.com。是专业的界面设计师交流、学习、展示平台。同时也是中国最大的UI设计师聚集地,会员均为一线UI设计师,覆盖主流互联网公司。我们希望借助互联网的力量打造国内最专业的UI设计平台,为UI设计师做最好的服务,提高UI设计行业价值!
前身iconfans.com,自2008年发展至今已有6年积累。历年来,iconfans曾经组织会员参与过一系列的国内外的设计竞赛,并取得了优异成绩。每年举办一届行业聚会。在我们的会员中赢得了口碑。不断的推出一些会员活动、曾经与小米、百度、腾讯、新浪、搜狐、锤子科技(Smartisan )、360桌面、点点网、搜狗、华为等知名互联网公司和企业合作活动。
未来,UI中国,将继续以UI设计师为中心,为UI设计师做最好的服务,提高UI设计行业价值!

前端里

前端里http://www.yyyweb.com/专注 Web 开发技术,分享最前沿的 Web 开发教程、资源和和素材,是面向程序开发人员和设计师的学习交流平台。
前端里

通用模板

优品PPT

一个有情怀的免费PPT模板下载网站!http://www.ypppt.com/
优品PPT

学习资料类

博客

CSDN

CSDN https://www.csdn.net/ (Chinese Software Developer Network) 创立于1999年,是中国最大的IT社区和服务平台,为中国的软件开发者和IT从业者提供知识传播、职业发展、软件开发等全生命周期服务,满足他们在职业发展中学习及共享知识和信息、建立职业发展社交圈、通过软件开发实现技术商业化等刚性需求。拥有超过3000万注册会员(其中活跃会员800万)、50万注册企业及合作伙伴。

旗下拥有:全球最大中文IT技术社区:csdn、权威IT专业技术期刊:《程序员》杂志、IT人力资源服务:科锐福克斯、IT技术学习平台:乐知教育、代码托管+社交编程平台:code、移动开发工具和服务聚合平台:mobilehub、IT专属求职网站:job、中文软件外包和项目交易平台:csto、程序员深度交流社区:iteye、中国最大技术管理者平台:CTO俱乐部、云计算产业人士沙龙:云计算俱乐部、面向移动开发者的技术组织:移动开发者俱乐部、面向全国大学生的技术组织:高校俱乐部。

简书

简书http://www.jianshu.com/ 是一个创作社区,任何人均可以在其上进行创作。用户在简书上面可以方便的创作自己的作品,互相交流。简书成为国内优质原创内容输出平台。

博客园

一个IT技术人员想为IT技术人员们提供一个纯净的技术交流空间,博客园很长时间只有一个不能再简单的博客,有近四年,博客园仅靠一个人几年工作的积蓄在维持,互联网浪潮的此起彼伏,“博客”从耀眼的明星成为平民,这些似乎都与博客园无关,博客园一步一个脚印地走着自己的路,傻傻地对每个用户注册进行人工审批、对首页内容宁缺毋滥、对不合适的广告拒之门外,傻傻地对用户体验关怀备至,对盈利模式冷若冰霜。
访问链接: https://www.cnblogs.com/

掘金

掘金技术 https://juejin.im/ 社区是中国质量最高的技术分享社区,邀请稀土用户作为 Co-Editor 来分享优质的技术干货,从前端到后端开发,从设计到产品,让每一个掘金用户都能挖掘到有价值的干货。

网上视频学习

慕课网

程序员的梦工厂<(http://www.imooc.com/>,源于国外,Massive(大规模)Open(开放)Online(在线)Course(课程)。提供大量优质的免费教程,也提供职业路径学习,收费的项目实战。

极客学院

极客学院http://www.jikexueyuan.com/ 是中国android开发在线学习平台,汇集了几十名国内顶尖的有多年项目和实战经验的Android开发授课大师,精心制作了上千个高质量视频教程,涵盖了Android开发学习的基础入门、中级进阶,高级提升、项目实战开发等专业的android开发课程。
极客学院背后是一支疯狂喜欢编程,狂热开发移动app的超有活力团队。他们希望通过最新的,高质量的,专业实战的在线开发课程打破传统的编程学习模式,以新一代的编程学习模式帮助开发者更快更好的学习Android开发,帮助开发者通过技术实现自己的理想。
作为国内最大IT职业在线教育平台,极客学院一直致力于“让学习更有效”,帮助IT从业者在最短的时间内获得最多的知识,技能得到最快的提升。目前,极客学院已拥有60多万IT从业者用户。

网易云课堂

网易云课堂<(http://study.163.com/>,是网易公司打造的在线实用技能学习平台,该平台于2012年12月底正式上线,主要为学习者提供海量、优质的课程,用户可以根据自身的学习程度,自主安排学习进度。
网易云课堂立足于实用性的要求,网易云课堂与多家教育、培训机构建立合作,课程数量已达4100+,课时总数超50000,涵盖实用软件、IT与互联网、外语学习、生活家居、兴趣爱好、职场技能、金融管理、考试认证、中小学、亲子教育等十余大门类。

麦子学院

麦子学院http://www.maiziedu.com/,国内第一家在美国建立商务中心的IT在线教育机构,目前已与美国知名教育公司取得合作,未来将源源不断向国内输出大量高质量教育资源
前身麦可网,2014年,麦可网完成千万级A轮融资,并更名为”麦子学院“,同时通过“麦子圈”IT职业实名社交圈——提供包括企业招聘对接,猎头,项目外包,经验分享,职业交友等一系列增值服务。
做在线职业教育示范学院,将教育和课程做到极致。除了提供高质量的课程学习之外,也提供包括就业推荐,职业交友,项目外包,创业服务等全面的增值服务,并且线上线下结合,移动设备和传统网络相结合,打造IT职业教育的一个完整生态链。
以高端IT技术型人才培养及服务为核心,探索及倡导技术交流创新模式。提供的不仅仅是技能培养,而是务实的职业导向。

中国大学Mooc

中国mooc http://www.icourse163.org/,汇集名校名师的优秀课程,让我们可以通过网络的方式来学习优质的免费课程!大赞,有点免费大学的感觉~

开课吧

开课吧http://www.kaikeba.com/ 是指作为慧科教育的重要成员企业,开课吧是中文泛IT在线教育平台,传承了慧科教育专注前沿科技、创新人才培养模式的基因,并积极探索在线教育模式创新。
开课吧集在线课程的创意、设计、前期拍摄、后期制作、 综合运营为一体,专注于移动开发、云计算、互联网营销等八大类泛IT课程,不断面向个人、高校和企事业单位提供在线产品研发咨询服务、在线课程制作服务、MOOC 平台服务、导学服务和认证服务等综合在线教育解决方案。

计蒜客

计蒜客 http://www.jisuanke.com/ 为弥合工业界、学术界对于优秀计算机人才的缺口而诞生,致力于为全国乃至全球希望投身计算机领域的人才提供高性价比的学习机会,培养快速学习、拥抱变化、敢于发现的优秀工程师与学术精英。大部分是收费的。

实验楼

实验楼 https://www.shiyanlou.com/ 是国内领先的IT技术实训平台,采用创新的“在线实验”学习模式,为学生及在职程序员提供编程、运维、测试、云计算、大数据、数据库等最新的IT技术实践课程。
实验楼建设初衷是帮助学习者通过动手实践收获知识,同时体会实验精神。德国教育学家斯普朗格说:“教育的最终目的不是传授已有的东西,而是要把人的创造力量诱导出来”,实验楼设计理念也是如此:从实践切入,依靠交互性、操作性更强的课程,理论学习+动手实践共同激发你的创造力。

腾讯课堂

腾讯课堂 https://ke.qq.com/ 是腾讯推出的专业在线教育平台,聚合了优质教育机构和教师的海量课程资源。作为开放式的平台,腾讯课堂计划帮助线下教育机构入驻,共同探索在线教育新模式,这无形中又为在线教育O2O增添了几分热度。

腾讯课堂凭借QQ客户端的优势,实现在线即时互动教学;并利用QQ积累多年的音视频能力,提供流畅、高音质的课程直播效果;同时支持PPT演示、屏幕分享等多样化的授课模式,还为教师提供白板、提问等能力。 腾讯创建在线教育平台—腾讯课堂,改善了中国教育资源分布和发展不均的现状,依托互联网,打破地域的限制,让每个立志学习,有梦想的人,都能接受优秀老师的指导和教学;同时希望给优秀的机构及教师一个展示的平台。

Apache Hadoop是一个在商业硬件大型集群上运行应用程序的框架。Hadoop框架为应用程序提供透明的可靠性和数据运动。Hadoop实现了一个名为Map / Reduce的计算范例,其中应用程序分为许多小的工作片段,每个工作片段都可以在集群中的任何节点上执行或重新执行。此外,它还提供了一种在计算节点上存储数据的分布式文件系统(HDFS),可在集群中提供非常高的聚合带宽。两者的MapReduce和Hadoop分布式文件系统的设计使得节点故障是由框架自动处理。

hadoop功能与优势

hadoop是什么我们前面已经介绍过了,下面我们主要介绍他的几个核心部分:

  1. HDFS:分布式文件系统,存储海量的数据
  2. MapReduce:并行处理框架,实现任务分解和调度。
    我们一般用hadoop来搭建大型数据仓库、PB级数据的存储、处理、分析、统计等业务。
  3. Common: 一组分布式文件系统和通用I/O的组建(序列化、java RPC和持久化数据结构)
  4. Pig: 一种数据流语言和运行环境,用以检索非常大的数据集。pig运行在MapRedyce和HdFS集群上。
  5. HBase: 一个分布式、按列存储数据库。Hbase使用HDFS作为底层储存,同时支持MapReduce的批量式计算和点查询(随机读取)。
  6. ZooKeeper:一个分布式、可用性高的协调服务。ZooKeeper提供分布式锁之类的基本服务,用于构建分布式应用。
  7. sqoop: 在数据库和HDFS之间高校传输数据的工具。

hadoop的优势:

  1. 高扩展
  2. 低成本
  3. 成熟的生态圈

参考资料

Retrofit与okhttp共同出自于Square公司,retrofit就是对okhttp做了一层封装。把网络请求都交给给了Okhttp,我们只需要通过简单的配置就能使用retrofit来进行网络请求了,其主要作者是Android大神JakeWharton。

我们上篇学习了如何通过poastMan来模拟http请求来学习elasticsearch的基本请求操作。接下来记录的是如何代码来对我们之前配置好的elasticsearch服务器进行增删改查的基本操作。为了简单起见,我们使用的是开源库retrofit来实现android断的网络通信。

保证可访问

在正式编写代码之前,我们需要做的一件非常重要的事情就是,确保我们的手机能正确访问到elasticsearch服务器,为了简单起见,我们就实现在同一个局域网之间能正常访问即可。至于如果将elasticsearch如果通过外网访问,这里我就不做介绍了。在确保我们手机与电脑在同一个局域网之后,我们需要做的事情就很简单了,用手机浏览器访问http://you ip:9200,如果浏览器显示如下提示:

1
2
3
4
5
6
7
8
9
10
11
12
name: "msater",
cluster_name: "reoger",
cluster_uuid: "D8qVJjX5SCSh62mz9ei3og",
version: {
number: "5.6.1",
build_hash: "667b497",
build_date: "2017-09-14T19:22:05.189Z",
build_snapshot: false,
lucene_version: "6.6.1"
},
tagline: "You Know, for Search"
}

则说明我们的手机已经可以正常访问到eslasticsearch服务器了,这个时候我们就可以进行android端的代码编写了。如果手机浏览器提示我们无法访问,可能是我们的eslasticsearch并并没有绑定我们指定的ip地址,具体做法就是到我们eslasticseach/config/elasticsearch.yml中,修改network.host:的值(如果没有就创建)为我们要访问的ip地址即可。如图:
配置IP地址

访问测试

下面的代码开始就是直接通过retrofit来进行网络请求,如果这个开源库不是很熟悉的话,可以先通过这里了解retroift的基本操作。因为例子比较简单,也可以通过本示例对retrofit做一个简单的了解。
通过我们手机浏览器的测试我们知道,我们只需要用代码实现访问之前测试通过的地址就可以陈宫访问,接下来我们就通过Retrofit来实现对Elasticsearch的访问测试。
代码很简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OkHttpClient client=new OkHttpClient();
Request request = new Request.Builder().url("http://you Ip:9200/").build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {
Log.d("TAG",e.toString());
}

@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
if(response!=null)
Log.d("TAG",response.body().string());
else
Log.d("TAG","没有返回任何的数据");
}
});

观察到,当我们运行上面的代码的时候,会打印出我们用浏览器访问的相同的输出。代表着我们的访问测试成功通过。上面ip地址需要修改成你真正的访问ip地址,本例主要通过okhttp来实现网络请求,retrofit是对okhttp进一步的封装,但是这里我们用okhttp反而更加清晰,简单。

创建索引

在我们前面的介绍中,我们已经知道如何通过postman这一工具来创建索引,其实就是通过put请求来访问http://you ip/你要创建的索引名称,然后通过上传json信息来确定索引的详细信息,示例的json代码如下:

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
{
"settings":{
"number_of_shards":3,
"number_of_replicas":1
},
"mappings":{
"man":{
"properties":{
"name":{
"type":"text"
},
"country":{
"type":"keyword"
},
"age":{
"type":"integer"
},
"data":{
"type":"date",
"format": "yyyy-MM-dd HH:mm:ss||yyy-MM-dd||epoch_millis"
}
}
},
"woman":{
"properties":{
"name":{
"type":"text"
},
"country":{
"type":"keyword"
},
"age":{
"type":"integer"
},
"data":{
"type":"date",
"format":"yyyy-MM-dd HH:mm:ss||yyy-MM-dd||epoch_millis"
}
}
}
}
}

下面我们来在android中创建上述的索引。整体来说,代码还是很简单,我们先看实现代码,然后进行简要的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
String sry ="{ \"settings\": { \"number_of_shards\": 3, \"number_of_replicas\": 1 }, \"mappings\": { \"man\": { \"properties\": { \"name\": { \"type\": \"text\" }, \"country\": { \"type\": \"keyword\" }, \"age\": { \"type\": \"integer\" }, \"data\": { \"type\": \"date\", \"format\": \"yyyy-MM-dd HH:mm:ss||yyy-MM-dd||epoch_millis\" } } }, \"woman\": { \"properties\": { \"name\": { \"type\": \"text\" }, \"country\": { \"type\": \"keyword\" }, \"age\": { \"type\": \"integer\" }, \"data\": { \"type\": \"date\", \"format\": \"yyyy-MM-dd HH:mm:ss||yyy-MM-dd||epoch_millis\" } } } } }";

RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json; charset=utf-8"),sry);
final Request request = new Request.Builder()
.url("http://192.168.139.1:9200/test")
.put(body)
.build();
OkHttpClient client=new OkHttpClient();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {
Log.d("TAG", "onFailure: "+e);
}

@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
if(response!=null)
Log.d("TAG",response.body().string());
else
Log.d("TAG","没有返回任何的数据");
}
});

运行上面的代码就能可以在android端创建一个索引,其实我们的代码也比较简单,就是将我们要上传的json数据当作boby一同提交给服务端就OK了。当然,这里我们的json数据是固定的,但是就对于创建索引来说,固定的json数据也能应该大部分情况了,但是添加数据就不能用固定的json数据,那么我们就不能采用这种简单粗暴的方法了,下面将会介绍如果上传动态json数据。

添加数据

前面文章中已经介绍过如果在postman中添加数据,那么我们现在通过自己编写代码来实现在android中向elsactisearch中添加数据,请求方式是一样的,因为实现起来非常简单,我们只介绍不指定文档ID插入。前面我们介绍到指定文档ID添加数据需要使用put请求方式,并且需要请求地址中表明ID,而不指定文档ID添加数据则需要用post请求方式,并且不需要再请求地址中表明ID。我们这里就实现用不指定文档ID插入。

通过前面的常见索引,相信大家应该知道如果将json数据上传到服务器,我们这里注重讲的书如何将上传动态json数据。
创建动态json数据,我们需要准备json格式对应的bean对象,这里我们采用gsonFormat插件来生成bean对象,例如我们要上传的json数据如下:

1
2
3
4
5
6
{
"name": "张三",
"country": "Englis",
"age": 18,
"date": "1999-03-07"
}

注意,这里上传的数据要与前面我们创建索引数据格式对应起来,否则不能成功添加。我们将该json对象的bean对象命名为AddData,源码如下:

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
public class AddData {


private String name;
private String country;
private int age;
private String date;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getCountry() {
return country;
}

public void setCountry(String country) {
this.country = country;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getDate() {
return date;
}

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

创建上述的bean对象后,我们就可以通过代码来动态的修改json数据,主要的代码如下:

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
AddData addData = new AddData();
addData.setAge(23);
addData.setCountry("China");
addData.setDate("2017-09-19");
addData.setName("DongMingZhu");//动态设置json数据

Gson gson = new Gson();
String dyJson = gson.toJson(addData);
RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json; charset=utf-8"),dyJson);
final Request request = new Request.Builder()
.url("http://192.168.139.1:9200/test/woman")
.post(body)
.build();
OkHttpClient client=new OkHttpClient();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {
Log.d("TAG", "onFailure: "+e);
}

@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
if(response!=null)
Log.d("TAG",response.body().string());
else
Log.d("TAG","没有返回任何的数据");
}
});

运行上述的代码后,发现我们的json数据已经成功上传到服务端。简单吧~

删除数据

删除操作就是用通过DELETE访问即可,要删除指定文档,我们通过DELETE访问到指定的文档ID即可,例如我想删除test索引中man类中的id为1的文档,我的访问链接可能就是http://127.0.0.1/9100/test/man/1 ,如果我们想要直接删除test索引,我们可以通过delete方法访问http://127.0.0.1/9100/test,删除操作很简单,但是我们却要很慎重的对待他,因为我们一旦删除就无法找回。在做删除操作之前最好确保数据备份。因为实现起来很简单,仅贴出删除指定数据的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OkHttpClient client=new OkHttpClient();
Request request = new Request.Builder().url("http://127.0.0.1/9100/test/man/1").delete().build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {
Log.d("TAG",e.toString());
}

@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
if(response!=null)
Log.d("TAG",response.body().string());
else
Log.d("TAG","没有返回任何的数据");
}
});

修改数据

查询数据

Elasticsearch 是什么

Elasticsearch是一个基于Apache Lucene(TM)的开源搜索引擎。无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
但是,Lucene只是一个库。想要使用它,你必须使用Java来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。
Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
不过,Elasticsearch不仅仅是Lucene和全文搜索,我们还能这样去描述它:
分布式的实时文件存储,每个字段都被索引并可被搜索
分布式的实时分析搜索引擎
可以扩展到上百台服务器,处理PB级结构化或非结构化数据
而且,所有的这些功能被集成到一个服务里面,你的应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。
上手Elasticsearch非常容易。它提供了许多合理的缺省值,并对初学者隐藏了复杂的搜索引擎理论。它开箱即用(安装即可使用),只需很少的学习既可在生产环境中使用。
Elasticsearch在Apache 2 license下许可使用,可以免费下载、使用和修改。
随着你对Elasticsearch的理解加深,你可以根据不同的问题领域定制Elasticsearch的高级特性,这一切都是可配置的,并且配置非常灵活。

开始使用

因为本人电脑是win10的系统,所以针对elasticsearch所有的操作与环境都是在win10环境下完成,至于其他系统环境,这里不予讨论。

安装

安装什么的最简单了,首先肯定要将elasticsearch下载到本地,要下载请戳这里,下载完成后,直接解析即可开始我们的探究之旅。关于版本,这里需要做一点说明的是,elasticsearch总共有三个大版本,即1XX、2XX和5XX三个,我们这里使用的是最新的版本5.6.1进行学习
解压完成后,我们进入bin目录下,点击elasticsearch.bat即可运行。在成功运行后,我们通过浏览器访问http://localhost:9200/得到json格式的数据,即说明我们的elasicsearch成功安装并运行了。如果你在这里失败了,可能因素很多,有可能是你电脑里的java环境问题(没有java环境、java版本低于8等等),也有可能是你的电脑内存啥的太小了(一般不会出现在自己的电脑上,不过我将他部署在腾讯云上时跑不起来就是因为内存太小了)。失败的解决方案就不多介绍了,我们继续往下走。

安装插件

通过前面我们的测试安装,我们发现elasticsearch给我们返回的是json格式的数据,并不是很直观,这里我们通过安装一个叫做elasticsearch-head-master的插件,使得我们能更加直观的看到elasticsearch给我们返回的数据。
这个插件的github地址在这里
,我们可以通过他的github地址来学习如何安装和使用。

  1. 首先肯定是将这个插件下载下来,在github下载资料对你来说应该是很简单了,这里就不赘述了,例如我们可以这样下载:git clone git://github.com/mobz/elasticsearch-head.git
  2. 进入下载并解压好的源代码的目录。
  3. 输入 npm install,安装必要的资源。
  4. 通过npm run start来启动插件
  5. 在浏览器中打开http://localhost:9100/来测试是否正常打开。
    能正常打开之后,这里时候我们发现elasticsearch和我们head并没有联系到一起,这里因为这两个都运行在独立的进程中,他们之间的访问存在跨域问题,这里时候我们需要进行一个简单的配置才能将这个插件真正运行起来。首先我们需要在elasticsearch目录下,找到config目录,然后再config中找到elasticsearch.yml文件,在这里文件的最后,添加下面的两行代码:
    1
    2
    http.cors.enabled: true
    http.cors.allow-origin: "*"
    保存,退出后,将elasticsearch和head-master都重启后,重新访问http://localhost:9100/就会发现,此时我们的集群已经链接。这里时候我们就可以继续探索了。

分布式安装

上面的我们的安装属于单利安装,elasticsearch也支持分布式安装。下面介绍一下分布式安装。首先,我们制定一个msater,即为集群的指挥官。还是在之前的节点配置文件下,修改elasticsearch.yml文件中的内容,主要就是在末尾添加如下的配置文件:

1
2
3
4
cluster.name: reoger    # 指定集群的名字
node.name: msater # 对master取名
node.master: true #指定他为master
network.host: 127.0.0.1 # 绑定ip

修改上面的配置重启后,我们再次访问http://localhost:9100/就可以发现集群的名字已经修改为我们配置的名字。
然后,我们再来添加其他的集群。步骤也很简单,我们将之前下载好的elasticsearch复制一份,然后修改其中的elasticsearch.yml文件,添加如下的配置:

1
2
3
4
5
6
7
cluster.name: reoger                            # 指定集群名气,需要和master指定的一致
node.name: slave1 # 指定服务节点的名字

network.host: 127.0.0.1 # 指定本地ip
http.port: 8200 # 指定端口号

discovery.zen.ping.unicast.hosts: ["127.0.0.1"] # 发现msater的

然后通过./bin/elasticsearch启动后,通过浏览http://localhost:9100/,就会发现我们已经添加了两个集群了。如图:
分布式安装效果图

创建索引

在创建索引之前,很有必要对elasticsearch中的基本概念进行一个简单的了解。
一句话理解就是:Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。

下面我们就来创建索引:
结构化创建
方法一、通过head-master创建
为了使创建索引比较直观,我们可以直接通过head-master插件来完成,具体我们可以访问http://localhost:9100/,查看我们集群的状况,然后选择索引 -> 新建索引,如图所示:

稍等片刻之后就会提示我们索引创建成功,然后在集群概览中就可以看到我们之前添加的索引。
然后选择复合查询,在查询创建我们的索引。json示例代码如下:

1
2
3
4
5
6
7
8
9
{
"novel": {
"properties": {
"title": {
"type": "text"
}
}
}
}

如图:
利用head-master常见索引

验证我们是否添加成功,可以在``概览 -> 信息 -> 索引信息 。
如图:
效果展示
我们可以明显的看出来,我们已经在mapppings中添加了novel这么一个索引。

当然,通过head-master编写不是很直观,我们可以通过更加方便的工具postMan来实现上述功能。
示例操作:
我们需要通过put请求127.0.0.1:9200/people其中的people是我们要创建的索引,然后通过我们上传的json文件对创建的索引进行说明。
例如上传的的json代码如下:

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
{
"settings":{
"number_of_shards":3,
"number_of_replicas":1
},
"mappings":{
"man":{
"properties":{
"name":{
"type":"text"
},
"country":{
"type":"keyword"
},
"age":{
"type":"integer"
},
"data":{
"type":"date",
"format": "yyyy-MM-dd HH:mm:ss||yyy-MM-dd||epoch_millis"
}
}
},
"woman":{

}
}
}

表示的是常见的people
验证方法和之前的一样。

非结构化创建

插入数据

指定文档ID插入

我们这里仅用postman做示例:
指定文档ID插入

不指定文档ID插入

不指定文档插入
可以看到,指定文档ID插入我们需要使用put请求方式,并且需要指定文档ID。而不指定文档ID插入则使用post请求方式,并且不需要知道哪个文档ID。

验证是否添加成功,在数据浏览中就可以看到我们插入的数据:
验证文档是否添加成功

修改数据

修改数据很简单,

直接修改文档

直接修改文档

脚本修改文档

保持要访问的地址和方式不变,将上传的json格式修改为下面的内容,即修改为script的关键字。

1
2
3
4
5
6
{
"script":{
"lang":"painless",
"inline":"ctx._source.age+=10"
}
}

或者我们也可以这么写:

1
2
3
4
5
6
7
8
9
{
"script":{
"lang":"painless",
"inline":"ctx._source.age=params.age",
"params":{
"age":100
}
}
}

删除

删除文档

删除文档很简单,只需要简单的通过DELETE方式访问制定ID文档即可删除。
例如我们想删除man索引中文档ID为1的文档,我们通过DELETE方式访问即可。
127.0.0.1:9200/people/man/1

删除索引

删除索引非常危险,因为删除索引后,索引里面所有的内容都将被删除!!!如果我们确定我们需要删除的话,可以通过head-master来实现。
具体可以选择相应的索引 -> 动作 -> 删除索引 -> 输入删除 -> 成功删除。

或者我们直接通过delete进行访问,例如想要删除people这个索引,我们只需要通过delete方式访问127.0.0.1:9200/people/即可删除。

查询

简单查询

直接通过文档进行查询,请求方式:GET
例如我想查询在people索引中,类型为man,文档ID为1的文档信息的话,直接访问:
127.0.0.1:9200/people/man/1即可。

条件查询

大多数情况下,简单查询不能满足我们的需求,这个时候我们就可以通过条件查询来满足我们复杂的需求。
如果我们需要查询people索引下所有的文档,可以通过post方法访问127.0.0.1:9200/people/_search,并将body的内容设置为如下的内容:

1
2
3
4
5
6
7
{
"query":{
"match_all":{

}
}
}

当然,大多数情况我们是需要按条件查询,下面是按条件查询的示例json请求格式:

1
2
3
4
5
6
7
8
9
10
{
"query":{ //注意 这里是query关键字
"match":{ //要查询的的条件
"country":"Test"
}
},
"sort":[ //查询结果后的排序
{"date": {"order":"desc"}} // 倒叙排序
]
}

聚合查询

聚合查询,简单来说就是将要查询的数据组合到一起,进而汇总来自多行的信息。
例如,我想查询people中,年龄相同的聚合信息,查询的方式仍然是post,访问的地址仍然是127.0.0.1:9200/people/_search,不同的发送的json数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"aggs":{ //关键字
"group_by_word_age":{ //自定义聚合的名字
"terms":{ //关键字
"field": "age" //聚合的字段名称
}
},
"group_by_data":{ //自定义聚合的名字
"terms":{ //关键字
"field": "date" //聚合字段的名称
}
}
}
}

我这里返回的信息为:

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
{
....,
"aggregations": {
"group_by_data": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 923443200000,
"key_as_string": "1999-04-07T00:00:00.000Z",
"doc_count": 2
},
{
"key": -1916697600000,
"key_as_string": "1909-04-07T00:00:00.000Z",
"doc_count": 1
}
]
},
"group_by_word_age": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 28,
"doc_count": 3
}
]
}
}
}

当然,我们不但可以通过terms查询字段的聚合信息,还可以通过statusminmax等关键字查询字段的状态信息。

最后,补充一点就是,如果想通过ip地址访问的话,需要修改elasticsearch.yml文件中的network.host:为我们指定的Ip字段。
暂时先写这么吧~


参考资料

面相对象的特征?

抽象、继承、封装、多态。
抽象:将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。
继承:从已有类得到继承信息创建新类的过程,
封装:把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
多态:允许不同子类型的对象对同一消息做出不同响应,多态分为编译时多态和运行时多态。

String属于基本的数据结构吗?

不是。Java中的基本数据类型只有8个:byte、short、int、long、float、char、boolean;
除了基本类型和枚举类型,剩下的都是引用类型。

float f = 3.4;是否正确?

错误,3.4是双精度数,将双精度赋值给单精度属于向下转型,会造成精度损失。因此需要强制转换。

String和StringBuilder、StringBuffer的区别?

Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder,它们可以储存和操作字符串。其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer要高。

int和Integer有什么区别?

Integer是int的包装类,int是基本类型,integer是对象。使用intent值得注意的一点是,如果integer整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象。例如题目:

1
2
3
4
5
6
7
8
   public static void main(String[] args) {
// TODO Auto-generated method stub
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
int f = 100;
System.out.println(f1 == f2);
System.out.println(f3 == f4);
System.out.println(f == f1);
}

输出的结果应该为 true,false,true;原因上面已经说过了。

内存中的栈(stack)、堆(heap)和静态区(static area),方法区的作用。

栈用于存放基本的数据类型、对象的引用和函数调用的现场保护等;
堆用于存放new关键字和构造器创建的对象;
静态区用于存放程序中的字面量,如直接书写100,”hello”和常量。
如语句: String str = new String(“hello”);
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而”hello”这个字面量放在静态区。
方法区:存放class二进制文件,包含类信息,静态变量,常量池,类的版本号等基本信息。方法区是各个线程共享的内存区域,用于存储class的二进制文件。

7. 构造器(construction)是否可以被重写(overrider)?

构造器不能被继承,因此不能被重写,但可以被重载。

8. 两个对象值相同( x.equals(y) == true ),那么他们的hashCode相同吗?

相同。在java中,hashCode值相同,他们并不一定相同(equals方法返回true),但是两个对象相同,他们的hasdCode一定相同。

9. 重载(Overload)和重写(Override)的区别。

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态。重载发生在一个类中,同名的方法如果有不同的参数,则为重载;重写发生在子类和父类之间,重写要求子类被重写方法的与父类被重写的方法有相同的返回类型,方法名和参数;重载对返回类型则没有特殊的要求。

10. JVM加载class文件的原理机制?

JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:

  • Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
  • Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
  • System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。

11. char 型变量中能不能存贮一个中文汉字。

char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16比特),所以放一个中文是没问题的。

12. 抽象类(abstract)和接口(interface)有什么异同?

抽象类和接口类都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部是抽象方法。
抽象类中的成员可以定义成员变量、而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必有抽象方法。
抽象类中的成员可以是private、protected、public的,而接口中的成员全部都是public。

13. 静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?

Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化。要在静态方法中创建内部类对象,可以这样做:

1
new Outer().new Inner();

14. 静态变量和实例变量的区别。

静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。

15. 接口是否可继承接口?抽象类是否可实现接口?抽象类是否可继承具体类?

接口可以继承接口,而且支持多重继承。抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类。

16. Throw和throws有什么区别?

throws是方法可能抛出异常的声明。用在方法方法函数头,表示抛出异常。throws表示出现异常的一种可能性,并不一定会发生这些异常;
throw是语句抛出一个异常。用在方法里,表示抛出了一个实实在在的异常,他必须要被处理。

17. final、finally、finalize的区别。

  • final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final的方法也同样只能使用,不能在子类中被重写。
  • finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
  • finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。

18. Collection和Collections的区别?

Collection是一个接口,它是Set、List等容器的父接口;Collections是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。

19. List、Set、Map是否继承自Collection接口?

List、Set 是,Map 不是。Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。

20. Sleep()方法和wait()方法有什么异同?

相同:都可以让线程处理冻结状态。
不同点:sleep()是Thread类中定义的方法,wait()是Object类中定义的方法;
Sleep()必须指定时间,而wait()可以指定也可不指定;
Sleep()不会释放锁,而wait()释放锁;
Sleep()需要捕获异常,而wait()只能在(synchronized)环境中使用。
Sleep()睡眠后不出让系统资源,wait让出系统资源其他线程可以占用CPU

21. 抽象的(abstract)方法是否可以同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰?

都不能。抽象方法需要子类重写,而静态的方法是无法重写的,因此二者是矛盾的。本地方法是由本地代码(如c/c++)实现的方法,而抽象方法是没有实现的,也是矛盾的。Synchronied和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

22. Error和Exception有什么区别?

Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;
Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题。也就是说,他表示如果程序运行正常,从不会发生的情况。

23. 多线程编程有几种实现方式?

Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runable接口。两种方式都要重写run()方法来定义线程的行为,推荐使用后者,因为java中的继承是单继承,显然接口更加灵活。

24. Thread类中的start()和run()方法有什么区别?

Start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有在新的线程中调用,没有新的线程启动,start()方法才会启动新的线程。

25. Java中的volatile 变量是什么?

volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生。

26. 什么是ThreadLocal变量?

ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。 当然,可以参考《android艺术探索》P375 了解ThreadLocal原理。

27. 有三个线程,怎么确保他们顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成线程继续执行;
你也可以用java中的锁机制(ReentrantLock、或者newCondition),在一个线程执行的时候,让他占有锁,线程执行完毕才释放锁;
也可以利用三个信号量来让三个线程按顺序执行,刚开始的时候设置只有第一个线程可以获取信号量,第一个线程运行完毕后释放第二个信号量,以此启动第二个线程。
甚至你可以将这三个线程放入线程池,利用线程池来让他们按顺序执行。
如果上面的讲述不是很清楚,可以参考代码

28. Java中如何停止一个线程?

JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束。如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程;也可以使用inerrupt方法终止线程,但interrupt并不一定会终止线程,该方法只是设置了一个中断状态。当然,停止线程也可以通过return语句来实现,不过个人推荐使用异常来停止线程,因为抛出的异常可以传递线程停止的信息而通过return语句就不能传递。

29. Switch能否用string做参数?

不能。在java 7以前,switch只能支持byte、short、char、int或者对应的封装类以及enum类型。在java 7中,switch支持string了,但是不支持long。

30. Java中四种引用的区别?

强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。

31. hashMap和hashTable的区别?

hashMap是非synchronied的(即线程安全的),而hashTable是线程安全的(使用synchronied修饰,代价就是效率变低)。
hashMap 可以接受null(即可以将键值或者值设置为null),但是hashTable不行。
hashMap中hash数组默认大小是11,增加的方式是old*2+1,hashMap中hash数组的默认大小是16,而且一定是2的指数。
hasMap是Map接口的一个实现,而Hashtable是基于陈旧的Dictionary类,完成了Map接口;

32. ArrayList和LinkedList的区别。

ArrayList的实现为动态数组,初识大小为10,大小不够会自动调用grow扩容:length = length+(length>>1);
LinkedList的实现为双向链表,没有默认的大小。

33. &和&&的区别

  • &表示位运算与,不会短路;
  • &&表示逻辑运算与,在第一个条件为false的情况下会短路。

34. 在java中如何跳出当前多重循环?

我们可以在外层循环添加一个标号(以”:”结束),当我们需要结束循环时,我们可以直接break +外层循环的标号;
当然,我们可以在外层循环中添加一个bool值的变量,当门需要退出时,改变bool的值让外层循环停止即可。

35. Synchronized锁对象与锁方法、代码块区别。

当synchronized锁的是对象,两个线程分别访问同一个类的同一个实例的相同方法时,他们会按照会同步执行两个类中所有被调用的方法;而当synchronized锁的是方法时,他们只会同步执行这个被锁住的方法,而当synchronized是一个代码块的时候,就意味着只有这个方法块才会同步执行。

36. 字节流和字符流的区别?

字节流用于读取或写出二进制数据,比如图片、影像等数据,几乎所有的数据都可以通过字节流来处理;字节流的基本单元为字节;字节流默认不使用缓存区;
字符流用于读取或写出字符数据,比如传输字符串;字符流的基本单元为Unicode码元;字符流默认使用缓存区;

37. TreeMap,LinkedHashMap,HashMap的区别是什么?

hashMap底层实现是散列表,因此它内存存储的元素的无序的;
TreeMap的底层实现是红黑树,所以它内部的元素是有序的。排序的依据是自然序或者是创建TreeMap时所提供的比较器(Comparator)对象;
LinkedHashMap能够记住插入元素的顺序。

38. 说说设计模式

详细资料可以参考这里
简单工厂模式:它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类型的实例。简单工厂模式专门定义一个类来负责创建其他类的实例。
https://github.com/mingjunli/JavaDesignPatterns

39. int-char-long在java中各占多少字节?

Byte Short Int Long Float Double Char
位数 8 16 32 64 32 64
字节数 1 2 4 8 4 8

OkHttp是一个处理网络请求的开源项目,是Android端最火热的轻量级框架,由移动支付Square公司贡献用于替代HttpUrlConnection和Apache HttpClient。随着OkHttp的不断成熟,越来越多的Android开发者使用OkHttp作为网络框架。

简单使用

在真正进行源码分析之前,简单的回顾一个okhttp的简单使用。首先将okhttp继承到自己的项目中,在build.gradle添加如下的依赖:

1
compile 'com.squareup.okhttp3:okhttp:3.7.0'

下面是一个okhttp简单进行get请求的一个例子:

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
//1.拿到okhttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();

//2. 构造request对象
Request.Builder builder = new Request.Builder();
Request request = builder.get().url("http://www.baidu.com").build();
//3. 构建Call对象
okhttp3.Call call = okHttpClient.newCall(request);
//4.执行
//同步执行 call.execute();

//异步执行
call.enqueue(new Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {
//失败的回调
}
@Override
public void onResponse(okhttp3.Call call, Response response) throws IOException {
//成功的回调
final String repo = response.body().string();
Log.d("TAG",repo+" ");
runOnUiThread(new Runnable() {
@Override
public void run() {
mText.setText(repo);
}
});
}
});

上面的代码用okhttp实现了一个简单的网络请求,主要由四步组成。关于okhttp更多的用法请参考博客。对上面的okhttp中常用的类做一个简单的介绍:

  1. OkHttpClient 可以理解用户面板,发送的网络请求都是通过他来实现的,每个OkhttpClient都在内部都维护了属于自己的任务队列,连接池,Cache,拦截器等,所以在使用OkHttp作为网络框架时应该全局共享一个OkHttpClient实例。
  2. Request 可以理解为用户发送的请求。
  3. Response 是响应是对请求的回复,包含状态码、HTTP头和主体部分。
  4. Call 描述一个实际的访问请求,用户的每一个网络请求都是一个Call实例。

下面将对上面的四步一步一步来进行分析,并探究其源码的实现。

创建okhttpClient对象

关于okhttpClient对象,在上面已经进行了一个简单的解释,那么他为甚是这个样子的,下面通过源码来验证。下面是OkHttpClient类中的部分代码:

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
public OkHttpClient() {
this(new Builder());
}

public Builder() {
dispatcher = new Dispatcher();//分发器
protocols = DEFAULT_PROTOCOLS;//协议
connectionSpecs = DEFAULT_CONNECTION_SPECS;//传输层版本和连接协议
eventListenerFactory = EventListener.factory(EventListener.NONE);//事件工厂
proxySelector = ProxySelector.getDefault();//代理选择
cookieJar = CookieJar.NO_COOKIES;//cookie
socketFactory = SocketFactory.getDefault();//socket工厂
hostnameVerifier = OkHostnameVerifier.INSTANCE;//主机名字确认
certificatePinner = CertificatePinner.DEFAULT;//证书链
proxyAuthenticator = Authenticator.NONE;//代理身份验证
authenticator = Authenticator.NONE;//本地身份验证
connectionPool = new ConnectionPool();//连接池,复用连接
dns = Dns.SYSTEM;//域名
followSslRedirects = true;//安全套接层重定向
followRedirects = true;//本地重定向
retryOnConnectionFailure = true;//重试连接失败
connectTimeout = 10_000;//连接超时时间
readTimeout = 10_000;//读超时
writeTimeout = 10_000;//写超时
pingInterval = 0;//
}
...

可以看出来,直接创建的OkHttpClient对象并且默认构造builder对象进行初始化。当然,直接创建OkhttpClient是非常简单的,但是其中的配置就只能用默认的配置了。如果需要子的自定义配置,可以通过下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OkHttpClient  okHttpClient= new c.Builder()
.cookieJar(new CookieJar() {
private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();

@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.put(url, cookies);
}

@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(url);
return cookies != null ? cookies : new ArrayList<Cookie>();
}
})
.build();
//为请求添加CookieJar。

至于实现,也非常简单,就是一个Builder模式。具体实现就不做过多的介绍了。

构造request对象

构建request对象的代码如下所示:

1
Request request = new Request.Builder().get().url("url").build();

Request的构建过程也非常简单,在request中的实现如下所示:

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
public Builder() {
this.method = "GET";
this.headers = new Headers.Builder();
}

public Builder get() {
return method("GET", null);
}

public Builder url(String url) {
if (url == null) throw new NullPointerException("url == null");

// Silently replace web socket URLs with HTTP URLs.
if (url.regionMatches(true, 0, "ws:", 0, 3)) {
url = "http:" + url.substring(3);
} else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
url = "https:" + url.substring(4);
}

HttpUrl parsed = HttpUrl.parse(url);
if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
return url(parsed);
}

public Request build() {
if (url == null) throw new IllegalStateException("url == null");
return new Request(this);
}

可以看出来,request的构建过程其实也是非常简单的,也是利用建造者模式构建出request对象。在request配置URl、get、等一些列的参数。整体来说,比较简单。

构建Call对象并执行

前两步都是非常简单的,不管是从源码的实现上,还是从我们代码的调用上来看都是非常简单的。但是前面的只是开胃菜,真正的大餐才正要开始。
我们将实例代码的三、四步放到一起来进行分析:

1
2
okhttp3.Call call = okHttpClient.newCall(request);
Response execute = call.execute();

从调用代码上来看,其实现也是非常简单的。下面将从源码的角度一步一步进行分析。
首先是构建Call对象,在OkHttpClient类中的实现如下。

1
2
3
@Override public Call newCall(Request request) {
return new RealCall(this, request, false /* for web socket */);
}

可以看出来,在okhttpClient中只是简单的调用了RealCall方法,我们继续来看在RealCall类中RealCall方法的实现:

1
2
3
4
5
6
7
8
9
10
11
RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
final EventListener.Factory eventListenerFactory = client.eventListenerFactory();

this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);

// TODO(jwilson): this is unsafe publication and not threadsafe.
this.eventListener = eventListenerFactory.create(this);
}

RealCall方法也只是对其中的参数进行一些设置。当然,对其中的参数有还是需要有一定的了解。

  • client对象就是我们前面创建的okhttpClient对象
  • originalRequest对象就是已经构建完毕的Request对象
  • forWebSocket值是为了区分是不是进行web socket通信,是为true,否为false;
  • eventListener是为后面执行完之后的回调设置的监听。
    构建一个call对象之后,就通过这个call对象来进行网络请求了。具体执行(同步执行)在RealCall类中的实现代码如下:
    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
    //同步执行网路请求
    @Override public Response execute() throws IOException {
    synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
    }
    captureCallStackTrace();
    try {
    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
    } finally {
    client.dispatcher().finished(this);
    }
    }

    //跟踪调用栈的信息,这里追踪的是response.body().close()方法的调用信息
    private void captureCallStackTrace() {
    Object callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()");
    retryAndFollowUpInterceptor.setCallStackTrace(callStackTrace);
    }

    //添加一堆的拦截器。
    Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
    interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
    }
    简单分析上面代码的实现:首先利用synchronized加锁,是为了确保一个call对象只能执行一次。captureCallStackTrace方法用于追踪调用栈的信息。通过client.dispatcher().executed(this)将当前的call加入到runningSyncCalls这样一个正在运行的队列中。关于这点,后面将会重点讲到,这里先只是提出这么一个概念。我们继续解析上面的代码,在将当前的call添加到运行队列中后,通过getResponseWithInterceptorChain为当前的call添加一堆的拦截器,并将网络请求的结果返回回来,至于getResponseWithInterceptorChain里面的具体实现,我们放在后面来讲。最后,通过client.dispatcher().finished(this);来结束当前访问和释放相关资源。
    下面来了解异步执行的相关逻辑。代码的实现部分同样是在RealCall类中,相关的代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
    }
    captureCallStackTrace();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
    }
    通过前面的同步访问的分析,我们对异步访问的分析,现在看异步请求就很简单了。前面的步骤都是一样的,就不一一介绍了。我们直接看最后一句,其中的参数AsyncCall表示的其实就是我们要添加的任务请求。在RealCall类中有如下的实现代码:
    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
    final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    AsyncCall(Callback responseCallback) {
    super("OkHttp %s", redactedUrl());
    this.responseCallback = responseCallback;
    }

    String host() {
    return originalRequest.url().host();
    }

    Request request() {
    return originalRequest;
    }

    RealCall get() {
    return RealCall.this;
    }

    @Override protected void execute() {
    boolean signalledCallback = false;
    try {
    Response response = getResponseWithInterceptorChain();
    if (retryAndFollowUpInterceptor.isCanceled()) {
    signalledCallback = true;
    responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
    signalledCallback = true;
    responseCallback.onResponse(RealCall.this, response);
    }
    } catch (IOException e) {
    if (signalledCallback) {
    // Do not signal the callback twice!
    Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
    } else {
    responseCallback.onFailure(RealCall.this, e);
    }
    } finally {
    client.dispatcher().finished(this);
    }
    }
    }
    而上面AsyncCall继承的NamedRunnable本身也实现了Runnable的接口。所以本质来说,AsyncCall其实就是一个Runnbale,即一个任务。在这里值访问请求任务。我们发现在execute方法中,真正实现访问请求的也是getResponseWithInterceptorChain,如果访问成功就回调onResponse方法,并将response传递过去;否则就回调onFailure方法,并将错误信息和CallBack对象传递过去。当然,最终也是通过finished方法结束访问。分析完了AsyncCall,接来继续分析前面的enqueue方法。发现其在Dispatcher类中的实现逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Ready async calls in the order they'll be run. */
//这个队列代表准备好的异步请求
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
//这个队列代表正在运行的异步请求
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
//这个队列代表正在运行的同步请求。
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

private int maxRequests = 64;
private int maxRequestsPerHost = 5;

synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}

即在当前运行异步请求队列数量小于64且访问同一个主机数量的队列小于5个时,将当前的请求直接加入正在运行的请求队列中,并通过executorService().execute(call)执行,否则的话就将请求添加到准备的请求队列中。至于executorService().execute(call)的方法的实现Dispatcher中创建executorService代码如下:

1
2
3
4
5
6
7
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}

通过代码可以很清楚的了解,executorService就是创建一个线程池,核心线程数为0,最大线程数为MAX_VALUE,线程空闲时最大的存活时间为60s,容器为先进先出的队列。然后执行execute方法,在线程池中运行该请求。那么运行完毕后,是怎么将请求从运行异步队列中移除?其实,在前面的分析过程中,我们对execute同步请求和enqueue异步请求的都最终会调用的一个方法client.dispatcher().finished(this);并没有仔细的去分析,下面我们分析该方法是如何将运行完成的请求从运行异步队列中移除的。下面是关键性的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** Used by {@code AsyncCall#run} to signal completion. */
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}

/** Used by {@code Call#execute} to signal completion. */
void finished(RealCall call) {
finished(runningSyncCalls, call, false);
}

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}

通过上面的代码,很容易就发现其实现finished中实现主要就是将已经运行完成的请求从正在运行的异步队列中移除。可以看到,当调用finished(RealCall call)方法时,会调用promoteCalls方法。我们继续来看其实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();

if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}

if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}

很清晰的可以看出来,promoteCalls方法就是在runningAsyncCalls队列数量小于64时,将readyAsyncCalls队列中的请求放入到runningAsyncCalls队列中。然后分别执行。否则就直接返回,什么也不做。相信这里的算是很简单的代码吧,就不多介绍了。到这里,在总结一下上面的三个队列的作用和转化吧。

  • runningAsyncCalls就用存储正在运行的异步请求,当正在请求的数量大于64时,将后面添加的请求放入到readyAsyncCalls队列中,在合适的时机(即当runningAsyncCalls数量小于64时),将readyAsyncCalls放入到runningAsyncCalls队列中。通过这种方式来保障当前正在运行的异步请求数量不会过大,相当于一个排队机制。
  • runningSyncCalls这个队列用于存储正在运行的同步请求,对于同步请求,并没有什么排队机制,因为他是阻塞式的,所以用一个队列来存储即可。

拦截器&网络请求的实现

通过上面的分析,我们并没有真正发现网络请求的实现,在前面的分析过程中,我们只丢下了一个重要的方法并没有深入来讲,即getResponseWithInterceptorChain这个方法。对前面分析的内容比较熟悉的话,应该知道无论是异步请求还是同步请求,都是通过getResponseWithInterceptorChain这个方法获取返回值,然后将在继续下面的内容的。那么肯定,网络请求的具体实现就在getResponseWithInterceptorChain这个方法中了。他在RealCall中的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}

通过代码,我们可以看到,首先添加了一系列的拦截器。然后创建一个拦截器链RealInterceptorChain,并执行了拦截器链的proceed方法。
我们首先对其中的拦截器进行解析,然后在来解析具体的网路请求。首先,先解释一下interceptors(拦截器)是什么吧。简单来说:
拦截器是一种强大的机制,可以监视,重写和重试调用。
如果相对拦截器更加深入的了解,可以参考githu上的wiki,如果阅读有困难的话,可以参考中文版
一个网络请求实际上就是一个个拦截器执行其intercept方法的过程。而这其中除了用户自定义的拦截器以外还有几个核心的拦截器完成网络访问的核心逻辑,按照先后顺序以此是:

  1. RetryAndFollowUpInterceptor 负责失败重试以及重定向
  2. BridgeInterceptor 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应
  3. CacheInterceptor 负责读取缓存直接返回、更新缓存
  4. ConnectInterceptor 负责和服务器建立连接
  5. networkInterceptors 配置okHttpClent时设置的,当然,此拦截器不适用于web Socket
  6. CallServerInterceptor 责向服务器发送请求数据、从服务器读取响应数据
    当然,如果有用户自己设计的拦截器,会在上面拦截其执行之前执行。
    在添加拦截器之后,会构建一个拦截器链RealInterceptorChain,并通过proceed方法开启链式调用。
    下面我们先来看一下RealInterceptorChain拦截器链的具体实现:
    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
    **
    * A concrete interceptor chain that carries the entire interceptor chain: all application
    * interceptors, the OkHttp core, all network interceptors, and finally the network caller.
    */
    public final class RealInterceptorChain implements Interceptor.Chain {
    private final List<Interceptor> interceptors;
    private final StreamAllocation streamAllocation;
    private final HttpCodec httpCodec;
    private final RealConnection connection;
    private final int index;
    private final Request request;
    private int calls;

    public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
    HttpCodec httpCodec, RealConnection connection, int index, Request request) {
    this.interceptors = interceptors;
    this.connection = connection;
    this.streamAllocation = streamAllocation;
    this.httpCodec = httpCodec;
    this.index = index;
    this.request = request;
    }

    @Override public Connection connection() {
    return connection;
    }

    public StreamAllocation streamAllocation() {
    return streamAllocation;
    }

    public HttpCodec httpStream() {
    return httpCodec;
    }

    @Override public Request request() {
    return request;
    }

    @Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
    }

    public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
    throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
    + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
    throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
    + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(
    interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
    throw new IllegalStateException("network interceptor " + interceptor
    + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
    throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    return response;
    }
    }
    上面代码比较多,重要的内容并不多,我们只需要看着重看第63行到66行就可以。从整体上来说,RealInterceptorChain中的proceed方法主要做了两件事情:
  • 实例化下一个拦截器对应的RealIterceptorChain对象,这个对象会传递给当前的拦截器
  • 调用当前拦截器的intercept方法,将下一个拦截器的orChain对象传递下去。

接下来我们就来分析以下传入到拦截器链中的拦截器的具体内容.我们首先来分析第一个拦截器:

RetryAndFollowUpInterceptor拦截器

作用:

  • 在网络请求失败后重试
  • 当服务器返回当前请求需要进行重定向时直接发起新的请求,并在条件允许的情况下复用当前连接
    其在RetryAndFollowUpInterceptor类中的构造函数和重要方法Response的实现如下:
    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
    private static final int MAX_FOLLOW_UPS = 20;
    private final OkHttpClient client;
    private final boolean forWebSocket;
    private StreamAllocation streamAllocation;
    private Object callStackTrace;
    private volatile boolean canceled;

    public RetryAndFollowUpInterceptor(OkHttpClient client, boolean forWebSocket) {
    this.client = client;
    this.forWebSocket = forWebSocket;
    }

    @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();

    streamAllocation = new StreamAllocation(
    client.connectionPool(), createAddress(request.url()), callStackTrace);

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
    if (canceled) {
    streamAllocation.release();
    throw new IOException("Canceled");
    }

    Response response = null;
    boolean releaseConnection = true;
    try {
    //执行下一个拦截器链的proceed方法
    response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
    releaseConnection = false;
    } catch (RouteException e) {
    ...
    } finally {
    // We're throwing an unchecked exception. Release any resources.
    if (releaseConnection) {
    streamAllocation.streamFailed(null);
    streamAllocation.release();
    }
    }

    // Attach the prior response if it exists. Such responses never have a body.
    if (priorResponse != null) {
    response = response.newBuilder()
    .priorResponse(priorResponse.newBuilder()
    .body(null)
    .build())
    .build();
    }

    Request followUp = followUpRequest(response);

    if (followUp == null) {
    if (!forWebSocket) {
    streamAllocation.release();
    }
    return response;
    }
    closeQuietly(response.body());

    if (!sameConnection(response, followUp.url())) {
    streamAllocation.release();
    streamAllocation = new StreamAllocation(
    client.connectionPool(), createAddress(followUp.url()), callStackTrace);
    } else if (streamAllocation.codec() != null) {
    throw new IllegalStateException("Closing the body of " + response
    + " didn't close its backing stream. Bad interceptor?");
    }

    request = followUp;
    priorResponse = response;
    }
    }
    经过删减后的代码还是有点多,但是我们只是理解流程的话,值需要特别关注第31行。这行代码是执行下一个拦截器链的proceed方法,而我们知道在下一个拦截器链中又会执行下一个拦截器的intercept方法。所以,整个执行过程都是一个拦截器与拦截链中交替执行,最终完成所有拦截器的操作。

BridgeInterceptor拦截器

作用:
从用户的请求构建网络请求,然后提交给网络,最后从网络相应中提取出用户响应。
下面来看源码实现:

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
public final class BridgeInterceptor implements Interceptor {

private final CookieJar cookieJar;

public BridgeInterceptor(CookieJar cookieJar) {
this.cookieJar = cookieJar;
}

@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();

RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}

long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}

if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}

if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}

// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}

List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}

if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}

Response networkResponse = chain.proceed(requestBuilder.build());

HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);

if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}

return responseBuilder.build();
}
}

可以看到,BridgeInterceptor中的实现就比较简单了。主要做了如下的工作:

  • 设置内容长度,内容编码
  • 设置gzip编码,并在接收到内容后进行解压。
  • 添加cookie
  • 设置其他的报头,如User-Agent,Host,Keep-Alive等。

CacheInterceptor拦截器

作用:主要负责Cache的管理
源码分析:

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
public final class CacheInterceptor implements Interceptor {
final InternalCache cache;

public CacheInterceptor(InternalCache cache) {
this.cache = cache;
}

@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;

long now = System.currentTimeMillis();

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

if (cache != null) {
cache.trackResponse(strategy);
}

if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}

// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}

Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}

// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();

// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}

Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();

if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}

if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
...

通过上面代码就可以分析出来,CacheInterceptor就是负责管理cache的,具体体现如下:

  • 当网络请求有符合要求的cache时直接返回Cache
  • 当服务器返回内容有改变时更新当前cache
  • 如果当前cache失效,则删除

ConnectInterceptor拦截器

作用:与服务端建立连接。
具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;

public ConnectInterceptor(OkHttpClient client) {
this.client = client;
}

@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();

// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}

上面的代码比较简单,我们简单进行分析。在intercept方法中,通过第15行代码创建了一个httpCodec对象,他将在后面的步骤中用到。简单介绍一下httpCodec,他其实就是对HTTP协议操作的抽象,具体实现有Http1Codec(对象HTTP1.1)、Http2Codec(对应Http2.0)两种。
然后通过第16行与服务端建立联系,因为里面的代码比较多,就不展开了。

##CallServerInterceptor拦截器
作用:发送和接收数据。
具体源码如下:

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
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();

long sentRequestMillis = System.currentTimeMillis();
httpCodec.writeRequestHeaders(request);

Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return what
// we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
responseBuilder = httpCodec.readResponseHeaders(true);
}

if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else if (!connection.isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from
// being reused. Otherwise we're still obligated to transmit the request body to leave the
// connection in a consistent state.
streamAllocation.noNewStreams();
}
}

httpCodec.finishRequest();

if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}

Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
//。。。省略部分代码。。。
return response;
}

作为拦截连中最后的一个拦截器,我们有必要对其进行分析。我们还是只针对核心部分进行解析:这段代码主要做了这么几件事:

  1. 首先获取HttpCodec对象,至于这个对象的产生在前面的ConnectInterceptor拦截器中。
  2. 通过writeRequestHeaders方法将request写入头部;
  3. 判断是否有需要写入请求的body部分,最后调用finishRequest将所有的数据刷新给底层的Sokcet;
  4. 通过调用readResponseHeaders方法读取响应的头部,;
  5. 然后通过构建一个新的Response对象,并通过openResponseBody获取返回的body
  6. 最后将构建好的response对象返回。

##总体流程图
通过上面的分析,对Okhttp网络请求的流程应该已经有一个比较清晰的认识了,下面是大神总结的一张整体流程图。
okHttp流程图
这篇文章只是对Okhttp整体做流程进行分析,很多的细节部分并没有深入去了解。譬如缓存管理,比如真正的网络请求,譬如IO操作。我们都只停留在具体的方法上。通过这篇文章,我们只知道,Okhhtp的底层是通过Socket进行通信的,利用OkIo来进行高效的IO操作,在缓存方面,使用了LRUCache算法。具体的细节,这里就不展开具体去深入了。

整体流程分析

通过上面的简单使用来看,可以初步看出okhttp的整体流程。
总体架构图
上图是OkHttp的总体架构,大致可以分为以下几层:

  • Interface——接口层:接受网络访问请求
  • Protocol——协议层:处理协议逻辑
  • Connection——连接层:管理网络连接,发送新的请求,接收服务器访问
  • Cache——缓存层:管理本地缓存
  • I/O——I/O层:实际数据读写实现
  • Inteceptor——拦截器层:拦截网络访问,插入拦截逻辑

Okhttp的优势与特点

  • 支持HTTPS/HTTP2/WebSocket等协议
  • 友好支持并发访问,支持多路复用
  • 提供拦截器

参考资料

ImageLoader是最早开源的 Android 图片缓存库, 强大的缓存机制, 早期使用这个图片加载框架的android应用非常多, 至今仍然有不少Android 开发者在使用。

ImagerLoader特征

  1. 支持本地、网络图片,且支持图片下载的进度监听
  2. 支持个性化配置ImagerLoader,如线程池,内存缓存策略,图片显示选项等
  3. 三层缓存加快图片的加载速度
  4. 支持图片压缩

开始使用

鉴于这篇是对ImageLoader源码来进行解析,我们首先回顾一下ImageLoader的使用。
可以通过这里下载universal-imager-loader的jar包,并将其导入到自己的项目中。
然后可以在Application或者Activity中初始化ImageLoade,参考如下:

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

@Override
public void onCreate() {
super.onCreate();
//创建默认的ImageLoader配置参数
ImageLoaderConfiguration configuration = ImageLoaderConfiguration
.createDefault(this);

//Initialize ImageLoader with configuration.
ImageLoader.getInstance().init(configuration);
}
}

当然,如果涉及到网络操作和磁盘缓存的话,有或者是在Application中进行初始化的话,记得要在Manifest中进行申明:

1
2
3
4
5
6
7
8
9
<manifest>  
<uses-permission android:name="android.permission.INTERNET" />
<!-- Include next permission if you want to allow UIL to cache images on SD card -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
<application android:name="YourApplication">
...
</application>
</manifest>

接下来我们就可以愉快的来加载图片了,如下所示:

1
ImageLoader.getInstance().displayImage(imageUri, imageView);

当然,如果你想添加监听,可以这么写:

1
2
3
4
5
6
7
8
9
10
ImageLoader.getInstance().loadImage(imageUrl, new SimpleImageLoadingListener(){  

@Override
public void onLoadingComplete(String imageUri, View view,
Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
mImageView.setImageBitmap(loadedImage);
}

});

至于更多的用法这里就不介绍了,如果有需要,可以参看这篇博客,了解更多关于ImageLoader的用法。下面就开始了源码的解析之路。

ImageLoaderConfiguration配置实现

我们首先还是从imageLoader的配置开始开始源码的探究之旅把。在上面的使用实例中,我们使用createDefault()方法来初始化配置,那么imageLoader的默认配置究竟是些什么呢?下面直接上代码:

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
public static ImageLoaderConfiguration createDefault(Context context) {
return new Builder(context).build();
}

public ImageLoaderConfiguration build() {
initEmptyFieldsWithDefaultValues();
return new ImageLoaderConfiguration(this);
}

private void initEmptyFieldsWithDefaultValues() {
if (taskExecutor == null) {
taskExecutor = DefaultConfigurationFactory
.createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
} else {
customExecutor = true;
}
if (taskExecutorForCachedImages == null) {
taskExecutorForCachedImages = DefaultConfigurationFactory
.createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
} else {
customExecutorForCachedImages = true;
}
if (diskCache == null) {
if (diskCacheFileNameGenerator == null) {
diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
}
diskCache = DefaultConfigurationFactory
.createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
}
if (memoryCache == null) {
memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);
}
if (denyCacheImageMultipleSizesInMemory) {
memoryCache = new FuzzyKeyMemoryCache(memoryCache, MemoryCacheUtils.createFuzzyKeyComparator());
}
if (downloader == null) {
downloader = DefaultConfigurationFactory.createImageDownloader(context);
}
if (decoder == null) {
decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
}
if (defaultDisplayImageOptions == null) {
defaultDisplayImageOptions = DisplayImageOptions.createSimple();
}
}
}

private ImageLoaderConfiguration(final Builder builder) {
resources = builder.context.getResources();//程序本地资源访问器
maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache;//内存缓存的图片最大宽度
maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache;//内存缓存的图片最大高度
maxImageWidthForDiskCache = builder.maxImageWidthForDiskCache;//磁盘缓存的图片最大宽度
maxImageHeightForDiskCache = builder.maxImageHeightForDiskCache;//磁盘缓存的图片最大高度
processorForDiskCache = builder.processorForDiskCache;//图片处理器,用于处理从磁盘缓存中读取到的图片
taskExecutor = builder.taskExecutor;//ImageLoaderEngine中用于执行从源获取图片任务的 Executor。
taskExecutorForCachedImages = builder.taskExecutorForCachedImages;//ImageLoaderEngine中用于执行从缓存获取图片任务的 Executor。
threadPoolSize = builder.threadPoolSize;//上面两个默认线程池的核心池大小,即最大并发数。
threadPriority = builder.threadPriority;//上面两个默认线程池的线程优先级。
tasksProcessingType = builder.tasksProcessingType;//上面两个默认线程池的线程队列类型。目前只有 FIFO, LIFO 两种可供选择。
diskCache = builder.diskCache;//图片磁盘缓存,一般放在 SD 卡。
memoryCache = builder.memoryCache;//图片内存缓存。
defaultDisplayImageOptions = builder.defaultDisplayImageOptions;//图片显示的配置项。比如加载前、加载中、加载失败应该显示的占位图片,图片是否需要在磁盘缓存,是否需要在内存缓存等。
downloader = builder.downloader;//图片下载器。
decoder = builder.decoder;//图片解码器,内部可使用我们常用的BitmapFactory.decode(…)将图片资源解码成Bitmap对象。

customExecutor = builder.customExecutor;//用户是否自定义了上面的 taskExecutor。
customExecutorForCachedImages = builder.customExecutorForCachedImages;//用户是否自定义了上面的 taskExecutorForCachedImages。

networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader);//不允许访问网络的图片下载器。
slowNetworkDownloader = new SlowNetworkImageDownloader(downloader);//慢网络情况下的图片下载器。

L.writeDebugLogs(builder.writeLogs);
}

上面的代码有点多,但是很简单也很清晰,就是一些列初始化的代码。通过一些系列的调用,在initEmptyFieldsWithDefaultValues方法中对一些没有配置的进行的项进行配置,并通过ImageLoaderConfiguration给出默认的参数配置。对于其中的一些配置,在上面的注释中已经表明,ImageLoaderConfiguration中默认的配置,可以参考第48-73行。
至于initEmptyFieldsWithDefaultValues中的配置,在这里进行简单的介绍:

  • taskExecutor 从源获取图片任务的线程池
  • taskExecutorForCachedImages 用于执行从缓存获取图片任务的线程池
    前面两个线程池的参数如下:
    核心线程数 最大线程数 空闲线程等待时间 容器
    3 3 0s 2
    前面两个线程池如果用户自定义的相应的线程池来实现的话,就会将customExecutor置为true,或将customExecutorForCachedImages置为true。其实customExecutor存在的意义就在于判断用户有没有自定义从源获取图片任务的线程池,customExecutorForCachedImages存在的意义判断在于用户判断用户有没有重写从缓存获取图片的线程池。
  • diskCacheFileNameGenerator 默认实现为HashCodeFileNameGenerator,即用mageUri.hashCode()值当前图片名字。
  • diskCache用于表示图片磁盘的缓存,默认实现为createDiskCache,默认的算法为LruDiskCache算法,缓存的目录为SD卡下的/data/data/" + context.getPackageName() + "/cache/uil-images目录下。
  • memoryCache用于表示图片内存的缓存,默认实现为createMemoryCache,默认使用的算法为LruMemoryCache
  • denyCacheImageMultipleSizesInMemorytrue时,表示内存缓存不允许缓存一张图片的多个尺寸。这个时候用通过FuzzyKeyMemoryCache来构建memoryCache
  • downloader表示图片下载器,默认实现为createImageDownloader,最终通过BaseImageDownloader构建下载器,其下载器中重要的两个参数分别为:连接超时时间connectTimeout默认值为5分钟,读取超时时间readTimeout默认值为20分钟。
  • decoder 表示图片解码器,默认实现为createImageDecoder,最终通过BaseImageDecoder实现。
  • defaultDisplayImageOptions 表示默认参数,最终回调到DisplayImageOptions方法中,里面设计相关的参数初始化。这里就不展开了。

加载配置

我们首先看ApplicationimgaerLoader设置配置的方法。

1
ImageLoader.getInstance().init(configuration);

接下来我们继续分析上面的代码是如何将配置应用到ImageLoader中的。首先是ImageLoader.getInstance()实例化一个ImageLoader,通过代码来看实例化的过程:

1
2
3
4
5
6
7
8
9
10
public static ImageLoader getInstance() {
if (instance == null) {
synchronized (ImageLoader.class) {
if (instance == null) {
instance = new ImageLoader();
}
}
}
return instance;
}

可以看出来,getInstance就是获取一个ImageLoader实例,运用了一个双重锁的单利模式,很简单,就不做解释了。
重点看init方法。具体在ImageLoader类中的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
public synchronized void init(ImageLoaderConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
}
if (this.configuration == null) {
L.d(LOG_INIT_CONFIG);
engine = new ImageLoaderEngine(configuration);
this.configuration = configuration;
} else {
L.w(WARNING_RE_INIT_CONFIG);
}
}

可以看出来,init的实现也是非常简单的。首先判断传入的configuration参数是否为空,为空就直接抛出一个异常,不为空就判断当前类属性configuration是否为空,类中configuration属性为空时调用ImageLoaderEngine构建engine对象,否则就打印警告日志。所以整个方法中最重要的一个语句就是new ImageLoaderEngine(configuration);。这里首先介绍一个ImageLoaderEngine类的作用。简单描述就是ImageLoaderEngine是任务分发器,负责分发LoadAndDisplayImageTaskProcessAndDisplayImageTask给具体的线程池去执行。具体实现后面会讲到。

加载图片

通过上面两个步骤,imgaeLoder的参数配置已经设置完毕,接下来我们就可以用imageLoader加载图片了。下面是三种加载图片的方式:
加载方式一,异步加载并显示图片到对应的imagerAware上

1
ImageLoader.getInstance().displayImage(imageUrl,imageView);

加载方式二,异步加载图片并执行回调接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ImageLoader.getInstance().loadImage(imageUrl,new  ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {

}

@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {

}

@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {

}

@Override
public void onLoadingCancelled(String imageUri, View view) {

}
});

加载方式三,同步加载图片

1
ImageLoader.getInstance().loadImageSync(imageUrl);

针对上面三种方法,我们先分析第一种加载图片的方法,其余的两种加载图片的分析也差不多,后面就不具体分析了,只是简单的体现其不同点。
我们来看displayImage方法在ImageLoader类中的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void displayImage(String uri, ImageView imageView) {
displayImage(uri, new ImageViewAware(imageView), null, null, null);
}

public void displayImage(String uri, ImageView imageView, ImageSize targetImageSize) {
displayImage(uri, new ImageViewAware(imageView), null, targetImageSize, null, null);
}

public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {
displayImage(uri, new ImageViewAware(imageView), options, null, null);
}

public void displayImage(String uri, ImageView imageView, ImageLoadingListener listener) {
displayImage(uri, new ImageViewAware(imageView), null, listener, null);
}

public void displayImage(String uri, ImageView imageView, DisplayImageOptions options,
ImageLoadingListener listener) {
displayImage(uri, imageView, options, listener, null);
}

我们看到上面的displayImage有很多中重载的方法,最终他们都会调用到下面的这个displayImage方法中来。

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
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
//省略了部分判空代码

...
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

listener.onLoadingStarted(uri, imageAware.getWrappedView());

Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}

ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}

从第6行开始看,当没有传入targetSize目标尺寸时,会通过第6行的代码产生一个合适的尺寸。具体逻辑为,当image没有尺寸时就采用测量出来的最大尺寸,当image有尺寸时就用image本身的尺寸。获取最大尺寸的逻辑为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ImageSize getMaxImageSize() {
//获取屏幕像素
DisplayMetrics displayMetrics = resources.getDisplayMetrics();

int width = maxImageWidthForMemoryCache;//最大的图片内存缓存宽度
if (width <= 0) {
width = displayMetrics.widthPixels;//屏幕宽度
}
int height = maxImageHeightForMemoryCache;//最大的图片内存缓存高度
if (height <= 0) {
height = displayMetrics.heightPixels;//屏幕高度
}
return new ImageSize(width, height);
}

即最大的尺寸为:如设置了maxImageWidthForMemoryCache值且该值大于0,则最大尺寸为其设置的值,否则屏幕宽度。在高度上也一样,就不赘述了。综上,我们可以知道要显示图片的大小的逻辑,我们设置了图片显示的尺寸,则图片尺寸为我们设置的尺寸。否则图片的本身有尺寸的时候,显示的就是自己本身的尺寸,否则就显示最大的图片尺寸。当最大图片内存缓存尺寸大于0时,最大图片尺寸即为最大图片内存尺寸,否则为屏幕尺寸。
分析了那么久,其实还只是分析了displayImage方法的一个方法,下面我们继续看displayImage中的实现。在计算好目标图片的尺寸之后,利用generateKey方法生成一个memoryCacheKey,这里的memoryCacheKey的组成形式为URI + size,用于表示要加载到内存中的图片。通过第10行代码,将要加载的图片加入到cacheKeysForImageAwares队列中,他是一个Collections.synchronizedMap(new HashMap<Integer, String>())类型的队列,他用来记录正在加载的任务,加载图片的时候会将ImageViewid和图片的url加上尺寸加入到HashMap中,加载完成之后会将其移除。然后通过第12行的代码回调onLoadingStarted方法,这个方法就是我们在使用时的onLoadingStarted方法回调,具体参考上面的加载方式二,异步加载图片并执行回调接口的使用实例。
对于最终调用的displayImage方法代码很重要,所以我们继续往下分析其中的代码。以下的代码已经省略前面已经分析的代码,完整代码参考前面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {//本地能获取到图片
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

if (options.shouldPostProcess()) {
---
//缺失的代码片段1
---
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
---
这里实现从网络上获取图片的逻辑
//缺失的代码片段2
---
}

首先从内存中拿出将要加载的图片(bitmap格式),然后在图片不为空且没被回收的基础上开始加载图片的逻辑。第5行中有一个判断,我们如果在DisplayImageOptions中设置了postProcessor就进入true逻辑,不过默认postProcessor是为null的,BitmapProcessor接口主要是对Bitmap进行处理,这个框架并没有给出相对应的实现,如果我们有自己的需求的时候可以自己实现BitmapProcessor接口(比如将图片设置成圆形的)。我们先分析默认情况,即shouldPostProcessfalse的情况下执行的第16-17行代码。第16行代码就将Bitmap设置到ImageView上面,这里我们可以在DisplayImageOptions中配置显示需求displayer,默认使用的是SimpleBitmapDisplayer,直接将Bitmap设置到ImageView上面。代码如下:

1
2
3
4
5
6
public final class SimpleBitmapDisplayer implements BitmapDisplayer {
@Override
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}
}

当然,ImageLoader也为我们提供了其他显示的方式,如CircleBitmapDisplayer(),FadeInBitmapDisplayer,RoundeBitmapDisplayer三种显示方式。第17行代码很好理解,就是回调到onLoadingComplete方法,提供给用户的回调。
接下来我们来看当用于设置了postProcessor下情况的逻辑,即上面缺失的代码片段1:

1
2
3
4
5
6
7
8
9
10
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}

这里代码执行的情况就是用于需要回调图片记载的进度时执行,即用户指定了postProcessor对象,而postProcessor主要用于表示缓存在内存之后的处理程序。
其中的ImageLoadingInfo主要用来加载和显示图片任务需要的信息,ProcessAndDisplayImageTask主要用于处理并显示图片的任务,他实现了Runnable接口。然后通过isSyncLoading判断是同步还是异步,当isSyncLoading为ture时表示当前是同步执行。这里还有一个点需要特别说明以下:我们看第1行代码中的engine.getLockForUri(uri),这个方法主要是用来给图片的URl加锁的,那么给URL要传入这个一个参数给ImageLoadingInfo呢?其实主要是实现对图片的复用,考虑这样一种场景,在一个LitView中,某个Item正在获取图片的过程中,我们将这个item滚出界面后又将其滚进来,滚进来如果没有加锁,该item又会去加载一次图片,为了避免多次对同一个URL重复请求,有必要对正在加载的URL加锁,当图片加载完成之后,就将锁释放掉。
我们在来分析同步执行的情况,直接执行run(),通过displayTask任务来执行,我们来了解ProcessAndDisplayImageTaskrun()方法里面的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void run() {
L.d(LOG_POSTPROCESS_IMAGE, imageLoadingInfo.memoryCacheKey);

BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor();
//图片处理器,用于处理从磁盘缓存中读取到的图片。
Bitmap processedBitmap = processor.process(bitmap);
//处理图片
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine,
LoadedFrom.MEMORY_CACHE);
//构建图片实现的任务
LoadAndDisplayImageTask.runTask(displayBitmapTask, imageLoadingInfo.options.isSyncLoading(), handler, engine);
//执行图片显示任务
}

可以看到,在从本地读取到图片的显示逻辑还是很简单的,run方法核心只有四行代码,首先对图片进行相对应的处理,然后构建图片显示的任务,最后执行图片显示的任务就OK了。我们来看DisplayBitmapTask中具体做了什么:

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
final class DisplayBitmapTask implements Runnable {

...
private final Bitmap bitmap;
private final String imageUri;
private final ImageAware imageAware;
private final String memoryCacheKey;
private final BitmapDisplayer displayer;
private final ImageLoadingListener listener;
private final ImageLoaderEngine engine;
private final LoadedFrom loadedFrom;

public DisplayBitmapTask(Bitmap bitmap, ImageLoadingInfo imageLoadingInfo, ImageLoaderEngine engine,
LoadedFrom loadedFrom) {
this.bitmap = bitmap;
imageUri = imageLoadingInfo.uri;
imageAware = imageLoadingInfo.imageAware;
memoryCacheKey = imageLoadingInfo.memoryCacheKey;
displayer = imageLoadingInfo.options.getDisplayer();
listener = imageLoadingInfo.listener;
this.engine = engine;
this.loadedFrom = loadedFrom;
}

@Override
public void run() {
if (imageAware.isCollected()) {//如果要显示的图片已经被GC回收
//回调onLoadingCancelled接口
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {//如果
//回调onLoadingCancelled接口
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
//正在显示图片的逻辑
displayer.display(bitmap, imageAware, loadedFrom);
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}

/** Checks whether memory cache key (image URI) for current ImageAware is actual */
//检查内存中当前图片的key是否是真实存在的
private boolean isViewWasReused() {
String currentCacheKey = engine.getLoadingUriForView(imageAware);
return !memoryCacheKey.equals(currentCacheKey);
}
}

上面run方法中的逻辑也比较清晰,首先对是否能进行图片显示的环境做一定的判断,在当前环境可以显示图片的前提下,利用BitmapDisplayer中的display方法显示图片,然后通过cancelDisplayTaskFor方法将当前显示的图片从cacheKeysForImageAwares队列中移除。这里的cacheKeysForImageAwares指的是ImageAware与内存缓存key对应的mapkeyImageAwareidvalue为内存缓存的key。完成之后就回调onLoadingComplete方法。
但是注意到,在ProcessAndDisplayImageTask中,并没有直接将displayBitmapTask通过start或者是run方法将其执行,而是通过一个LoadAndDisplayImageTask中的runTask方法,我们来看其实现:

1
2
3
4
5
6
7
8
9
static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
if (sync) {
r.run();
} else if (handler == null) {
engine.fireCallback(r);
} else {
handler.post(r);
}
}

从实现上来说,还是比较简单的。如果是同步加载的话,就直接调用run方法,否则(异步执行)就调用handler调用post方法将其投递到主线程中去执行,这个handler的实现在ImageLoader中。如果handler为空的话,就取消图片显示,直接处理善后工作。这个handlerImageLoader中的实现如下:

1
2
3
4
5
6
7
8
9
private static Handler defineHandler(DisplayImageOptions options) {
Handler handler = options.getHandler();
if (options.isSyncLoading()) {
handler = null;
} else if (handler == null && Looper.myLooper() == Looper.getMainLooper()) {
handler = new Handler();
}
return handler;
}

可以看出来,handler的创建也只会在异步加载的时候才会创建,同步情况下不会创建handler。

从网络上加载图片

分析完本地加载图片后,我们来分析上面displayImage中缺失的代码片段2,即本地无法获取图片的图片加载逻辑,我们先来看其中的代码:

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
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
...
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
...
} else {
//这下面的代码就是在本地无法获取图片的情况下加载图片的逻辑
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}

ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}

可以看到,在无法获取本地图片情况下加载图片的逻辑稍微比本地加载图片的逻辑稍微多一点,但实现上很多方法是相同的,我们一点一点来开始分析:

  1. 首先利用shouldShowImageOnLoading方法判断在加载的过程中是否需要显示图片,当用户设置了imageResOnLoading占位图片资源id,或者设置了加载中占位图片drawable对象时其返回值为ture,即执行在图片加载过程中显示占位图片的逻辑。
  2. 在用户没有设置占位图片的情况下,会继续判断是否需要重设图片,若需要重设图片,就将图片设为null。
  3. 至于这里的ImageLoadingInfo(加载和显示图片任务需要的信息)和前面的实现一样,这里就不重复介绍了。
  4. 这里的LoadAndDisplayImageTask,为下载和显示图片任务,用于从网络、文件系统或者内存获取图片并解析,然后调用DisplayBitmapTaskImageAware中显示图片。
  5. 在同步加载的情况下,直接运行displayTask
  6. 异步加载的情况下,将displayTask提交到taskDistributor线程池中运行。
    接下来,我们就具体分析LoadAndDisplayImageTask中的run方法。下面是LoadAndDisplayImageTask类中run的具体实现:
    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public void run() {
    if (waitIfPaused()) return;
    if (delayIfNeed()) return;

    ---
    暂时神略
    }
    可以看到,LoadAndDisplayImageTask中的run()方法里面的逻辑还是稍微有点复杂的。我们一点一点来分析;
    首先看前面两个方法的实现,即3-4行的代码实现,他们在LoadAndDisplayImageTask类中的实现代码如下:
    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
    //主要用于判断当前线程是否被打断,被打断返回ture,否则返回isTaskNotActual()的返回值
    private boolean waitIfPaused() {
    AtomicBoolean pause = engine.getPause();
    if (pause.get()) {
    synchronized (engine.getPauseLock()) {
    if (pause.get()) {
    L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
    try {
    engine.getPauseLock().wait();
    } catch (InterruptedException e) {
    L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
    return true;
    }
    L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
    }
    }
    }
    return isTaskNotActual();
    }

    //主要用于判断是否需要预处理,不要要返回false,需要返回isTaskNotActual()的返回值
    private boolean delayIfNeed() {
    if (options.shouldDelayBeforeLoading()) {
    L.d(LOG_DELAY_BEFORE_LOADING, options.getDelayBeforeLoading(), memoryCacheKey);
    try {
    Thread.sleep(options.getDelayBeforeLoading());
    } catch (InterruptedException e) {
    L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
    return true;
    }
    return isTaskNotActual();
    }
    return false;
    }

    //主要用于判断imageVire是否被回收和重用,满足其中一个条件返回ture,否则返回false
    private boolean isTaskNotActual() {
    return isViewCollected() || isViewReused();
    }

    //主要用于判断ImageView是否被GC回收了,回收了返回ture,否者返回false
    private boolean isViewCollected() {
    if (imageAware.isCollected()) {
    L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
    return true;
    }
    return false;
    }

    //主要用于判断imageView是否被重用,被重用返回true,否则返回false
    private boolean isViewReused() {
    String currentCacheKey = engine.getLoadingUriForView(imageAware);
    // Check whether memory cache key (image URI) for current ImageAware is actual.
    // If ImageAware is reused for another task then current task should be cancelled.
    boolean imageAwareWasReused = !memoryCacheKey.equals(currentCacheKey);
    if (imageAwareWasReused) {
    L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
    return true;
    }
    return false;
    }
    看起来好像也有点复杂,但是并不难,我们直接一路看过去就好了。每个方法的作用我都已经在代码中添加了注释了,这里我们来整理一下思路:通过上面的源码,我们基本上可以确定他们的作用是什么了,但是为什么需要他们呢?我们试想这样一种场景,在使用ListView来显示图片时,在手指滑动的时候一般不会去加载图片,因为在这个过程中很多图片是没有必要加载的。这个时候我们就可以通过PauseOnScrollListener(ImageLoader imageLoader, boolean pauseOnScroll, boolean pauseOnFling)来控制在滑动过程中图片的加载。第一个参数用来控制手指按着滑动情况下的是否加载图片,第二个参数用来控制手指松开后时候加载图片。至于中间参数的参数和值的传递比较简单,这里就不全部给出来了,可以自行通过查看源码了解pauseOnScroll是如何改变waitIfPaused方法中pause的值的(默认为false)。
    至于isViewReused的方法存在的意义就更好理解了,在ListView中存在一种复用的优化策略,即在ListView在滑动时,会复用Item,为了避免图片显示时的错位情况,在ImageLoader就通过isViewReused来解决这个问题。

接下来我们继续看LoadAndDisplayImageTask中的run()方法中剩下的代码:

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
@Override
public void run() {
...
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;//获取锁
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) { //判断锁是否被持有
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}

loadFromUriLock.lock();/加锁
Bitmap bmp;
try {
checkTaskNotActual();//判断当前请求是否是可实现的,(当imageView被GC回收或者此次请求的URL无法获取imageView时时为不可实现的请求)

bmp = configuration.memoryCache.get(memoryCacheKey);//尝试从内存中加载图片
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();//尝试从文件中加载图片,如果没有再去网络中获取,然后将bitmap保存在文件系统中。
//这个方法是重点,后面会进行讲到
if (bmp == null) return; // listener callback already was fired

checkTaskNotActual();
checkTaskInterrupted();//用于判断当前任务有没有被打断,被打断直接抛出异常

if (options.shouldPreProcess()) {//默认为ture,表示缓存在内存之前没有要处理的程序
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);//对bitmap进行适当的剪裁
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
}

if (bmp != null && options.isCacheInMemory()) {//如果有必要缓存到内存中的话
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
configuration.memoryCache.put(memoryCacheKey, bmp);//将图片保存到内存缓存中去
}
} else {
loadedFrom = LoadedFrom.MEMORY_CACHE;
L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
}

if (bmp != null && options.shouldPostProcess()) {
L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPostProcessor().process(bmp);//自定义的bitmap操作会在这里进行
if (bmp == null) {
L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
}
}
checkTaskNotActual();
checkTaskInterrupted();
} catch (TaskCancelledException e) {
fireCancelEvent(); //解移除的监听 上面很多方法会抛出异常都需要这个方法来移除监听
return;
} finally {
loadFromUriLock.unlock();//释放锁
}

DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom); //构建显示任务
runTask(displayBitmapTask, syncLoading, handler, engine);//将图片显示到指定的imageView上
}

上面的代码也有点多,但是在添加了先关的注解之后,详细阅读起来还是比较简单的。这里在梳理一下整个LoadAndDisplayImageTask中的run方法的相关逻辑。首先会判断当前是否是可以加载图片的状态,不可以加载图片的话就直接返回,什么都不做。在可以加载图片的前提下,会给以下的核心逻辑代码添加一个锁:【首先尝试从内存中获取图片,没有对应的图片就会从磁盘中寻找,如果磁盘中也找不到,那么就只能从网络中去需找,在找到图片后将其存在文件系统中,如果用户定义了图片的预处理,就会执行用户定义的图片预处理,如果需要缓存到内存就会缓存到内存中,继而执行用户定义的图片后处理(提前是用户定义了图片后处理),最后判断一下当前状态是否还可以显示图片,若当前状态不能显示图片就会直接抛出异常,在catch语句中移除相关的监听。如果当前状态还可以显示图片,在finally语句中释放锁 】以此保障多线程的可靠性,然后执行图片显示任务将图片显示到图片上,到此完成了整个图片的加载。用流程图表示如下:
图片加载流程

下面我们来分析上面run()方法中最重要的一个方法tryLoadBitmap(),他的实现也在LoadAndDisplayImageTask类中,实现代码如下:

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
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
File imageFile = configuration.diskCache.get(uri);//先判断文件中有没有该文件
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {//如果文件中有该文件,就直接调用decodeImage去解码图片
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE;

checkTaskNotActual();//判断当前是否具有加载图片的状态,这个方法在前面已经解析过了
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));//解码图片
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
//表示文件中没有找到图片,就会指定到网络上获取bitmap,
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK;

String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
//options.isCacheOnDisk()用来表是否需要将图片缓存到文件系统中,默认为fasle。
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}

checkTaskNotActual();
bitmap = decodeImage(imageUriForDecoding);

if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
} catch (IllegalStateException e) {
fireFailEvent(FailType.NETWORK_DENIED, null);
} catch (TaskCancelledException e) {
throw e;
} catch (IOException e) {
L.e(e);
fireFailEvent(FailType.IO_ERROR, e);
} catch (OutOfMemoryError e) {
L.e(e);
fireFailEvent(FailType.OUT_OF_MEMORY, e);
} catch (Throwable e) {
L.e(e);
fireFailEvent(FailType.UNKNOWN, e);
}
return bitmap;
}

上面的代码虽然看起来有点多,但是逻辑还是很清晰的,我在关键的地方都添加了注释,相信阅读起来很简单。这里再次梳理一下tryLoadBitmap的逻辑吧。首先从尝试从文件中去获取图片,如果能从文件中获取图片的话,就判断当前状态是否可以加载图片,然后通过decodeImage方法将图片解码成可以显示的格式。如果文件中没有要显示的图片,在设置了从网络获取图片的前提下就会利用tryCacheImageOnDisk方法从网络上获取图片,然后将图片解码成要显示的格式,可以参考下面的流程图:
tryLoadBitmap流程图
在上面的流程中,我们对其中两个重要的方法来进一步的探究其实现,一个方法是decodeImage,另一个是tryCacheImageOnDisk()。着两个方法的实现源码如下,他们都在LoadAndDisplayImageTask类中。

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 Bitmap decodeImage(String imageUri) throws IOException {
//获取图片的
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
return decoder.decode(decodingInfo);
}

/** @return <b>true</b> - if image was downloaded successfully; <b>false</b> - otherwise */
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);

boolean loaded;
try {
loaded = downloadImage();
if (loaded) {
int width = configuration.maxImageWidthForDiskCache;
int height = configuration.maxImageHeightForDiskCache;
if (width > 0 || height > 0) {
L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey);
resizeAndSaveImage(width, height); // TODO : process boolean result
//解码成bitmap图片,并保存他。关于这个方法就不在深入了。
}
}
} catch (IOException e) {
L.e(e);
loaded = false;
}
return loaded;
}

//负责下载图片,并将其保存到文件缓存中
private boolean downloadImage() throws IOException {
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
if (is == null) {
L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
return false;
} else {
try {
return configuration.diskCache.save(uri, is, this);
} finally {
IoUtils.closeSilently(is);
}
}
}

这里就不对上面的代码进行解释了,我们直接看decode方法在BaseImageDecoder中的具体实现,至于其他的方法,请参考注释。

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
@Override
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;

InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
return null;
}
try {
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}

if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}

综上,我们暂时分析完了run()方法中逻辑和主要方法。接下来我们继续分析异步的情况,这里再次贴出之前displayImage的主要流程代码。因为之前的代码隔得有点远了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
---
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
if (options.shouldPostProcess()) {
...
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
}else{
...
缺失的代码片段2
}
}

我们之前已经分析了displayTask.run();的主要流程,接下来我们分析异步的执行engine.submit(displayTask);的主要流程和方法,他的实现主要在ImageLoaderEngine中,submit的实现如下:

1
2
3
4
void submit(ProcessAndDisplayImageTask task) {
initExecutorsIfNeed();
taskExecutorForCachedImages.execute(task);
}

方法很简单,首先只有两个方法调用,第一行代码从名字分析就应该是用来初始化Executor的(有必要的话),然后执行将此次任务提交到线程池中运行。在线程池中的执行也会执行调用之前的run方法,这里就不再分析了。我们分析一下第一行代码,验证一下我们的猜想是不是正确的。

1
2
3
4
5
6
7
8
9
private void initExecutorsIfNeed() {
if (!configuration.customExecutor && ((ExecutorService) taskExecutor).isShutdown()) {
taskExecutor = createTaskExecutor();
}
if (!configuration.customExecutorForCachedImages && ((ExecutorService) taskExecutorForCachedImages)
.isShutdown()) {
taskExecutorForCachedImages = createTaskExecutor();
}
}

从上面代码分析:首先判断当前的taskExecutor是不是关闭了,如果处于关闭状态就创建一个新的Executor,这里的taskExecutor指的是用与执行从源获取图片任务的线程池。然后判断taskExecutorForCachedImages是不是就绪,如果他被关闭的话就创建一个新的线程池taskExecutorForCachedImages,用于执行从缓存获取图片任务的线程池。综上,源码验证了我们之前的猜测,initExecutorsIfNeed方法的确是用来初始化相关线程池的。

displayImage方法总结

从上面的流程中,可以明显看出来,displayImage方法就是imageLoader加载图片的核心,我们在这里在来总结一下整个displayImage的逻辑,先将整个displayImage代码完整的贴上来:

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
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}

if (TextUtils.isEmpty(uri)) {
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}

if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

listener.onLoadingStarted(uri, imageAware.getWrappedView());

Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}

ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}

将上面代码用流程图可以表示为以下的表现形式。
displayImage

(原创图,敬请批评指正)

可以看到,在上面流程图算比较复杂,但是逻辑很清晰,基本上所有的功能集中在displayImage中进行调度使用,所以给我们分析ImageLoader降低了不少的难度。
至于上面流程图中没有具体体现的任务可以参考前面的分析。

针对上面三种显示图片的方法,最终都会通过调用displayImage来实现,只是对其中的参数进行了一定的设置,这里就不在详细介绍了,有兴趣的可以自己查阅源码。

LRUCache和DisLruCacher分析

LRUCache

LruCache是android 3.1所提供的一个缓存类,他是一个泛型类,他内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了getput方法来完成缓存的获取和添加属性,当缓存满时,LruCache会移除较早使用的缓存对象,然后在添加新的缓存对象。
构造方法如下:

1
2
3
4
5
6
7
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}

可以看到,LruCache的构造方法非常简单,只需要传入一个maxSize设置最大的缓存对象即可,然后实例化map对象。
这里也附上get和put的源码:

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
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}

V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}

V createdValue = create(key);
if (createdValue == null) {
return null;
}

synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);

if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}

if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}


public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}

V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, value);
}

trimToSize(maxSize);
return previous;
}

更多的分析可以参考这里

DisLruCache

DisLruCache用于实现存储设置缓存,即磁盘缓存,他通过将缓存对象写入文件系统从而实现缓存的效果。
我们在这里对其创建,缓存添加和移除缓存进行简单的分析。
创建过程:

1
2
3
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
...简单的赋值,就不贴出来了
}

其中的四个参数分别是:

  • directory表示磁盘存在文件系统中的存储路径;
  • appVersion 表示应用的版本号,一般设置1就可;
  • valueCount 表示单个节点锁对应的数据的个数,一般设为1就可以了;
  • maxSize 表示缓存的总大小,比如50MB,当缓存大小超过这个设置值后,DisLruCache会清除一些缓存从而保证总大小不大于这个设定值。
    当然,DiskLruCache提供了open方法来创建自身:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
    throws IOException {
    if (maxSize <= 0) {
    throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <= 0) {
    throw new IllegalArgumentException("valueCount <= 0");
    }

    // If a bkp file exists, use it instead.
    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
    if (backupFile.exists()) {
    File journalFile = new File(directory, JOURNAL_FILE);
    // If journal file also exists just delete backup file.
    if (journalFile.exists()) {
    backupFile.delete();
    } else {
    renameTo(backupFile, journalFile, false);
    }
    }
  • DisLruCacher的缓存添加:*
    DisLruCache的缓存操作通过Editor完成的,Editor表示一个缓存对象的编辑对象。在ImageLoader的运用,首先需要获取图片的URL所对用的Key,然后根据Key就可以通过edit()方法来获取Editor对象,如果这个缓存正在被编辑,那么edit会返回null,即DisLrucache不允许同时编辑一个缓存对象。之所以要把url转换成key,是因为url中可能有特殊字符,这将影响url在Adnroid中的直接使用,一般采用url的md5值作为key。

DisLruCacher的缓存查找:
缓存查找过程也需要将url转换成key,然后通过DisLrache的get方法得到一个snapshot对象即可得到缓存的文件输入流,进而得到Bitmap对象。为了避免加载图片过程中导致的OOM问题,一般建议不直接加载原始图片,建议先对图片进行压缩之后在去加载。下面是get方法的实现逻辑:

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
public synchronized Value get(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}

if (!entry.readable) {
return null;
}

for (File file : entry.cleanFiles) {
// A file must have been deleted manually!
if (!file.exists()) {
return null;
}
}

redundantOpCount++;
journalWriter.append(READ);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}

return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}

DisLruCacher的缓存删除:
DisLruCacher提供了remove,delete方法来进行磁盘的删除操作。删除通过需要将url转换成key,然后从lruEntriesLinkedHashMap对象中获取该对象,在对象存在的前提下,删除文件中对应的文件,然后移除lruEntries对应的key值。代码实现如下:

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
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}

for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}

redundantOpCount++;
journalWriter.append(REMOVE);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');

lruEntries.remove(key);

if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}

return true;
}

杂谈

下面是我对imageLoader源码分析之后的一些感悟和一些总结,有一些还是面试时被问到的问题,这里一并记录下来。

ImageLoader运用的设计模式

从源码分析上来看,最明显的就是建造者模式和单例模式,这两种模式在实际项目中也是运行最广的设计模式。还使用了工厂模式,装饰者模式,代理模式,策略模式等等。设计模式参考

当ListView显示图片,滚动时ImageLoader是如何避免OOM的?

首先是对缓存进行管理,具体管理内存的方法是LruCache,实现算法是LRU:通过优先淘汰最近最少使用的缓存对象,保证总缓存大小不高于限定值。

LRUCacher算法的具体实现

他内部采用一个LinkedhashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LRUcacher会移除较早使用的缓存对象,然后再添加新的缓存对象。

ImagerLoader的为什么会被淘汰

  • 首先相对于Gilde来说,ImagerLoader的配置相对繁琐,需要对其中的参数有比较详细的了解才能比较好的驾驭ImageLoader,而Gilde简单易用,没有繁琐复杂的配置;
  • Gilde中的内存管理比ImageLoader做的更好,虽然ImageLoader也说有三层缓存,但是实际上是两层,一个磁盘,一个内存缓存。而Gilde中的内存管理做到了两级内存缓存,更加可靠;
  • 在网络请求方面,ImageLoader采用的是HttpConnection,而Gilde默认采用更加高效的okhttp,虽然两者都支持自定义下载器,但是明显Gilde的支持更好。

参考链接