centos 通过yum 安装 mysql 5.7

  1. 下载 mysql 源安装包
1
curl -LO http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
  1. 安装 mysql 源

    1
    sudo yum localinstall mysql57-community-release-el7-11.noarch.rpm
  2. 安装 mysql

    1
    sudo yum install mysql-community-server

一顿按 y 就可以了.

  1. 启动mysql

    1
    2
    3
    4
    5
    6
    // 安装服务
    [root@localhost share]# sudo systemctl enable mysqld
    // 启动服务
    [root@localhost share]# sudo systemctl start mysqld
    // 查看服务状态
    [root@localhost share]# sudo systemctl status mysqld
  2. 修改 root 默认密码

    1
    2
    3
    4
    MySQL 5.7 默认密码 在 /var/log/mysqld.log 文件中, 下面这个指令可以拿到默认指令.
    grep 'temporary password' /var/log/mysqld.log
    [Note] A temporary password is generated for root@localhost: **********
    登录 MySQL 并修改密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql -u root -p
Enter password:
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '你自己的密码';
```

6. 修改数据库默认字符
```sql
SHOW VARIABLES LIKE 'character%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | latin1 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | latin1 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+

这是我的 mysql 安装以后默认的格式,一般我们实际工作中都是用 utf8mb4 格式.这里可以通过修改 /etc/my.conf 进行修改

