伍中联(Vanda)的博客

当一切理所当然就没有你什么事了 当你说:“这个好麻烦,那么不要犹豫,主动出击”

0%

带着不舍离开

2018春节刚过,从广州到杭州,之间有考虑了多次,换城市是一件非常麻烦的事情,其实伴着也很无奈,

广州
我职业生涯开始的地方
UC
我奋斗成长收获的地方
感谢你们!

世界这么小,所以微笑面对

新坐标

杭州(浙江-杭州-余杭-阿里西溪园区)

记录下我在UC的美好时光

入职UC,在UC是工作开心、玩也开心。
那个时候还玩着dota,跟着彬哥和成哥一起玩dota,然后认识了很多大佬(其实大佬们也爱玩游戏),可能是我游戏玩得比较好(自夸),和大佬们玩我都会默默的买组眼、买好小鸡(毕竟大佬们先发育)^ _ ^。

成哥 — 脾气好技术也溜的man(成哥应该是玩小黑的,老手抖)
彬哥 — 集美貌、智慧、才华于一身的老大(嗯,彬哥的全能还是很到位的)

超哥 — 我们的产品大佬,玩游戏也溜(混沌还是很溜的)
花哥 — 游戏中的花哥怼起队友来,哈哈哈(脑补画面),花哥的白牛冲出去就回不来了。

默哥 — 很佩服默哥处理事情的能力,稳重、得体。反正描述不过来,很厉害的市场营销大佬,默哥的拿手英雄是蚂蚁 (^ ^)

转眼2年半的时间过去了,从UC到夸克,是付出、成长、收获的

记录下我在夸克的美好时光

当得知公司要成立新的项目组的时候,我主动申请过去,我觉得我自己适合这样的场景,同时也能够让自己有一个更好的成长过程。说到夸克,不得不提我的老大志明了。

我接触过UC系的挺多的App代码架构,大部分都和UC雷同,但夸克改变了,扬长避短,夸克的App质量也非常高。这一切得益于志明老大早期的应用架构设计上的改变,同时增加单元测试,MVP,代码解耦,SDK化上。

夸克组每个研发都很nice

志明 — 对代码架构、技术前瞻性、App质量、功能输出很有想法的老大
锋锋 — UC公认的UI大神,没有之一
惠均 — 美貌于才华的骚气十足的文艺男青年,卧槽,我感觉我在欺骗自己
太虚真人 — 可能是真的太虚了吧,哈哈哈,做事条理性很强,不愧是名校毕业的。
xiangsheng — 卧槽,字太难打了,负责测试研发这块,还是给夸克带来了很多工作的,毕竟老年人了(哈哈哈)。
智艺 — 在我印象中,你就是游戏少年啊啊啊,哈哈哈,一个人负责夸克的这段时间也很溜了。

不管未来身处何方,记得给自己鼓掌加油,有机会我们杭州见!!!

2017回顾

理解业务 -> 技术的持续投入

        2017自己对业务和技术的理解更多了,做夸克的一年多的时间里,里面有太多值得回忆。对技术的持续追求,对产品的理解,从业务更好的理解技术,以及从技术功能点对业务的反向思考,给夸克带来许多实用的功能。需要做到用户喜欢用这个功能,自己需要深入的体验挖掘用户的潜在需求,然后在技术上能不能够实现,如果能实现,方案是怎样的,会存在什么问题,以及问题的解决方案等。

业务 -> 抽离SDK

        在夸克的这一年来,经过我手头的项目已经很多。从早期对国内UC的代码耦合杂乱就提出要在夸克里面避免糟糕的代码,夸克需要模块解耦,功能内敛、使用单一简单、接口清晰明确。

        夸克的加解密SDK、升级SDK、二维码SDK、QuarkDownloader SDK、分享SDK、翻译SDK、OCR SDK。这些SDK我在夸克中完成的,都有很明显的特性,功能解耦、接口清晰、使用简单。在业务功能的抽象分解上自己做到了。目前多个SDK应用在其他产品上面。

应用架构

         夸克浏览器在架构方面并没有完全参照国内UC,反而是知道国内UC架构的不足作出了优化(我对比了多个产品先的架构,都是照搬国内UC的架构),夸克的老大还是很有思想的,我们做出了MVC + feature MVP功能架构,避免了Controller承载业务造成业务重度耦合的情况,目前UC系的产品都是单Activity模式,通过自己的WindowManager来管理窗口,然后建立自己的一套MsgDispather,通过id映射Controller的机制来驱动feature来实现对应的业务,当然这种也存在一些问题,不过有点大于缺点吧!

大致梳理下架构图

总结

        在夸克的一年是辛苦的一年也是收获的一年,收获了自己对业务和技术的理解、收获了自己对应用架构、SDK、底层技术实现和优化的经验。同时自己具备了独当一面的能力,能够有序的拆解业务,推进业务的进展,感谢UC的老大给予的信任。