1
2
3
4
5
6
7
8
9
10
vi /etc/my.cnf
之后 插入以下的语句 即可修改
[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character_set_server=utf8mb4

  1. 允许远程访问
    默认情况下 root 账号是不允许远端访问的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    mysql> mysql> select host,user  from user;
    +-----------+---------------+
    | host | user |
    +-----------+---------------+
    | localhost | mysql.session |
    | localhost | mysql.sys |
    | localhost | root |
    +-----------+---------------+
    3 rows in set (0.00 sec)

执行以下命令

1
2
3
mysql> update user set host = '%' where user = 'root';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

执行完之后 systemctl restart mysqld 重启服务 即可.

Share Comments

nginx && 定向本地静态资源 && 反代 golang 服务

前提

这几天在公司的局域网服务器上申请了一个服务器, 准备没事整点自己没接触过的东西玩玩, 比如 nginx 等等

开始折腾

申请到的服务器系统版本

1
2
cat /etc/redhat-release
CentOS Linux release 7.3.1611 (Core)

安装 nginx
  1. 添加CentOS 7 nginx yum资源库,打开终端,使用以下命令:

    1
    sudo rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
  2. 使用 yum 命令从 nginx 源服务器中获取来安装 nginx:

    1
    sudo yum install -y nginx
  3. 启动 Nginx

    1
    sudo systemctl start nginx.service
  4. 现在基本上就可以通过IP地址 访问 nginx 的默认页面了
    比如我的ip 是192.168.9.251
    这个时候我在浏览器输入我的 ip, 就可以看到 nginx 的默认页面了

看到这个就代表nginx装好了

期间遇见的一个问题

启动服务器以后, 访问网页显示无法打开,去查了下发现是因为80端口没有开放.
首先:开启 web 端口
firewall-cmd –zone=public –add-port=80/tcp –permanent
然后重启 firewall
firewall-cmd –reload
搞定

nginx 的一些基本命令和配置
  • 默认的服务器 html 存放根目录是/usr/share/nginx/html.
    可以 cd 到这个目录中去看到一些基础的 html, 比如 404.html, 和 index.html
    可以把自己的网页放进这里面

  • 默认的服务器模块配置文件位于
    /etc/nginx/conf.d/default.conf.

  • nginx总的配置文件位于
    /etc/nginx/nginx.conf.

可以看到最后 那句 include 就是把我们刚才配的 /etc/nginx/conf.d/default.conf 引入进来

  • nginx 重新加载配置文件

    1
    nginx -s reload
  • nginx 关闭服务
    nginx -s stop

启动 go 服务

我在本机 使用 echo 写了一个简单的 http server
比较关键的几个代码如下

1
2
3
4
5
s := &http.Server{
Addr: ":1323",
ReadTimeout: 20 * time.Minute,
WriteTimeout: 20 * time.Minute,
}

这里是声明我这个服务 listen 1323端口.

这里是我声明了1个path group, 以 /me 打头

1
2
3
4
me := e.Group("/me")
me.GET("/ping", func(c echo.Context) error {
return c.String(200, "Pong")
})

当别人访问 /me/ping 的时候 我会返回 1个Pong 的 response.

ok 代码很简单.
如果本机跑的话 直接go build -> ./main 或者go run main.go 就ok
但是因为我要部署到服务器centos系统上, 我本机是mac, 所以需要一些特殊操作, go 可以很轻松的支持这种跨平台编译

1
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go

这样就会生成1个可以在 centos 下 跑起来的二进制文件
通过 scp 命令 把该文件传到 我的服务器的某个路径上
运行该文件

1
./usr/share/go/main &

这样我的服务器上就已经成功跑起来该服务了
访问 http://192.168.9.251:1323/me/ping
可以收到 1个 pong 的 response.
前提依然是你的防火墙已经打开了 1323的端口

反向代理

我们先看看 正向代理 和 反向代理的 一些概念

正向代理的概念

正向代理,也就是传说中的代理,他的工作原理就像一个跳板,简单的说,我是一个用户,我访问不了某网站,但是我能访问一个代理服务器,这个代理服务器呢,他能访问那个我不能访问的网站,于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容,代理服务器去取回来,然后返回给我。从网站的角度,只在代理服务器来取内容的时候有一次记录,有时候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站。** 结论就是,正向代理 是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。

反向代理的概念

用户访问 http://www.test.com/readme ,但 www.test.com 上并不存在readme页面,他是偷偷从另外一台服务器上取回来,然后作为自己的内容返回用户,但用户并不知情。这里所提到的 www.test.com 这个域名对应的服务器就设置了反向代理功能。
结论就是,反向代理正好相反,对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。

我的需求

我服务器本身是不希望打开过多的端口, 希望外部访问这台机器的时候, 只通过80端口来接受 http 请求, 并且根据相应的规则分发到不同的端口
比如 http://192.168.9.251/me/ping
这样就可以 收到 pong 的 回应, 而不用 加上 :1323 的端口
nginx 实现这个需求非常简单
只需要 在 /etc/nginx/conf.d/default.conf 加入这个规则就好

1
2
3
4
5
6
location /me {
proxy_pass http://127.0.0.1:1323;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

这样就是说 凡是 /me 的请求 都分发到本机的1323端口上
改完conf 之后, 执行 nginx -s reload
再次访问 http://192.168.9.251/me/ping
就能收到 我们来自 1323端口 go服务的 pong 的回应了

Share Comments

Lottie 源码阅读.

纯手打原创,转载请注明出处,感谢!

参考资料

概略介绍

Lottie is the native engine that Airbnb’s awesome team built. It uses Bodymovin as the animation exporter and is the ideal complement for getting animations to play natively everywhere. Follow these links to get each player:

官方效果图

整体开发流程

如图所示,通过安装AE上的bodymovin的插件,能够将AE中的动画工程文件转换为通用的json格式描述文件(bodymovin插件本身是用于网页上呈现各种AE效果的一个开源库),lottie所做的事情就是实现在不同移动端平台上呈现AE动画的方式,从而达到动画文件的一次绘制、一次转换,随处可用的效果,这个跟Java一次编译随处运行效果一样

各平台 代码简单示范
android:

1
2
3
4
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);
animationView.playAnimation();

iOS:

1
2
3
4
5
LOTAnimationView *animation = [LOTAnimationView animationNamed:@"Lottie"];
[self.view addSubview:animation];
[animation playWithCompletion:^(BOOL animationFinished) {
// Do Something
}];

WEB:

1.8
1
2
3
4
5
LOTAnimationView *animation = [LOTAnimationView animationNamed:@"Lottie"];
[self.view addSubview:animation];
[animation playWithCompletion:^(BOOL animationFinished) {
// Do Something
}];

思路分析

我们先从底层思考下如何在屏幕上绘制动画,最简单的方式是把动画分为多张图片,然后通过周期替换屏幕上绘制的图片来形成动画,这种暴力的方式非常简单,但缺点明显,很耗内存,动画播放中前后两张替换的图片在很多元素并没有变化,重复的内容浪费了空间。
为了提高空间利用率,可以把图片中的元素进行拆分,使用过photoshop的同学知道,其实在处理一张图片时,可以把一张复杂的图片使用多个图层来表示,每个图层上展示一部分内容,图层中的内容也可以拆分为多个元素。拆分元素之后,根据动画需求,可以单独对图层,甚至图层中的元素设置平移、旋转、收缩等动画。

Lottie使用json文件来作为动画数据源,json文件是通过Bodymovin插件导出的,查看sample中给出的json文件,其实就是把图片中的元素进行来拆分,并且描述每个元素的动画执行路径和执行时间。Lottie的功能就是读取这些数据,然后绘制到屏幕上。
现在思考如果我们拿到一份json格式动画如何展示到屏幕上。首先要解析json,建立数据到对象的映射,然后根据数据对象创建合适的Drawable绘制到View上,动画的实现可以通过操作读取到的元素完成。

从android 源码解析

项目结构图

该图因为太大了,所以有可能会看不清楚,感兴趣的同学可以下载到本地放大查看
核心类

通过如下3个核心类来来完成整个工作流程,因而使用起来比较简单。以下是三个比较核心的类的说明:

  • LottieComposition (json->数据对象)
    Lottie使用LottieComposition来作为After Effects的数据对象,即把Json文件映射为到LottieComposition,该类中提供了解析json的静态方法。
  • LottieDrawable (数据对象->Drawable)
    这个类是最上层使用的重要的一个类,动画的绘制就是由这个类来实现。
  • LottieAnimationView(绘制)
    操作集合,LottieAnimationView 继承自 AppCompatImageView,封装了一些动画的操作,具体的绘制时委托为 LottieDrawable 完成的。
    数据解析
    数据的解析,主要参考 LottieComposition.fromJsonSync 方法:
    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 static LottieComposition fromJsonSync(Resources res, JSONObject json) {
    Rect bounds = null;
    float scale = res.getDisplayMetrics().density;
    int width = json.optInt("w", -1);
    int height = json.optInt("h", -1);

    if (width != -1 && height != -1) {
    int scaledWidth = (int) (width * scale);
    int scaledHeight = (int) (height * scale);
    bounds = new Rect(0, 0, scaledWidth, scaledHeight);
    }

    long startFrame = json.optLong("ip", 0);
    long endFrame = json.optLong("op", 0);
    float frameRate = (float) json.optDouble("fr", 0);
    String version = json.optString("v");
    String[] versions = version.split("[.]");
    int major = Integer.parseInt(versions[0]);
    int minor = Integer.parseInt(versions[1]);
    int patch = Integer.parseInt(versions[2]);
    LottieComposition composition = new LottieComposition(
    bounds, startFrame, endFrame, frameRate, scale, major, minor, patch);
    JSONArray assetsJson = json.optJSONArray("assets");
    parseImages(assetsJson, composition);
    parsePrecomps(assetsJson, composition);
    parseFonts(json.optJSONObject("fonts"), composition);
    parseChars(json.optJSONArray("chars"), composition);
    parseLayers(json, composition);
    return composition;
    }

还有 parseImages、parsePrecomps、 parseLayers 这几个方法。
JSON文件的属性含义

动画描述文件是通过 bodymovin 插件导出来的,里面包含了动画的一切信息,包括了帧率,动画形态,如何做动画等。接下来将简单说明一下动画描述文件中的主要属性。

最外部结构:
1
2
3
4
5
6
7
8
9
10
11
12
{
"v": "4.6.2", // bodymovin的版本
"fr": 25, // 帧率
"ip": 0, // 起始关键帧
"op": 1000, // 结束关键帧
"w": 720, // 动画宽度
"h": 800, // 动画高度
"nm": "合成 1",
"ddd": 0,
"assets":[], // 动画图片资源信息
"layers":[] // 动画图层信息
}

从这里可以获取 设计的动画的宽高,帧相关的信息,动画所需要的图片资源的信息以及图层信息。

assets

图片资源信息, 相关类 LottieImageAsset、 ImageAssetBitmapManager。

1
2
3
4
5
6
7
8
9
"assets": [
{
"id": "image_0", // 图片id
"w": 58, // 图片宽度
"h": 31, // 图片高度
"u": "images/",
"p": "img_0.png" // 图片名称
}
}

layers

图层信息,相关类:Layer、BaseLayer以及 BaseLayer 的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"ddd": 0,
"ind": 0, // layer id 图层 id
"ty": 2, // layertype 图层类型
"nm": "btnSlice.png", // layerName 图层信息
"cl": "png",
"refId": "image_0", // 引用的资源 id,如果是 ImageLayer 那么就是图片的id
"ks": {....},
"ao": 0,
"ip": 0, // inFrame 该图层起始关键帧
"op": 90.0000036657751, // outFrame 该图层结束关键帧
"st": 0, // startFrame 开始
"bm": 0,
"sr": 1
}

播放动画

官方说法
  • 如果没有mask和mattes,那么性能和内存非常好,没有bitmap创建,大部分操作都是简单的cavas绘制。
  • 如果存在mattes,将会创建2~3个bitmap。bitmap在动画加载到window时被创建,被window删除时回收。所以不宜在RecyclerView中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。除了内存抖动,mattes和mask中必要的bitmap.eraseColor()和canvas.drawBitmap()也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。
  • 如果在列表中使用动画,推荐使用缓存LottieAnimationView.setAnimation(String, CacheStrategy) 。
Share Comments

Volley 源码阅读.

纯手打原创,转载请注明出处,感谢!

Volley项目是大概3年前谷歌出的一套网络访问框架,从出来之后我就接触过并在项目中有使用过,但是一直都是浅尝的使用,没有认认真真的去读过内部结构,
最近项目结束了一个版本,接着这个间隙我准备去重新读下这个三年前出来的代码.

Class Volley

Volley暴露出给我们最直接的类就是Volley这个类,其中有一个newRequestQueue的方法,来为我们创建一个RequestQueue.

1
RequestQueue mQueue = Volley.newRequestQueue(context);

我们就跟着这个方法开始读代码.

Class RequestQueue

这个类是比较关键的一个入口,该类的API告诉我们,这是一个Request 发送的队列, dispatchers 工作在线程池里.
我们可以使用 add(Request) 把一个我们希望的Request请求 加入队列中, 它会在在工作线程中从缓存中或者网络中解析这个请求,
并且从主线程传递出一个解析好的 Response.
其中RequestQueue的构造方法是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
*
* @param cache A Cache to use for persisting responses to disk
* @param network A Network interface for performing HTTP requests
* @param threadPoolSize Number of network dispatcher threads to create
* @param delivery A ResponseDelivery interface for posting responses and errors
*/
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}

可以看出这个队列需要以下四个初始化参数

  • 用于把response缓存在磁盘的策略.
  • 一种履行Http请求的实现.
  • 用来发送网络请求的线程数量
  • 一种发送responses 和 errors的实现.
    其中Cache Network ResponseDelivery 都是Interface, 我们暂且只看他的工作流程不去看具体Impl,后面再回来看具体实现.
    当我们根据APi指示,调用RequestQueue的Start方法时,RequestQueue 才会真正开始工作.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * Starts the dispatchers in this queue.
    */
    public void start() {
    stop(); // Make sure any currently running dispatchers are stopped.
    // Create the cache dispatcher and start it.
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    mCacheDispatcher.start();

    // Create network dispatchers (and corresponding threads) up to the pool size.
    for (int i = 0; i < mDispatchers.length; i++) {
    NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
    mCache, mDelivery);
    mDispatchers[i] = networkDispatcher;
    networkDispatcher.start();
    }
    }

可以看出来我们初始化了一个CacheDispatcher, 它是extends 线程的类, 它是一个可以对请求队列中cache 进行分类的线程.
也初始化了一个NetWorkDispatcher数组, NetWorkDispatcher 也是extends 线程的, 他是一个负责把从队列中取出来Request 并且执行的线程.
接下来当我们有Request 希望发出的时候, 我们就需要调用 RequestQueue的add方法了.
我们一句句的跟着add的方法进行分析,

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
public <T> Request<T> add(Request<T> request) {
// 给这个request设置队列
request.setRequestQueue(this);
// 把这个request加入到一个HashSet中, 这个HashSet存着所有马上或者正在被任何dispacher处理中的request.
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// 给request 加上一些编号和marker
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");

// 如果当前这个request是不希望被缓存的, 那么他将直接被加入到NetworkQueue中去.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}

synchronized (mWaitingRequests) {
// 如果这个Request是希望被缓存的,会先检查下WaitingRequests里有没有该Reuqest.
String cacheKey = request.getCacheKey();
if (mWaitingRequests.containsKey(cacheKey)) {
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
} else {
mWaitingRequests.put(cacheKey, null);
// 把该Request加入到mCacheQueue中.
mCacheQueue.add(request);
}
return request;
}
}

我们先来看不需要缓存的Request 被加入到mNetworkQueue之后的流程, 之前我们说过,RequestQueue 会有几个NetworkDispatcher 工作线程在run, 我们来看下其中的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
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
Request<?> request;
try {
// 这里的mQueue 就是RequesQueue中的mNetworkQueue, request被加入到mNetworkQueue中以后,会被取出
request = mQueue.take();
} catch (InterruptedException e) {
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);

// 这里是执行request 请求的地方, 会返回一个NetworkResponse,这里的NetworkResponse是最基础的response raw数据.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");

if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}

// 对networkResponse进行解析,其中这里的泛型是Request设置的,由request自己根据自己的需要 对response中的data 字节数组进行解析.
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");

// 这里需要主要的是 如果这个request需要做缓存, 那么将被存入Cache中.
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}

// 通过mDelivery返回 request 和 response. 注意这里是通过handler post到主线程的.
request.markDelivered();
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
}
}
}

至此一次完整的 无缓存 Http 请求就结束了.
接下来可以观察下 需要缓存的Request 和 不需要缓存的Request 处理区别, 不难发现 需要缓存的Request请求 会加入到mCacheQueue中去, 而会处理CacheQueue的Dispatcher 正是
刚才我们初始化的mCacheDispatcher, 我们来看看mCacheDispatcher的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
60
61
62
63
64
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
mCache.initialize();
while (true) {
try {
// 这里从我们刚才加入request的CacheQueue取出 Request, 如果request 是空 会卡住该线程,直到有Request入列.
final Request<?> request = mCacheQueue.take();
request.addMarker("cache-queue-take");

if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// 这里会去Cache中找该Request的缓存, 如果找不到就会把他加入到mNetworkQueue 去执行首次请求.
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
mNetworkQueue.put(request);
continue;
}

// 如果从缓存中查找到了这个Request的缓存, 但是它已经过期了,那么依旧会把他加入到mNetworkQueue 去再次执行请求.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}

// 走到这说明我们拿到了这个Request的 有效缓存
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");

if (!entry.refreshNeeded()) {
// 如果这个Request 不需要更新, 那么到此该Http请求就结束了, 依旧是通过mDelivery 返回Request和response.
mDelivery.postResponse(request, response);
} else {
// 如果需要更新该request, 那么会在返回request和response的同时,再次去请求该Request.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
response.intermediate = true;
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Not much we can do about this.
}
}
});
}

} catch (InterruptedException e) {
if (mQuit) {
return;
}
}
}
}