2018技术学习

        给自己定几个目标吧

  1. 研究JVM的内存管理机制
  2. 学习研究我们常用的集合类的实现原理,比如HashMap、ArrayList等
  3. 学习研究跨进程通信的详细细节,AIDL的实现原理
  4. 学习直播相关的知识(RTMP协议格式,建立连接的过程、传输过程、解码过程等),以前自己就是做过这个的,加深理解吧

下载支撑

QuarkDownloader SDK 支撑夸克浏览器中几乎所有的下载任务。

  1. 功能支撑
  2. 业务支撑

功能支撑

  1. 设置自动中断任务
  2. 更新链接
  3. 开放线程调节
  4. 外部调用夸克浏览器下载

业务支撑

  1. so 升级下载
  2. 图片下载
  3. CMS资源下载
  4. 视频缓存
  5. 内核回调的文件下载

下载特性

读写分离/非分离

全局一个线程写文件

任意线程

网络库定制

IO可定制(网络IO和文件IO)

设置自动中断

按需启动

更新链接

进程核心统计接口

通知栏抽象接口

静默/非静默任务

Koala Mimi 表情

手绘作品





小七鸭


Logo设计





H5


平面广告

![](./%E8%96%9B%E4%B8%9A%E4%B9%94-%E4%BD%9C%E5%93%81/平面广告/4G进企业—— wifi进厂区 稿件三.jpg)
![](./%E8%96%9B%E4%B8%9A%E4%B9%94-%E4%BD%9C%E5%93%81/平面广告/4G进企业—— wifi进厂区 稿件三02.jpg)



下载组件优势

  1. 易接入、易用性
  2. 扩展性好(网络库扩展、文件IO扩展、调节线程数、众多可选参数)
  3. 网络和文件IO优化,体现下载速度较快
  4. 读写分离
  5. 无任何第三方依赖
  6. 通知栏的简易对接
  7. 下载组件的执行逻辑

Demo地址

http://www.coolapk.com/apk/com.vanda_adm.vanda

开源地址

http://gitlab.alibaba-inc.com/uc-mobile-open/QuarkDownloader

易接入

如何接入

在QuarkDownloader设计的时候,就易用性、低的接入成本经过了考虑,我们先看看App如何接入QuarkDownloader。

1
2
3
4
5
6
7
8
9
10
public class VandaApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
String defaultDownloadPath = PathConfig.getDownloadRootPath();
QuarkDownloader.init(VandaApplication.this, defaultDownloadPath, new OkhttpNetworkConnect.Creator());
}
}

这里是给配置下载进程所需要的上下文参数,这些初始化是不耗时操作。这样就已经配置好了下载所必要的环境。

如果不需要外接网络库,则如下:

1
QuarkDownloader.init(VandaApplication.this, defaultDownloadPath);

该方式使用系统内置网络库(URLConnection)

使用下载组件来下载文件

启动一个下载任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void startTask(String url, String title) {
QuarkDownloadRequest request = new QuarkDownloadRequest
.Builder()
.url(url)
.title(title)
.build();

QuarkDownloader.getInstance().createTask(request).addOnStateChangeListener(new OnStateChangeListener() {
@Override
public void onStateChange(QuarkDownloadTask task, int status, long sofar, long total) {
//your code
}
}).start();
}


这样就建立一个下载任务,你只要在回调中处理你自己的逻辑就OK了。这里的例子只是下载组件最基本的启动方式,下载组件还提供了众多的参数,以及支持众多的场景。

扩展性好

网络库扩展、IO扩展

下载组件能够支持外部网络库接入,这也就说明,下载组件可以支持更多的下载协议,下载组件只关心数据读取、数据的存储。

下载组件抽象了一组接口,外部可以提供自己的网络库实现,如下代码:

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

public interface NetworkConnection {

int NO_RESPONSE_CODE = 0;

/**
* 数据的请求方式
*/
enum RequestWays {
POST,
GET,
}

/**
* 添加请求头部信息,这个会被添加到网络库中的请求的header中
* @param name
* @param value
*/
void addHeader(String name, String value);


/**
* 请求头部信息
* @return
*/
Map<String, List<String>> getRequestHeaderFields();

/**
* 响应头部信息,这个下载组件会获取一些必要的信息
* @return
*/
Map<String, List<String>> getResponseHeaderFields();

/**
* 获取制定name的value
* @param name
* @return
*/
String getResponseHeaderField(String name);

/**
* 执行网络请求
* @throws IOException
*/
void execute(RequestWays requestWays, Map<String, String> postBody) throws IOException;

/**
* 获取网络请求的响应code
* @return
* @throws IOException
*/
int getResponseCode() throws IOException;

/**
* 下载完成或出现异常需要处理的回调
*/
void ending() throws Throwable;

/**
* 下载组件提供了一组内存映射的输入流,提高文件的IO效率
* @param outputStream
*/
void outputStream(OutputStream outputStream);

/**
* 读取制定输入流中的字节数的数据
* @param byteCount
* @return
*/
long read(long byteCount) throws IOException;

/**
* 将读取的数据写入到磁盘
* @throws IOException
*/
void emit() throws IOException;

/**
* 释放文件操作相关
* @throws IOException
*/
void release() throws IOException;

/**
* @param etag
* @param offset
* @return
*/
@Deprecated
boolean dispatchAddResumeOffset(String etag, long offset);

/**
* 网络输入流
* @return
* @throws IOException
*/
@Deprecated
InputStream getInputStream() throws IOException;
}
Okhttp实现

下载组件进行了抽象接口,来实现最大化的定制需求,方便业务自己的需求。为了更为方便的说明问题,我们列举一个使用Okhttp来实现网络库和IO操作的实现类:

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

private Request.Builder mRequestBuilder;
private Call mCall;
private Response mResponse;
private String mUrl;

private BufferedSink mSink = null;
private BufferedSource mSource = null;
private Buffer mBuffer = null;

public OkhttpNetworkConnect(String url) throws IOException {
mRequestBuilder = new Request.Builder();
this.mUrl = url;
}

@Override
public void addHeader(String name, String value) {
mRequestBuilder.addHeader(name, value);
}

@Override
public boolean dispatchAddResumeOffset(String etag, long offset) {
return false;
}

@Override
public InputStream getInputStream() throws IOException {
return mResponse.body().byteStream();
}

@Override
public Map<String, List<String>> getRequestHeaderFields() {
return null;
}

@Override
public Map<String, List<String>> getResponseHeaderFields() {
return null;
}

@Override
public String getResponseHeaderField(String name) {
return mResponse.header(name);
}

@Override
public void execute(FileDownloadConnection.RequestWays requestWays, Map<String, String> postBody) throws IOException {
mRequestBuilder.cacheControl(CacheControl.FORCE_NETWORK);

FormBody.Builder builder = new FormBody.Builder();
RequestBody requestBody = null;

if (requestWays == RequestWays.POST && postBody != null) {
for (Map.Entry<String, String> entry : postBody.entrySet()) {
builder.addEncoded(entry.getKey(), entry.getValue());
}

requestBody = builder.build();
}

mRequestBuilder.cacheControl(CacheControl.FORCE_NETWORK);

// start download----------------
// Step 3, init request
final Request request = requestWays == RequestWays.POST ? mRequestBuilder.url(mUrl).post(requestBody).build() : mRequestBuilder.url(mUrl).get().build();
mCall = OkHttpProxy.getInstance().newCall(request);
mResponse = mCall.execute();
mSource = mResponse.body().source();

Log.d("vanda", "head = " + mResponse.headers().toString() + " mSource = " + mSource);
}

@Override
public int getResponseCode() throws IOException {
return mResponse.code();
}

@Override
public void ending() {
if (mResponse != null) {
mResponse.close();
}
}

@Override
public void outputStream(OutputStream outputStream) {
mSink = Okio.buffer(Okio.sink(outputStream));
mBuffer = mSink.buffer();
}

@Override
public long read(long byteCount) throws IOException {
return mSource.read(mBuffer, byteCount);
}

@Override
public void emit() throws IOException {
mSink.emit();
}

@Override
public void release() throws IOException {
if (mSource != null) {
mSource.close();
}

if (mSink != null) {
mSink.close();
}
}

public static class Creator implements NetworkConnectionCreator {

public Creator() {
}

@Override
public NetworkConnection create(String originUrl) throws IOException {
return new OkhttpNetworkConnect(originUrl);
}
}
}

如上代码就实现了Okhttp网络库的接入,接口实现也比较简单。我们注意到网络库涉及到网络IO这块,一些网络库在网络库IO有自己的实现,典型的例子是Okhttp使用了自身的Okio,所以在网络IO上我们把操作权交给了业务方,这样能够获取更为高效的网络IO性能。在文件IO上我们提供了内部已经计算好文件range和seek的OutputStream,业务方可以对其进行包装或者直接使用都能够获取更为高效的文件IO,至于为什么高效后面我会说到。

网络IO读取策略

一般来说我们利用空间和换取时间的策略,来提高网络IO的使用率,也就是说,我们使用内存空间来提高网络IO效率。如下图:

如上图,我们使用读取segment,一个segment有一个固定大小,下载组件中抽离了Okio的核心逻辑,组合成QuarkOkio,每次进行emit数据时必须满足上述条件,使用segment来存储数据,这样有3个好处:

  1. 每次读取segment时可以提供任务进度回调,而这个时候并没有进行文件IO操作,我们把数据保存在内存中
  2. segment是一个双向链式结构,并且提供SegmentPool来缓存segment,避免系统GC和申请byte时的zero-fill。,这些数据块使用链表进行管理,这可以仅通过移动“指针”就进行数据的管理,而不用真正去处理数据,而且对扩容来说也十分方便。
  3. 它对数据进行了分块处理,这样在大数据IO的时候可以以块为单位进行IO,这可以提高IO的吞吐率。