看到这里,Volley的大体框架 和工作流程就结束了,Volley这份代码虽然是三年前的框架,但是现在看依然是有很多可以学习的地方, 它的结构简单清晰, 可插拔性强, 层与层之间耦合低,都体现出了
它的水平.

Share Comments

正确的保存View状态.

纯手打原创,转载请注明出处,感谢!

前言

android 的 View以及ViewGroup保存相应状态是一件很重要的事情,但是最近由于用到了相关的内容, 搜了下国内相关文章,涉及到的很少.
可能很多人都在开发的时候忽略掉了这个功能.自己看了下相应的源码以及翻阅了下相关的文章,了解了下相应的知识,于是写下这篇记录,算是给以后自己翻阅做个笔记.

首先我们要明白View 为什么要保存自己的状态? 我咨询了一些做android开发的朋友,给出的答案都是五花八门, 但大部分都是说不关心保存状态这件事, 当我说这样不对的时候,
往往得到的回复是我的代码一直好好的啊 没什么问题啊.
是的,代码如果不去关心View 保存状态这件事 在大部分时间都是没问题的,但是在一些情况下会出乱子.
我举个例子, 我们的Activity A 去Activity B, 这时候A会走 onPause onStop onSaveInstanceState() 的生命周期. 我们要先明白Activity给我们这个onSaveInstanceState()回调时机的意义,
来看一下源码的说明

意思就是说 这个Activity在被杀死之前 给你一个机会让你保留你希望保存的东西, 而你保存的东西会在什么时候还给你呢, 会在onCreate 或者onRestoreState的时候.
之前有人跟我, 我完全可以不用care这个回调啊,我可以在Activity内自己创建一个Member变量容器 去保存我希望保存的内容啊, 这里是我们需要明白的一个关键,
那就是 Activity A 去Activity B, Activity A 如果不被系统杀死回收的话,那怎么搞都是没问题的,但是一旦因为内存不足,Activity A被回收掉,你想想,你的Member容器也会随之一起回收掉.
等你从Activity B back 回Activity A的时候, 就会丢失掉你所有希望保存的内容.
这里有一个小细节就是 会有人不知道怎么去测试 Activity A 去Activity B, 然后Activity A 被回收的情况, 我们的android手机内 开发者模式, 有一个选项叫 不保留活动.打开以后,
只要你退出当前的Activity(按home或者去别的Activity), 你当前的Activity 就必定会被系统杀死回收. 看到这里如果不了解这个选项的朋友可以去打开这个选项试试自己的app.或许你会发现打开了新大门..
还有一个小细节就是有可能打开这个开关以后你检查了下你的app 然后告诉我说并没有什么问题, 不会有崩溃啊什么的, 要注意,这里的问题是指 本来应该保存的一些内容 或者用户已经加载过 或者用户已经在当前页面做过的一些动作
没有被保存,这句话需要仔细去理解一下,然后再check下自己的app,看看是不是会有东西丢失.

View 保存State的一些知识点

View saveState 与 restoreState 两套动作都是相对应的, 分别有这么几个关键的方法.

saveState.
  • void saveHierarchyState(SparseArray container)
    这个方法是android framework层调用,在需要保存状态的时机. 看下View源码发现通常是不做什么事情的.是直接调用dispatchSaveInstanceState的
  • void dispatchSaveInstanceState(SparseArray container)
    这个方法是被saveHierarchyState调用,它内部会调用onSaveInstanceState 方法去拿到一个存储View当前状态的Parcelable对象, 然后存在container中.这里有两个个关键点
    1 它的第一句就是 if(mId != NO_ID) 才会执行接下来的保存状态的工作,也就是说如果我们的View希望保存状态那么一定要有一个ID,其实这很好理解, 保存着我们View状态的parcelable 是以key value的形式
    保存在container中的,那么key是我们的id 也是合情合理的.
    代码如下 非常清晰
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
    mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
    Parcelable state = onSaveInstanceState();
    if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
    throw new IllegalStateException(
    "Derived class did not call super.onSaveInstanceState()");
    }
    if (state != null) {
    // Log.i("View", "Freezing #" + Integer.toHexString(mID)
    // + ": " + state);
    container.put(mID, state);
    }
    }
    }

2 如果当前View是一个ViewGroup那么它会去循环调用每个child的dispatchSaveInstanceState(), 去给child机会保存状态, android这样设计的地方很多, 比如Touch分发 比如 Measure分发等等,这里就不过多表述了.
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
super.dispatchSaveInstanceState(container);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
c.dispatchSaveInstanceState(container);
}
}
}

  • Parcelable onSaveInstanceState()
    这个方法就很简单了, 是真正干活的地方,保存当前View的状态, 我们写自定义View的时候 也往往是重写该方法去保存我们需要保存的状态.
restoreState.
  • void restoreHierarchyState(SparseArray container)
    对应saveHierarchyState()
  • void dispatchRestoreInstanceState(SparseArray container)
    对应dispatchSaveInstanceState()
  • void onRestoreInstanceState(Parcelable state)
    对应onSaveInstanceState()

还有一个方法是需要特殊说明的, 如果你的自定义View需要保存状态, 那么你需要调用setSaveEnabled(true)方法. 当然widgets官方提供的view都是打开了的.你不用操心.
看到这里大家应该以及知道了View保存恢复状态的整个流程是什么样的了,一个View 如果想要保存状态,那么只需要做两件事

  1. 有id
  2. setSaveEnabled(true)

如何保存自己的状态

我们已经知道了我们自定义View应该在onSaveInstanceState的时候去保存我们希望保存的状态,那么具体应该怎么去做呢.接下来是示范(炒鸡简单)
比如我们的View内有1个int值希望保存下来, 那么我们的代码应该这样写,

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 int mState;
...
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.state = customState;
return ss;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setCustomState(ss.state);
}

static class SavedState extends BaseSavedState {
int state;

SavedState(Parcelable superState) {
super(superState);
}

private SavedState(Parcel in) {
super(in);
state = in.readInt();
}

@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(state);
}

public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}

public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}

一个经验教训.