同时我们下载组件提供了多线程下载机制,结合QuarkOkio数据缓存机制,能够最大程度利用网络IO,从而使网络IO最大化。

文件IO策略

内置众多文件IO方式,可以扩展

QuarkDownloader组件比对过Java里面的文件IO性能,我们先列举性能最优的IO,MappedByteBuffer,下载组件中有一个IO类IOChannel,这个类中继承了OutputStream,利用MappedByteBuffer来完成文件的IO,现在我们来说说这个和普通的IO有和不同。

简单介绍下传统IO的方式

IO,就是数据不停地搬入搬出缓冲区而已(使用了缓冲区)。比如,用户程序发起读操作,导致“ syscall read ”系统调用,就会把数据搬入到buffer中。用户发起写操作,导致 “syscall write ”系统调用,将会把buffer中的数据搬出去(发送到网络中或者写入到磁盘文件)

普通的IO处理的流程图:

  1. 将数据从磁盘读数据到内核缓冲区,由DMA来完成
  2. 然后内核将内核缓冲区的数据拷贝到用户空间的用户进程
  3. 这样完成了数据的读

一般Java应用程序读取数据代码如下:

1
2
3
4
5
byte[] b = new byte[4096];
long len;
while((len = inputStream.read(b))>=0) {

}

当执行到read()方法时,底层执行了很多操作的:

  1. 内核给磁盘控制器发指令:我要读磁盘上的某块磁盘块上的数据。
  2. 在DMA的控制下,把磁盘上的数据读入到内核缓冲区。
  3. 内核把数据从内核缓冲区拷贝到用户缓冲区。

从上面几步我们可以分析出:

  1. 就操作系统而言,JVM只是一个用户进程,处于用户态空间中。而处于用户态空间的进程是不能直接操作底层硬件的。而IO操作就需要操作底层的硬件,比如磁盘。因此,IO操作必须得借助内核的帮助才能完成(中断,trap),即:会有用户态到内核态的切换。
  2. 对于磁盘块的读取而言,每次访问磁盘读数据时,并不是读任意大小的数据的,而是每次读一个磁盘块或者若干个磁盘块(这是因为访问磁盘操作代价是很大的,而且我们也相信局部性原理) 因此,就需要有一个“中间缓冲区”–即内核缓冲区。先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区。
内存映射

内存映射IO,也即JAVA NIO中提到的内存映射文件或者说 直接内存,示例图如下:

从上图可以看出:内核空间的 buffer 与 用户空间的 buffer 都映射到同一块 物理内存区域。

它的主要特点如下:

  1. 对文件的操作不需要再发read 或者 write 系统调用了

  2. 当用户进程访问“内存映射文件”地址时,自动产生缺页错误,然后由底层的OS负责将磁盘上的数据送到内存。

这就是是JAVA NIO中提到的内存映射缓冲区(Memory-Mapped-Buffer)它类似于JAVA NIO中的直接缓冲区(Directed Buffer)。MemoryMappedBuffer可以通过java.nio.channels.FileChannel.java(通道)的 map方法创建。

使用内存映射缓冲区来操作文件,它比普通的IO操作读文件要快得多。甚至比使用文件通道(FileChannel)操作文件 还要快。因为,使用内存映射缓冲区操作文件时,没有显示的系统调用(read,write),而且OS还会自动缓存一些文件页(memory page)

既然建立内存映射没有进行实际的数据拷贝,那么进程又怎么能最终直接通过内存操作访问到硬盘上的文件呢?那就要看内存映射之后的几个相关的过程了。
 
mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址。
 
前面讲过,建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中。
 
如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上。

总结来说,内存映射文件是将硬盘上文件的位置与进程逻辑地址空间中一块大小相同的区域之间一一对应, 建立内存映射由mmap()系统调用将文件直接映射到用户空间,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,要操作其中的数据时即第一次访问ptr指向的内存区域,必须通过MMU将逻辑地址转换成物理地址,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,这个过程只进行了一次数据拷贝。因此,内存映射的效率要比read/write调用效率高。

通过以上的 网络IO 和 文件IO 的策略,加上我们多线程并发处理,在下载速度上能够有一个较快的下载速度,尤其是在处理高速网络下载能力。

读写分离

QuarkDownloader实现了读写分离,同时提供了可选方式。全局一个线程来完成文件的写入。为了结合外部网络库的自身网络IO情况,内部保留了网络库的网络IO和文件IO的结合方式,典型的就是Okhttp的Okio

无任何第三方依赖

QuarkDownloader下载组件剔除了所有的依赖,目前下载组件没有任何第三方依赖,当然我们需要JDK在1.4以上。

通知栏接入和自定义

QuarkDownloader提供了通知栏的高度自定义化接口,同时支持任务是否要显示在通知栏,在下载组件内部有静默任务和非静默任务,非静默任务通过注册的通知栏接口产生回调。我们来看看如何实现自己的通知栏。

QuarkDownloader抽象了一个通知栏通用接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface OnShowNotification {
/**
* 暂停或者移除任务时,处理通知栏
* @return
*/
boolean pauseOrRemove();

/**
*
* 显示通知栏和更新通知栏
*
* @param status
* @param baseDownloadTask
*/
void show(final int status, final BaseDownloadTask baseDownloadTask);

/**
* 取消通知栏
*/
void cancel();
}

非静默任务会根据接口的实现来回调通知栏。

如何接入通知栏

我们提供一组自定义的通知栏显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
public class DownloadNotificationManager implements QuarkDownloadNotificationManager.OnShowNotification {

public static final int DOWNLOAD_NOTIFICATION_ID = 0x1213;

private static final String PATTERN = "mm':'ss";
private static final long ONE_HOUR = 60 * 60 * 1000;
private static final long TIME_INTERVAL = 5 * 1000;

public static final String OPEN_TYPE = "open_type";
public static final String OPEN_TYPE_ENTER_DOWNLOAD_INTERFACE = "enter_download_interface";
private static final String FORMAT_M = "%#.1fM/s";
private static final String FORMAT_K = "%dK/s";
private static final String FORMAT_NOTIFICATION_CONTENT = "%s%s%s%s/%s%s%s%s%s";
private static final String FORMAT_SPLITE = " | ";
private static final String FORMAT_PERCENT = "%";
private static final String STRING_ONE_HOUR = ">1 hour";
private static final String STRING_UNKNOWN = "...";
private static final String STRING_POINT = ". ";


private SimpleDateFormat mStringDateFormat;
private Date mDate;

private long mCurDownloadId = -1;
private long mCurDownloadTime = -1;
private int mDownloadingCount = 0;

NotificationCompat.Builder mBuilder;
private ArrayList<Long> mListId = new ArrayList<>();

public DownloadNotificationManager() {
mStringDateFormat = new SimpleDateFormat(PATTERN);
mDate = new Date();

Intent[] intents = new Intent[4];
intents[0] = new Intent(ContextManager.getApplicationContext(), MainActivity.class);
intents[0].putExtra(OPEN_TYPE, OPEN_TYPE_ENTER_DOWNLOAD_INTERFACE);
PendingIntent pendingIntent[] = new PendingIntent[4];
pendingIntent[0] = PendingIntent.getActivity(ContextManager.getApplicationContext(), 0, intents[0], PendingIntent.FLAG_UPDATE_CURRENT);

mBuilder = new NotificationCompat.
Builder(FileDownloadHelper.getAppContext());

mBuilder.setOngoing(true)
.setLights(0, 0, 0) //不需要显示灯光
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent[0])
.setWhen(0)
.setAutoCancel(true)
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(ContextManager.getApplicationContext().getResources(), R.drawable.ic_logo));
}

private NotificationManager manager;

public NotificationManager getManager() {
if (manager == null) {
manager = (NotificationManager) FileDownloadHelper.getAppContext().
getSystemService(Context.NOTIFICATION_SERVICE);
}
return manager;
}

private static long mTime = 0;
private static long INTERVAL = 1000;

@Override
public void show(final int status, final BaseDownloadTask baseDownloadTask) {
if (!SystemUtils.isOpenNotification || (SystemUtils.isFg && !SystemUtils.isShowNotificationMainUi)) {
return;
}

long end = System.currentTimeMillis();
long interval = end - mTime;

if (interval < INTERVAL && status == QuarkDownloadTask.STATUS_PROGRESS) {
return;
}
mTime = end;
ThreadManager.post(ThreadManager.THREAD_BACKGROUND, new Runnable() {
@Override
public void run() {
showInBackgroundThread(status, baseDownloadTask);
}
});
}