当我们做一个View的时候,那么上面的内容足够让你保存你想要保存的状态. 但是如果是一个ViewGroup的话, 代码或许在某些情况下应该这样去设计,

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
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.childrenStates = new SparseArray();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).saveHierarchyState(ss.childrenStates);
}
return ss;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).restoreHierarchyState(ss.childrenStates);
}
}

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}

你需要去注意一下在 dispatchSaveInstanceState 和 dispatchRestoreInstanceState,我们并不是使用super方法 去遍历我们的children去挨个保存和恢复状态,而是
调用dispatchFreezeSelfOnly 和 dispatchThawSelfOnly 来告诉系统,我这个ViewGroup不需要去挨个找我的child去保存状态,我们(ViewGroup)自己来就可以了.
当然并不是说,如果我们写ViewGroup的时候就一定要这样去写,具体情况要根据自己的实际情况去决定.

另外一个血和泪的经验教训.

在上面的例子中, 我们的SavedState 存储了一个int值, 这属于比较简单的一种实现,一般是不会有什么问题的,如果你需要存储的内容是一个Bundle的时候,你往往会这样写;

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
static class SavedState extends BaseSavedState {
Bundle state;

SavedState(Parcelable superState) {
super(superState);
}

private SavedState(Parcel in) {
super(in);
state = in.readBundle(Bundle.class.getClassLoader());
}

@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeBundle(state);
}

public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}

public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}

请注意,这些代码是系统自己生成的, 在大多数情况这样写依然是没有问题的, 但是 一旦你的Bundle里存储了一个你自定义的Parcelable类的时候,事情就会变的蛋疼起来,在你这个Bundle进行unparcel()的时候,会崩溃, 爆出BadParcelableException, 原因是 找不到你的自定义的那个类. 这个bug卡了我好长时间,
最后定位到了这个异常,爆出位置 : 首先你可以在Parcel类中的readValue(ClassLoader loader) 中找到readParcelable() 然后跟进去最后发现 会执行这么一段代码.

1
2
3
// If loader == null, explicitly emulate Class.forName(String) "caller
// classloader" behavior.
ClassLoader parcelableClassLoader =(loader == null ? getClass().getClassLoader() : loader);

第一次看到这个代码的时候,并没有注意什么. 后来查了很多stackoverflow,开始怀疑跟这个ClassLoader有关系.然后开始相应查相关的内容,最终找到了问题所在, 直接上结论把.

Android has two different classloaders: the framework classloader (which knows how to load Android classes) and the APK classloader (which knows how to load your code).
The APK classloader has the framework classloader set as its parent, meaning it can also load Android classes.

说的很清晰了, 就是ClassLoader找错了, 解决方案也很简单,

1
2
3
4
private SavedState(Parcel in) {
super(in);
state = in.readBundle(getClass().getClassLoader());
}

这样写就会给Bundle 设置一个APK的ClassLoader, 这样就可以找到你自己定义的那个Parcelable类了.

Share Comments

android 源生DownloadManager 的两个Crash 坑

如果你的APP使用源生DownloadManager去更新apk,那么下面这3行代码你肯定不陌生,

1
2
3
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(你的APK下载地址));
request.setDestinationUri(你的app 本地存储URI));
long mDownloadId = mDownloadManager.enqueue(request);

IllegalArgumentException

这三行代码 最近在一些机型上爆出了java.lang.IllegalArgumentException: UnknownURLcontent://downloads/my_downloads 异常.
是崩溃在了mDownloadManager.enqueue(request)这里,
跟着代码进去以后

1
2
3
4
5
6
public long enqueue(Request request) {
ContentValues values = request.toContentValues(mPackageName);
Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
long id = Long.parseLong(downloadUri.getLastPathSegment());
return id;
}

继续跟进,找到了崩溃的源头, 是 mResolver.insert这句的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final @Nullable Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
Preconditions.checkNotNull(url, "url");
IContentProvider provider = acquireProvider(url);
if (provider == null) {
// 看这里 嘿嘿嘿
throw new IllegalArgumentException("Unknown URL " + url);
}
try {
long startTime = SystemClock.uptimeMillis();
Uri createdRow = provider.insert(mPackageName, url, values);
long durationMillis = SystemClock.uptimeMillis() - startTime;
maybeLogUpdateToEventLog(durationMillis, url, "insert", null /* where */);
return createdRow;
} catch (RemoteException e) {
// Arbitrary and not worth documenting, as Activity
// Manager will kill this process shortly anyway.
return null;
} finally {
releaseProvider(provider);
}
}

原因是系统的DownloadManager Service被关闭了, 遇见这种问题, 可以有两种解决方法
1 是通过产品角度去告知用户,让用户去系统里找到相应的开关 打开下载服务,
2 是catch 住这个IllegalArgumentException 然后不再使用downloadmanager 而是自己去手写下载, 就可以解决.

SecurityException

这三行代码在某些机器上会爆出SecurityException,原因是Context.getExternalFilesDir得到的地址 和Environment.getExternalStorageDirectory() 不一致.
具体崩溃代码可以去看源码 的713行.

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
private void checkFileUriDestination(ContentValues values) {
String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
if (fileUri == null) {
throw new IllegalArgumentException(
"DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
}
Uri uri = Uri.parse(fileUri);
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals("file")) {
throw new IllegalArgumentException("Not a file URI: " + uri);
}
final String path = uri.getPath();
if (path == null) {
throw new IllegalArgumentException("Invalid file URI: " + uri);
}
try {
final String canonicalPath = new File(path).getCanonicalPath();
final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
if (!canonicalPath.startsWith(externalPath)) {
// 看这里,嘿嘿嘿
throw new SecurityException("Destination must be on external storage: " + uri);
}
} catch (IOException e) {
throw new SecurityException("Problem resolving path: " + uri);
}
}

出现这种bug 我们的解决方案 是自己去拼1个存储地址..
所以这三行代码的最终使用是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(你的APK下载地址));
request.setDestinationUri(你的app 本地存储URI));
try {
long mDownloadId = instance.mSystemDownloadManager.enqueue(request);
} catch (SecurityException e) {
File f = Environment.getExternalStorageDirectory();
f = new File(f, "Android");
f = new File(f, "data");
f = new File(f, mContext.getPackageName());
f = new File(f, "files");
f = new File(f, Environment.DIRECTORY_DOWNLOADS);
if (!f.exists()) {
f.mkdirs();
}
request.setDestinationUri(f));
instance.mDownloadId = instance.mSystemDownloadManager.enqueue(request);
} catch (IllegalArgumentException e) {
// 自己去写把 要么弹toast告诉用户他傻逼, 要么自己去手写下载
}