private void showInBackgroundThread(int status, BaseDownloadTask baseDownloadTask) {
if (baseDownloadTask == null) {
return;
}

if (status != FileDownloadStatus.completed && status != FileDownloadStatus.error && status != FileDownloadStatus.paused) {
if (mCurDownloadId == -1) {
mCurDownloadTime = System.currentTimeMillis();
mCurDownloadId = baseDownloadTask.getId();
mListId.add(mCurDownloadId);
mDownloadingCount = QuarkDownloader.getInstance().getDownloadingTaskCount().length;
} else if (mListId.contains((long) baseDownloadTask.getId()) && mCurDownloadId != baseDownloadTask.getId() && ((System.currentTimeMillis() - mCurDownloadTime) < TIME_INTERVAL)) {
return;
} else if (mCurDownloadId != baseDownloadTask.getId() && (System.currentTimeMillis() - mCurDownloadTime) >= TIME_INTERVAL) {
mCurDownloadTime = System.currentTimeMillis();
}
}

mCurDownloadId = baseDownloadTask.getId();
if (!mListId.contains(mCurDownloadId)) {
mListId.add(mCurDownloadId);
}

switch (status) {
case FileDownloadStatus.pending:
mDownloadingCount = QuarkDownloader.getInstance().getDownloadingTaskCount().length;
break;
case FileDownloadStatus.started:
mDownloadingCount = QuarkDownloader.getInstance().getDownloadingTaskCount().length;
break;
case FileDownloadStatus.progress:
break;
case FileDownloadStatus.retry:
break;
case FileDownloadStatus.error:
if (mListId.contains(mCurDownloadId)) {
mListId.remove(mCurDownloadId);
}
break;
case FileDownloadStatus.paused:
if (mListId.contains(mCurDownloadId)) {
mListId.remove(mCurDownloadId);
}
break;
case FileDownloadStatus.completed:
if (mListId.contains(mCurDownloadId)) {
mListId.remove(mCurDownloadId);
}
break;
case FileDownloadStatus.warn:
break;
}

if (mListId.size() != mDownloadingCount) {
mDownloadingCount = QuarkDownloader.getInstance().getDownloadingTaskCount().length;
}

String speed;
long sp = baseDownloadTask.getSpeed();
if (sp > 1024) {
speed = String.format(Locale.CHINESE, FORMAT_M, (float) sp / (float) 1024);
} else {
speed = String.format(Locale.CHINESE, FORMAT_K, sp);
}

long sofar = baseDownloadTask.getLargeFileSoFarBytes();
long total = baseDownloadTask.getLargeFileTotalBytes();

int progress = total <= 0 ? 0 : (int) ((sofar / (float) total) * 100);
String title;
String content;

if (((status == FileDownloadStatus.completed && QuarkDownloader.getInstance().isIdle()) || status == FileDownloadStatus.error || status == FileDownloadStatus.paused) && mListId.size() == 0) {
int[] completeAndPauseCount = QuarkDownloader.getInstance().getCompleteAndPauseCount();
title = FileDownloadHelper.getAppContext().getString(R.string.app_name) + " " + FileDownloadHelper.getAppContext().getString(R.string.download);
content = (completeAndPauseCount[1] > 0 ? FileDownloadHelper.getAppContext().getString(R.string.download_notification_task) + " " + completeAndPauseCount[1] + FORMAT_SPLITE : "") + FileDownloadHelper.getAppContext().getString(R.string.download_notification_complete) + " " + completeAndPauseCount[0];
mDownloadingCount = 0;
mBuilder.setOngoing(false);
mBuilder.setProgress(0, 0, false);
notify(title, content, false);
} else {
int index = mDownloadingCount > 1 ? getIndex(mListId, baseDownloadTask.getId()) : -1;
title = (index > 0 ? index + STRING_POINT : "") + baseDownloadTask.getFilename();
long timeLeft = sp <= 0 ? (total - sofar) : (total - sofar) / sp;
mDate.setTime(timeLeft);
String timeTips = timeLeft >= ONE_HOUR ? STRING_ONE_HOUR : mStringDateFormat.format(mDate);
content = String.format(FORMAT_NOTIFICATION_CONTENT, total <= 0 ? STRING_UNKNOWN : progress + "", FORMAT_PERCENT, FORMAT_SPLITE, QuarkFileUtlis.formatSize(sofar), QuarkFileUtlis.formatSize(total), FORMAT_SPLITE, speed, FORMAT_SPLITE, total <= 0 ? STRING_UNKNOWN : timeTips);
mBuilder.setOngoing(true);
mBuilder.setProgress(1000, (int) ((sofar / (float) total) * 1000), total <= 0);
notify(title, content, true);
}


}

public static int getIndex(List<Long> list, int id) {
if (list == null || list.size() == 0) {
return -1;
}
for (int i = 0; i < list.size(); i++) {
if (list.get(i) == id) {
return i + 1;
}
}
return -1;
}

@Override
public boolean pauseOrRemove() {
ThreadManager.post(ThreadManager.THREAD_BACKGROUND, new Runnable() {
@Override
public void run() {
pauseInBackgroundThread();
}
});
return true;
}