Share Comments

音视频编码基础技术.

本文参考文章:
流媒体系统对比
封装格式对比
视频编码器对比
音频编码格式对比
雷霄骅雷神博客
HEVC
x265编码器

我们平时下载的电影,因为下载的来源不同,这些电影文件有不同的格式,用不同的后缀表示:avi,rmvb,mp4,flv,mkv等等(当然也使用不同的图标)。在这里需要注意的是,这些格式代表的是封装格式。何为封装格式?就是把视频数据和音频数据打包成一个文件的规范。仅仅靠看文件的后缀,很难能看出具体使用了什么视音频编码标准。总的来说,不同的封装格式之间差距不大,各有优劣。
注:有些封装格式支持的视音频编码标准十分广泛,应该算比较优秀的封装格式,比如MKV;而有些封装格式支持的视音频编码标准很少,应该属于落后的封装格式,比如RMVB。
在此我选用了魔力盒中的一个视频 进行说明.
该视频的 后缀为 mp4. Video codec_name 为H.264, Audio codec_name 为 aac.
这样我们就可以说 这个视频 采用mp4的QuickTime封装格式(MOV),采用了H.264(AVC)的视频压缩编码标准, AAC的音频编码标准.

接下来说一下我们使用播放器去播放一个视频, 中间发生了什么.
视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。
如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。
他们的过程如图所示。
过程图

其中
解协议的作用,就是将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。
解封装的作用,就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。
解码的作用,就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。
视音频同步的作用,就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来

其中几个概念的 特殊说明

流媒体协议

流媒体协议是服务器与客户端之间通信遵循的规定。当前网络上主要的流媒体协议如表所示。

主要流媒体协议一览

流媒体

封装格式

封装格式的主要作用是把视频码流和音频码流按照一定的格式存储在一个文件中。现如今流行的封装格式如下表所示:

主要封装格式一览

封装格式

视频编码

视频编码的主要作用是将视频像素数据(RGB,YUV等)压缩成为视频码流,从而降低视频的数据量。如果视频不经过压缩编码的话,体积通常是非常大的,一部电影可能就要上百G的空间。视频编码是视音频技术中最重要的技术之一。视频码流的数据量占了视音频总数据量的绝大部分。高效率的视频编码在同等的码率下,可以获得更高的视频质量。

主要视频编码一览

视频编码
由表可见,有两种视频编码方案是最新推出的:VP9和HEVC。目前这两种方案都处于研发阶段,还没有到达实用的程度。当前使用最多的视频编码方案就是H.264。

音频编码

音频编码的主要作用是将音频采样数据(PCM等)压缩成为音频码流,从而降低音频的数据量。音频编码也是互联网视音频技术中一个重要的技术。但是一般情况下音频的数据量要远小于视频的数据量,因而即使使用稍微落后的音频编码标准,而导致音频数据量有所增加,也不会对视音频的总数据量产生太大的影响。高效率的音频编码在同等的码率下,可以获得更高的音质。

主要音频编码一览

音频编码
由表可见,近年来并未推出全新的音频编码方案,可见音频编码技术已经基本可以满足人们的需要。音频编码技术近期绝大部分的改动都是在MP3的继任者——AAC的基础上完成的。

现有网络视音频平台对比

现有的网络视音频服务主要包括两种方式:点播和直播。点播意即根据用户的需要播放相应的视频节目,这是互联网视音频服务最主要的方式。绝大部分视频网站都提供了点播服务。直播意即互联网视音频平台直接将视频内容实时发送给用户,目前还处于发展阶段。直播在网络电视台,社交视频网站较为常见。

直播平台参数对比

主流互联网视音频平台直播服务的参数对比如表所示:
现有网络视音频平台参数对比
zhibo

可以看出,直播服务普遍采用了RTMP作为流媒体协议,FLV作为封装格式,H.264作为视频编码格式,AAC作为音频编码格式。采用RTMP作为直播协议的好处在于其被Flash播放器支持。而Flash播放器如今已经安装在全球99%的电脑上,并且与浏览器结合的很好。因此这种流媒体直播平台可以实现“无插件直播”,极大的简化了客户端的操作。封装格式,视频编码,音频编码方面,无一例外的使用了FLV + H.264 + AAC的组合。FLV是RTMP使用的封装格式,H.264是当今实际应用中编码效率最高的视频编码标准,AAC则是当今实际应用中编码效率最高的音频编码标准。视频播放器方面,都使用了Flash播放器。

点播平台参数对比

主流网络视音频平台点播服务的参数对比如表所示:
现有互联网视音频平台参数对比
dianbo
可以看出,点播服务普遍采用了HTTP作为流媒体协议,H.264作为视频编码格式,AAC作为音频编码格式。采用HTTP作为点播协议有以下两点优势:一方面,HTTP是基于TCP协议的应用层协议,媒体传输过程中不会出现丢包等现象,从而保证了视频的质量;另一方面,HTTP被绝大部分的Web服务器支持,因而流媒体服务机构不必投资购买额外的流媒体服务器,从而节约了开支。点播服务采用的封装格式有多种:MP4,FLV,F4V等,它们之间的区别不是很大。视频编码标准和音频编码标准是H.264和AAC。这两种标准分别是当今实际应用中编码效率最高的视频标准和音频标准。视频播放器方面,无一例外的都使用了Flash播放器。

H.265.

首先可以明确的是 H265 属于一种视频编码格式, 这里有一篇比较专业的说明, HEVC / H.265 Explained
h265

说到H.265, 就不得不先提一下H.264了, H.264仅仅是一个编码标准,而不是一个具体的编码器,H.264只是给编码器的实现提供参照用的。
Google推出的VP8属于和H.264同一时代的标准。总体而言,VP8比H.264要稍微差一点。除了在技术领域,VP8和H.264在专利等方面也是打的不可开交。
可以武断的说,H264.是现在最主流的视频编码标准.
而H.265 是 H.264/MPEG-4 AVC的继任者。目前正在由ISO/IEC MPEG和ITU-T VCEG开发中。为此目的MPEG与VCEG联合成立了一个JCT-VC(JointCollaborative Team on Video Coding)作为共同开发HEVC的团队。HEVC被认为不仅提升图像质量,同时也能达到H.264/MPEG-4 AVC两倍之压缩率(等同于同样画面质量下比特率减少了50%),可支持4K分辨率甚至到超高画质电视,最高分辨率可达到8192×4320(8K分辨率)。第一版的HEVC/H.265视频压缩标准在2013年4月13日被接受为国际电信联盟(ITU-T)的正式标准。
VP9是一个由Google开发的开放的视频压缩标准。VP9将是VP8的后继者。VP9的开发始于2011。VP9的目标之一是相同质量下相对于VP8可以减少50%的比特率。而另一个目标就是争取能在压缩效率上高于HEVC。2013年2月21日,第一个支持VP9解码技术的Google Chrome网页浏览器发布了。
HEVC VP9 以及x264的性能对比, 结果参考 Dan Grois等人的论文《Performance Comparison of H.265/MPEG-HEVC, VP9, andH.264/MPEG-AVC Encoders》

选用的编码器如下:

HEVC:HM
VP9:libvpx
H.264:x264

HEVC性能最强,奇怪的是x264的性能竟然好于VP9。要知道VP9可是Google推出的下一代编码标准。x264太强悍了!

PS:此外,x264的速度是远远高于HEVC和VP9的。

对比1

下表显示了HEVC在同等质量的前提下(以PSNR为依据),相对于VP9和x264节约的码率。下表显示了所有序列的情况。总体来说HEVC相对于VP9节约了41.9%,HEVC相对于x264节约了38.9%。

对比2

下表显示了三种编码器整体性能的比较。表中百分比数字的意义是:同等视频质量的前提下,该列所属的编码器相对于该行所属的编码器节约的码率,如果为负值,则代表反而消耗了更多的码率。例如,同等质量的前提下,x264相对于VP9节约了8.4%的码率。

对比3

同等视频质量的前提条件下,编码消耗时间对比如下表所示。可以看出,VP9编码时间大约是x264的130倍。HEVC编码时间大约是VP9的7倍。

对比4

此外,在码率一定的情况下,几种编码标准结果大致是这样的:HEVC > VP9 > H.264> VP8 > MPEG4 > H.263 > MPEG2。

HEVC 在未来拥有很大的优势, 具体可以参考以下文字段

在数字视频应用产业链的快速发展中,面对视频应用不断向高清晰度、高帧率、高压缩率方向发展的趋势,当前主流的视频压缩标准协议H.264(AVC)的局限性不断凸显。同时,面向更高清晰度、更高帧率、更高压缩率视频应用的HEVC(H.265)协议标准应运而生。本文重点分析了下一代视频压缩协议标准HEVC(H.265)的技术亮点,并对其在未来应用中将给整个产业带来的深刻变化予以展望。

H.264(AVC)从2003年5月草稿发布以来,凭借其相对于以往的视频压缩标准在压缩效率以及网络适应性方面的明显优势,逐步成为视频应用领域的主流标准。根据 MeFeedia的数据,由于iPad以及其它新兴设备大多支持H.264硬件加速,至2011年底,80%的视频使用H.264编码,并且随着支持H.264解码的设备不断增多,这一占有率还将进一步增长。

  但是,随着数字视频应用产业链的快速发展,视频应用向以下几个方向发展的趋势愈加明显:

(1) 高清晰度(HigherDefinition):数字视频的应用格式从720 P向1080 P全面升级,在一些视频应用领域甚至出现了4K x 2K、8K x 4K的数字视频格式;

(2) 高帧率(Higherframe rate ):数字视频帧率从30fps向60fps、120fps甚至240fps的应用场景升级;

(3) 高压缩率(HigherCompression rate ):传输带宽和存储空间一直是视频应用中最为关键的资源,因此,在有限的空间和管道中获得最佳的视频体验一直是用户的不懈追求。

  由于数字视频应用在发展中面临上述趋势,如果继续采用H.264编码就出现的如下一些局限性:

(1) 宏块个数的爆发式增长,会导致用于编码宏块的预测模式、运动矢量、参考帧索引和量化级等宏块级参数信息所占用的码字过多,用于编码残差部分的码字明显减少。

(2) 由于分辨率的大大增加,单个宏块所表示的图像内容的信息大大减少,这将导致相邻的4 x 4或8 x 8块变换后的低频系数相似程度也大大提高,导致出现大量的冗余。

(3) 由于分辨率的大大增加,表示同一个运动的运动矢量的幅值将大大增加,H.264中采用一个运动矢量预测值,对运动矢量差编码使用的是哥伦布指数编码,该编码方式的特点是数值越小使用的比特数越少。因此,随着运动矢量幅值的大幅增加,H.264中用来对运动矢量进行预测以及编码的方法压缩率将逐渐降低。

(4) H.264的一些关键算法例如采用CAVLC和CABAC两种基于上下文的熵编码方法、deblock滤波等都要求串行编码,并行度比较低。针对GPU/DSP/FPGA/ASIC等并行化程度非常高的CPU,H.264的这种串行化处理越来越成为制约运算性能的瓶颈。

  为了面对以上发展趋势,2010年1月,ITU-T VCEG(VideoCoding Experts Group) 和ISO/IEC MPEG(Moving Picture Experts Group)联合成立JCT-VC(JointCollaborative Team on Video Coding)了联合组织,统一制定下一代编码标准:HEVC(High Efficiency Video Coding)。
HEVC协议标准计划于2013年2月份正式在业界发布,目前整个框架结构已基本确定。截至2012年4月份,JCT-VC联合工作组已经召开了第八次会议,并于2012年2月17日发布了第一版内部草稿《High efficiency videocoding (HEVC) text specification draft 6》,计划2012年7月发布第一版公开版草稿,在H.264标准2~4倍的复杂度基础上,将压缩效率提升一倍以上。

Share Comments

WebView开发小记.

最近重构了公司的播放器模块,重新整理了整套代码,并且加入了StateMachine, 然后现在处于提测阶段, 打算在提测确认无bug之后进行一次StateMachine使用的总结,在这个任务空档期,老大安排了我去搞一下公司的WebView模块, 也算是换换脑子.