private void pauseInBackgroundThread() {
if (mListId == null || mListId.size() == 0) {
int[] completeAndPauseCount = QuarkDownloader.getInstance().getCompleteAndPauseCount();
String title = FileDownloadHelper.getAppContext().getString(R.string.app_name) + " " + FileDownloadHelper.getAppContext().getString(R.string.download);
String content = (completeAndPauseCount[1] > 0 ? FileDownloadHelper.getAppContext().getString(R.string.download_notification_task) + " " + completeAndPauseCount[1] + " | " : "") + FileDownloadHelper.getAppContext().getString(R.string.download_notification_complete) + " " + completeAndPauseCount[0];
mDownloadingCount = 0;
mBuilder.setOngoing(false);
mBuilder.setProgress(0, 0, false);

if (completeAndPauseCount[0] == 0 && completeAndPauseCount[1] == 0) {
cancel();
} else {
notify(title, content, false);
}
}
}

private void notify(String title, String content, boolean isShowFileDownloadProcess) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mBuilder.setContentTitle(content)
.setSubText(title);
} else {
mBuilder.setContentTitle(title)
.setContentText(content);
}
mBuilder.setNumber(mDownloadingCount);
Notification notification = mBuilder.getNotification();
int smallIconId = ContextManager.getApplicationContext().getResources().getIdentifier("right_icon", "id", android.R.class.getPackage().getName());
if (smallIconId != 0) {
if (notification.contentView != null) {
notification.contentView.setViewVisibility(smallIconId, View.INVISIBLE);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if (notification.bigContentView != null) {
notification.bigContentView.setViewVisibility(smallIconId, View.INVISIBLE);
}
}
}

if (isShowFileDownloadProcess) {
QuarkDownloader.getInstance().notifyNotification(DOWNLOAD_NOTIFICATION_ID, notification);
} else {
getManager().notify(DOWNLOAD_NOTIFICATION_ID, notification);
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void cancel() {
ThreadManager.removeRunnable(mCancleRunnable);
ThreadManager.post(ThreadManager.THREAD_BACKGROUND, mCancleRunnable);
}

private Runnable mCancleRunnable = new Runnable() {
@Override
public void run() {
getManager().cancel(DOWNLOAD_NOTIFICATION_ID);
}
};
}

这里的实现,业务方可以根据自己的业务需求自己来做出实现,这样非常灵活,QuarkDownloader只是把通知栏的抽象起来,提供给外部统一的处理入口。

接下来,我们只要初始化这个实现,就能够轻松的实现通知栏:

1
2
3
DownloadNotificationManager mDownloadNotificationManager = new DownloadNotificationManager();
QuarkDownloadNotificationManager.getInstance().setOnShowNotification(mDownloadNotificationManager);

那么这样你的应用就拥有自己的通知栏。

下载组件的执行逻辑

这里我画了下下载组件的大致流程图:

简单介绍下传统IO的方式

IO,其实意味着:数据不停地搬入搬出缓冲区而已(使用了缓冲区)。比如,用户程序发起读操作,导致“ syscall read ”系统调用,就会把数据搬入到 一个buffer中;用户发起写操作,导致 “syscall write ”系统调用,将会把一个 buffer 中的数据 搬出去(发送到网络中 or 写入到磁盘文件)

普通的IO处理的流程图:

  1. 将数据从磁盘读数据到内核缓冲区,由DMA来完成
  2. 然后内核将内核缓冲区的数据拷贝到用户空间的用户进程
  3. 这样完成了数据的读

一般Java应用程序读取数据代码如下:

1
2
3
4
5
byte[] b = new byte[4096];
long len;
while((len = inputStream.read(b))>=0) {

}

当执行到read()方法时,底层执行了很多操作的:

  1. 内核给磁盘控制器发指令:我要读磁盘上的某块磁盘块上的数据。
  2. 在DMA的控制下,把磁盘上的数据读入到内核缓冲区。
  3. 内核把数据从内核缓冲区拷贝到用户缓冲区。

从上面几步我们可以分析出:

  1. 就操作系统而言,JVM只是一个用户进程,处于用户态空间中。而处于用户态空间的进程是不能直接操作底层硬件的。而IO操作就需要操作底层的硬件,比如磁盘。因此,IO操作必须得借助内核的帮助才能完成(中断,trap),即:会有用户态到内核态的切换。

  2. 对于磁盘块的读取而言,每次访问磁盘读数据时,并不是读任意大小的数据的,而是每次读一个磁盘块或者若干个磁盘块(这是因为访问磁盘操作代价是很大的,而且我们也相信局部性原理) 因此,就需要有一个“中间缓冲区”–即内核缓冲区。先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区。


    这也是为什么我们总感觉到第一次read操作很慢,而后续的read操作却很快的原因吧。因为,对于后续的read操作而言,它所需要读的数据很可能已经在内核缓冲区了,此时只需将内核缓冲区中的数据拷贝到用户缓冲区即可,并未涉及到底层的读取磁盘操作,当然就快了。如果数据不可用,process将会被挂起,并需要等待内核从磁盘上把数据取到内核缓冲区中。

  3. DMA为什么不直接将磁盘上的数据读入到用户缓冲区呢?一方面是 ⓑ中提到的内核缓冲区作为一个中间缓冲区。用来“适配”用户缓冲区的“任意大小”和每次读磁盘块的固定大小。另一方面则是,用户缓冲区位于用户态空间,而DMA读取数据这种操作涉及到底层的硬件,硬件一般是不能直接访问用户态空间的(OS的原因吧)

    综上,由于DMA不能直接访问用户空间(用户缓冲区),普通IO操作需要将数据来回地在 用户缓冲区 和 内核缓冲区移动,这在一定程序上影响了IO的速度。那有没有相应的解决方案呢?