该文章记录一下几个在开发过程中的小经验

  • 当你使用WebView加载网页的时候, 如果这个网页有JavaScript, 你要跟它进行交互的话,你需要打开开关.

    1
    WebView.getSetting.setJavaScriptEnabled(true);
  • Java与JavaScript 交互比较关键的一个方法是

    1
    WebView.addJavascriptInterface(Object, String);

    这个方法简单的来说
    就是往JavaScript 注入一个可以供他们使用,进行回调的Object, String 就是 这个Object的名字.
    举例来说:
    比如 我们在Java代码中 写了这样一段代码

    1
    2
    3
    4
    5
    6
    WebView.addJavascriptInterface(new Object(){
    @JavascriptInterface
    public void inject() {
    Log.d(TAG, "Inject success");
    }
    }, "InjectObject");

    而在Javascript中 代码如下

    1
    InjectObject.inject("hi, i'm Javascript");

    这样Java中就可以回调打印出Inject success的字段.
    这里需要特殊说明的一点就是 addJavascriptInterface的Object 中必须有一个方法是声明 @JavascriptInterface 的, 否则是无法add进去的.
    并且只有声明了 @JavascriptInterface 的方法 才能被JavaScript 成功调用.

  • Java 和 Javascript 交互的本质是字符串的传递.
    Java 无论你上层如何封装, 最终将字符串从Java 传到Javascript 都是需要这样去传递的.
    1
    WebView.loadUrl("javascript:WebViewJavascriptBridge._handleMessageFromJava('%s')")

其中 javascript: 是必须的. WebViewJavascriptBridge 是Javascript那边的对象,_handleMessageFromJava 是这个对象所拥有的方法. ()内就是要传递过去的字符串!
这里有一点需要注意, loadUrl方法会进行checkThread(); 所以也就是说你必须在MainThread中调用该方法.

  • WebView 有内存泄漏的问题, 属于系统代码的1个bug, 在这里我们做了特殊的 WorkAround.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private void recycle() {
    try {
    MagicWebView thiz = this;
    thiz.setWebViewClient(null);
    thiz.setWebChromeClient(null);

    Field field = View.class.getDeclaredField("mContext");
    field.setAccessible(true);
    field.set(thiz, getContext().getApplicationContext());
    field.setAccessible(false);
    field = View.class.getDeclaredField("mParent");
    field.setAccessible(true);
    field.set(thiz, null);
    field.setAccessible(false);
    field = View.class.getDeclaredField("mLayoutParams");
    field.setAccessible(true);
    field.set(thiz, null);
    field.setAccessible(false);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

这样并不能完全解决WebView 会泄露的问题, WebView 依旧会被泄露, 但是WebView跟相应的Activity 会完全解耦,
不会造成Activity也跟着泄露.

  • 在Https的网址下,有的H5图片会加载不正常. 这个时候需要加入这样一个设置
    1
    2
    3
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
    }
Share Comments

android工程师简历.

Share Comments

购房记.

转眼来北京就9年多了,毕业也5年了, 随着年龄越来越大, 以及经历各种合租的痛楚之后,越发的希望自己能有一套自己的房子.
原本的计划是在回龙观里买一个商住两用房, 30多平米的那种.
买这种小产权的房子也是无奈,北京买房是需要购房资格的,需要连续5年社保.
我如果从毕业开始算起,也算是交了5年了,但是中间有一段时间从中华英才网离职,在家学习开发,断交了一段时间.
导致我失去了购房资格. 所以如果强行在北京买, 只能买这种小产权的房子.

由于哥哥在回龙观买的房子,所以我毕业以后一直在回龙观这边住,也算是半个观里人了, 对这里比较熟悉,比较依赖.
所以优先考虑在回龙观去选了,也基本上只考虑这里了. 仔细观察了下, 符合我要求的房子, 竟然只有一个楼盘.
叫东亚上北中心.
总结一下这里房子的特点.

优点
  • 因为面积小,只有37平米附近, 大概需要120w附近,总价相对来说比较低.
缺点
  • 小户型. 使用面积只有20多平.
  • 超北 没有阳光
  • 贷款只能走商贷,而且只能10年. 每个月还款压力很大
  • 问了下银行这种房子,我最多只能贷款30w, 也就是说需要至少90W的首付! 这也是一个致命伤, 是我根本承受不起的价格.
  • 以后就算有了孩子,也不能上学.更别说落户了.
做决定的那几天想了很多事情,感觉买这种房子的话,一辈子的生活都要搭进去了, 而且父母那边也要卖掉老家空置的那套房子,然后再加上哥哥的帮忙,也不一定能凑够首付. 所以最后选择了放弃.

然后买房这件事就暂时冷了下来,我开始静下来考虑一些漫无边际的事情,大概就是人生啊,生活啊,以后啊云云的.
思索了几个月也没思索出什么结果, 反倒是之前看的那个房子又涨了几十万….北京的房子真是令人不能琢磨啊.
但是这种漫无边际的涨价到是坚定了我买房的心.
脑子不知道怎么回事就冒出了去济南买房的念头,然后跟父母大概提了下,父母对这件事也一直很是上心,是他们的一个心病.
考虑去济南的原因大概有这么几个

  • 济南IT行业虽然说不是很好,但是还是有的.以后去的话还是可以做相关的工作.
  • 是山东的省会,作为一个山东人回自己的省会发展还是说的过去的.
  • 房价还算温和, 除了东面高新区猛涨以外,西面南面还是很靠谱的.
  • 离北京很近,高铁1个半小时就到了. 离菏泽也不远,如果菏泽开了动车以后,应该也是2个小时以内的行程.
  • 济南有很多老家的朋友 和 亲戚.

父母也是做了决定以后, 立马就动身去了济南, 不过一开始看房的方向是有点选错了,这也是我的问题.
因为我一直在北京的缘故, 总是不由自主的去考虑二手房, 导致我父母浪费了很多时间去看二手房,
济南的二手房都是那种很破很烂的那种,并且价格很高.
最后立哥带着我父母去了一个新的楼盘,恒大的房子. 我父母一眼就看中了样板间,然后就跟我开始商量决定要买.
我综合考虑了下这房子的优点

  • 均价 9100 附近
  • 未来出门就是地铁
  • 离济南大学超近, 不知道为什么,我总感觉离大学近是好事.
  • 离我立哥比较近
  • 送精装修, 先不说是不是羊毛出在羊身上了, 因为我要一直在北京上班,没有时间去管装修的事.

虽然现在是列了那么多,但是当时只是脑子已经乱了,每天我在北京上着班,听着父母传来的信息,感觉父母也累到极限了, 也实在是不想
让父母再去逛了,于是我立马拍板了,就他了.
决定之后也就好说了,第二天4点50就起床然后高铁到了济南, 直接打车到了楼盘, 然后我亲自看了下样板间,看起来确实很不错!
然后接下来就一路程序走下来, 认购 -> 去银行面签 -> 然后签合同. 一切算是尘埃落定了吧

下面是房子的一些照片

户型图

主卧

主卫

过道

次卧1

次卧2

客厅

客厅换了个角度

Share Comments