内存映射

内存映射IO,也即JAVA NIO中提到的内存映射文件或者说 直接内存,示例图如下:

从上图可以看出:内核空间的 buffer 与 用户空间的 buffer 都映射到同一块 物理内存区域。

它的主要特点如下:

  1. 对文件的操作不需要再发read 或者 write 系统调用了—The user process sees the file data asmemory, so there is no need to issue read() or write() system calls.

  2. 当用户进程访问“内存映射文件”地址时,自动产生缺页错误,然后由底层的OS负责将磁盘上的数据送到内存。

这就是是JAVA NIO中提到的内存映射缓冲区(Memory-Mapped-Buffer)它类似于JAVA NIO中的直接缓冲区(Directed Buffer)。MemoryMappedBuffer可以通过java.nio.channels.FileChannel.java(通道)的 map方法创建。

使用内存映射缓冲区来操作文件,它比普通的IO操作读文件要快得多。甚至比使用文件通道(FileChannel)操作文件 还要快。因为,使用内存映射缓冲区操作文件时,没有显示的系统调用(read,write),而且OS还会自动缓存一些文件页(memory page)

操作系统的内存区域分布

内存映射文件和之前说的 标准IO操作最大的不同之处就在于它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。

为了说清楚这个,我们以Linux操作系统为例子,看下图:

Linux中的进程虚拟存储器,即进程的虚拟地址空间,如果你的机子是 32 位,那么就有 2^32 = 4G的虚拟地址空间,我们可以看到图中有一块区域: “Memory mapped region for shared libraries” ,这段区域就是在内存映射文件的时候将某一段的虚拟地址和文件对象的某一部分建立起映射关系,此时并没有拷贝数据到内存中去,而是当进程代码第一次引用这段代码内的虚拟地址时,触发了缺页异常,这时候OS根据映射关系直接将文件的相关部分数据拷贝到进程的用户私有空间中去,当有操作第N页数据的时候重复这样的OS页面调度程序操作。注意啦,原来内存映射文件的效率比标准IO高的重要原因就是因为少了把数据拷贝到OS内核缓冲区这一步(可能还少了native堆中转这一步)。

1
2
3
4
File file = new File("vanda.apk");  
FileInputStream in = new FileInputStream(file);
FileChannel channel = in.getChannel();
MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 0,channel.size());

这样就获取到了内存映射MappedByteBuffer。

总结来说:

内存映射文件是将硬盘上文件的位置与进程逻辑地址空间中一块大小相同的区域之间一一对应, 建立内存映射由mmap()系统调用将文件直接映射到用户空间,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,要操作其中的数据时即第一次访问ptr指向的内存区域,必须通过MMU将逻辑地址转换成物理地址,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,这个过程只进行了一次数据拷贝。因此,内存映射的效率要比read/write调用效率高。

多谢大家的支持,本软件长期维护,如果你觉得好用,或者有更好的意见和建议,欢迎告知。

开发一款软件需要时间和精力,你的小小心意是我继续的动力,谢谢你们!

支付宝捐赠 (长按保存图片):

微信捐赠 (长按保存图片):

地址 备注 所属情况
https://aq.qq.com/cn2/manage/mbtoken/3g_token_download?source_id=2886 不可续传
http://cn.club.vmall.com/forum.php?mod=attachment&aid=MjkzMzgwOXxiODViYzM3MnwxNDg5NTEyMzcxfDcxMjE1OTh8NTc4MjA3Mw%3D%3D 不可续传
学科网
http://g.onegreen.net/Index.html
微盘下载
http://www.rmdown.com/link.php?hash=1717dd391fab49680e22806924fd918be3d85f3bca2 chunked类型
pdfdo 上的文件下载 chunk类型 传输结束后返回异常
网易邮箱附件 chunk类型
http://a.byfen.com/update.html
https://forum.xda-developers.com/devdb/project/dl/?id=23698
https://download.mql5.com/cdn/web/metaquotes.software.corp/mt4/metatrader4.apk?utm_campaign=www.metatrader4.com
https://www.metatrader4.com/en/download
https://s3.amazonaws.com/psiphon/web/mjr4-p23r-puwl/PsiphonAndroid.apk
种子文件
http://www.46eh.com/html/article/670350.html