为Glide增加加载视频缩略图的feature

基本概念

因为Glide支持gif的缘故,这段时间准备把项目整体由UIL替换为Glide。没想到之前在我眼里没啥特点的UIL还有个了不得的feature,它支持直接传入视频的绝对路径然后加载视频缩略图的功能,而Glide并没有这个功能,但是看过Glide的源码之后,感觉完全可以利用Glide的ModelLoader的接口动手实现一个。

代码实现

首先看代码之前必须对Glide的源码有个大概的了解,推荐看这篇Glide源码分析。然后开始编码:

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

public class VideoThumbnailLoader implements ModelLoader<Uri, InputStream> {

public VideoThumbnailLoader() {
}

@Override
public DataFetcher<InputStream> getResourceFetcher(Uri model, int width, int height) {
return new VideoThumbnailFetcher(model);
}
}

public class VideoThumbnailFetcher implements DataFetcher<InputStream> {

private final Uri mUri;
private InputStream inputStream;

public VideoThumbnailFetcher(Uri uri) {
this.mUri = uri;
}

@Override
public InputStream loadData(Priority priority) throws Exception {
Bitmap bitmap = ThumbnailUtils.createVideoThumbnail(mUri.getPath(), MediaStore.Images.Thumbnails.MINI_KIND);
if (bitmap != null) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos);
inputStream = new ByteArrayInputStream(bos.toByteArray());
}
return inputStream;
}

@Override
public void cleanup() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Do nothing.
}
}
}

@Override
public String getId() {
return mUri.toString();
}


@Override
public void cancel() {

}

代码很简单,在关键的loadData方法里面实现将视频的绝对路径转换为InputStream的工作,使用的工具是SDK提供的ThumbnailUtils这个类,这个工具类能方便地将路径转换为缩略图。然后再看怎么使用:

1
2
3
4
5
6

Glide.with(context)
.using(new VideoThumbnailLoader(),InputStream.class)
.load(Uri.parse(uri))
.as(Bitmap.class)
.into(imageView);

ok,大功告成,这样使用就可以直接传入视频路径加载为缩略图了。

但是在研究ModelLoader的过程中,我发现Glide已经有一个类似的实现了MediaStoreStreamLoader,为了方便我直接贴出对应的MediaStoreThumbFetcher的完整实现:

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

public class MediaStoreThumbFetcher implements DataFetcher<InputStream> {
private static final String TAG = "MediaStoreThumbFetcher";
private static final int MINI_WIDTH = 512;
private static final int MINI_HEIGHT = 384;
private static final ThumbnailStreamOpenerFactory DEFAULT_FACTORY = new ThumbnailStreamOpenerFactory();

private final Context context;
private final Uri mediaStoreUri;
private final DataFetcher<InputStream> defaultFetcher;
private final int width;
private final int height;
private final ThumbnailStreamOpenerFactory factory;
private InputStream inputStream;

public MediaStoreThumbFetcher(Context context, Uri mediaStoreUri, DataFetcher<InputStream> defaultFetcher,
int width, int height)
{

this(context, mediaStoreUri, defaultFetcher, width, height, DEFAULT_FACTORY);
}

MediaStoreThumbFetcher(Context context, Uri mediaStoreUri, DataFetcher<InputStream> defaultFetcher, int width,
int height, ThumbnailStreamOpenerFactory factory) {
this.context = context;
this.mediaStoreUri = mediaStoreUri;
this.defaultFetcher = defaultFetcher;
this.width = width;
this.height = height;
this.factory = factory;
}

@Override
public InputStream loadData(Priority priority) throws Exception {
ThumbnailStreamOpener fetcher = factory.build(mediaStoreUri, width, height);

if (fetcher != null) {
inputStream = openThumbInputStream(fetcher);
}

if (inputStream == null) {
inputStream = defaultFetcher.loadData(priority);
}

return inputStream;
}

private InputStream openThumbInputStream(ThumbnailStreamOpener fetcher) {
InputStream result = null;
try {
result = fetcher.open(context, mediaStoreUri);
} catch (FileNotFoundException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to find thumbnail file", e);
}
}

int orientation = -1;
if (result != null) {
orientation = fetcher.getOrientation(context, mediaStoreUri);
}

if (orientation != -1) {
result = new ExifOrientationStream(result, orientation);
}
return result;
}

@Override
public void cleanup() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Do nothing.
}
}
defaultFetcher.cleanup();
}

@Override
public String getId() {
return mediaStoreUri.toString();
}

@Override
public void cancel() {
// Do nothing.
}

private static boolean isMediaStoreUri(Uri uri) {
return uri != null
&& ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
&& MediaStore.AUTHORITY.equals(uri.getAuthority());
}

private static boolean isMediaStoreVideo(Uri uri) {
return isMediaStoreUri(uri) && uri.getPathSegments().contains("video");
}

static class FileService {
public boolean exists(File file) {
return file.exists();
}

public long length(File file) {
return file.length();
}

public File get(String path) {
return new File(path);
}
}

interface ThumbnailQuery {
Cursor queryPath(Context context, Uri uri);
}

static class ThumbnailStreamOpener {
private static final FileService DEFAULT_SERVICE = new FileService();
private final FileService service;
private ThumbnailQuery query;

public ThumbnailStreamOpener(ThumbnailQuery query) {
this(DEFAULT_SERVICE, query);
}

public ThumbnailStreamOpener(FileService service, ThumbnailQuery query) {
this.service = service;
this.query = query;
}

public int getOrientation(Context context, Uri uri) {
int orientation = -1;
InputStream is = null;
try {
is = context.getContentResolver().openInputStream(uri);
orientation = new ImageHeaderParser(is).getOrientation();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open uri: " + uri, e);
}
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
return orientation;
}

public InputStream open(Context context, Uri uri) throws FileNotFoundException {
Uri thumbnailUri = null;
InputStream inputStream = null;

final Cursor cursor = query.queryPath(context, uri);
try {
if (cursor != null && cursor.moveToFirst()) {
thumbnailUri = parseThumbUri(cursor);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (thumbnailUri != null) {
inputStream = context.getContentResolver().openInputStream(thumbnailUri);
}
return inputStream;
}

private Uri parseThumbUri(Cursor cursor) {
Uri result = null;
String path = cursor.getString(0);
if (!TextUtils.isEmpty(path)) {
File file = service.get(path);
if (service.exists(file) && service.length(file) > 0) {
result = Uri.fromFile(file);
}
}
return result;
}
}

static class ImageThumbnailQuery implements ThumbnailQuery {
private static final String[] PATH_PROJECTION = {
MediaStore.Images.Thumbnails.DATA,
};
private static final String PATH_SELECTION =
MediaStore.Images.Thumbnails.KIND + " = " + MediaStore.Images.Thumbnails.MINI_KIND
+ " AND " + MediaStore.Images.Thumbnails.IMAGE_ID + " = ?";

@Override
public Cursor queryPath(Context context, Uri uri) {
String imageId = uri.getLastPathSegment();
return context.getContentResolver().query(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,
PATH_PROJECTION,
PATH_SELECTION,
new String[] { imageId },
null /*sortOrder*/);
}
}

static class VideoThumbnailQuery implements ThumbnailQuery {
private static final String[] PATH_PROJECTION = {
MediaStore.Video.Thumbnails.DATA
};
private static final String PATH_SELECTION =
MediaStore.Video.Thumbnails.KIND + " = " + MediaStore.Video.Thumbnails.MINI_KIND
+ " AND " + MediaStore.Video.Thumbnails.VIDEO_ID + " = ?";

@Override
public Cursor queryPath(Context context, Uri uri) {
String videoId = uri.getLastPathSegment();
return context.getContentResolver().query(
MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,
PATH_PROJECTION,
PATH_SELECTION,
new String[] { videoId },
null /*sortOrder*/);
}
}

static class ThumbnailStreamOpenerFactory {

public ThumbnailStreamOpener build(Uri uri, int width, int height) {
if (!isMediaStoreUri(uri) || width > MINI_WIDTH || height > MINI_HEIGHT) {
return null;
} else if (isMediaStoreVideo(uri)) {
return new ThumbnailStreamOpener(new VideoThumbnailQuery());
} else {
return new ThumbnailStreamOpener(new ImageThumbnailQuery());
}
}
}
}

根据代码我们开一看出来在这里Glide是使用CursorContentResolver去获取路径对应的缩略图,属于官方推荐的方法。但是坏处是它只能传入content:这种格式的路径,不支持传入绝对路径。但是无论如何这个思路输入官方推荐的思路,而且以后为了兼容Android N 恐怕我们项目里相关的代码都要这么写,我提供的这个方案只能作为临时的应急方案。

IntentService、HandlerThread相关

最近看Android基础,看过了Handler源码以及对应的HandlerThread,包括涉及到的ThreadLocal类,觉得很有意思。这几个类网上的源码分析比较多,也算是比较经典的Android源码,我就不分析了。这篇要记录的是另一个组件IntentService的源码相关,源码不多但是很有意思。

基本概念

IntentService应该属于那种用过的人很熟,没用过的人没听过的组件。源码的注释是这样的:

IntentService is a base class for {@link Service}s that handle asynchronous requests (expressed as {@link Intent}s) on demand. Clients send requests through {@link android.content.Context#startService(Intent)} calls; the service is started as needed, handles each Intent in turn using a worker thread, and stops itself when it runs out of work.

简单的说,就是对于一般的Service来说,无论我们是start还是bind,都要自己管理Service的销毁。而IntentService呢,你可以把耗时操作扔给它,然后就不用管了,它自己处理完了以后就把自己关掉了。

源码分析

IntentService的源码太少了,我直接就把整个类copy了:

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

public abstract class IntentService extends Service {
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private String mName;
private boolean mRedelivery;

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

public IntentService(String name) {
super();
mName = name;
}

public void setIntentRedelivery(boolean enabled) {
mRedelivery = enabled;
}

@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

@Override
public void onDestroy() {
mServiceLooper.quit();
}

@Override
@Nullable
public IBinder onBind(Intent intent) {
return null;
}

@WorkerThread
protected abstract void onHandleIntent(@Nullable Intent intent);
}

源码很少。首先这是一个抽象类,继承自Service,继承这个类的话必须实现onHandleIntent这个方法,也是在这个方法里我们进行耗时操作。

首先我们看onCreate方法,也是Service的生命周期毁掉方法,在这里首先会new一个HandlerThread出来,然后拿到这个HandlerThread线程对应的Looper,并使用这个Looper构建了一个ServiceHandler出来。ServiceHandler继承自Handler。在onStart方法里面,其实就是我们常用的用ServiceHandler发送了一个Message出去,在handleMessage方法这里会调用我们刚才说到的onHandleIntent方法,这个方法执行完毕以后接着调用stopSelf方法关掉自己。接着在onDestroy方法里会调用Looper.quit方法跳出Looper的循环。

逻辑很简单,我之所以觉得有意思的是Google的工程师真的是把自己写的组件用的淋漓尽致,看起来挺有用的IntentService,其实本质上就是利用了HandlerThread自己能够提供线程对应的Looper的特性,实现了一个IntentService-HandlerThread-Handler的绑定。用最简单易懂的代码实现了一个异步耗时任务的Service。

既然分析到这里,那么正好也分析一下HandlerThread的这个特性,源码仍然不多:

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

public class HandlerThread extends Thread {
int mPriority;
int mTid = -1;
Looper mLooper;

public HandlerThread(String name) {
super(name);
mPriority = Process.THREAD_PRIORITY_DEFAULT;
}

public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}

protected void onLooperPrepared() {
}

@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

public Looper getLooper() {
if (!isAlive()) {
return null;
}
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}

public boolean quit() {
Looper looper = getLooper();
if (looper != null) {
looper.quit();
return true;
}
return false;
}

public boolean quitSafely() {
Looper looper = getLooper();
if (looper != null) {
looper.quitSafely();
return true;
}
return false;
}

public int getThreadId() {
return mTid;
}
}

核心代码都在run方法里面了。其实就做了三件事情(假设看到这里的同学都熟悉Looper的源码了),Looper.prepare()mLooper = Looper.myLooper()Looper.loop()。熟悉Looper的同学肯定已经了解了,因为我们在任何地方想要自己实现一个子线程的Handler,都需要手动调用Looper.prepare()Looper.loop()。官方提供的这个HandlerThread其实就是帮我们做了这个事情,然后再对外暴露一个getLooper()方法,你就可以拿到这个线程对应的Looper来构建对应的Handler。IntentService也是利用了这一点,充分精简了代码。

总结

IntentService与其说是官方提供的一个组件,不如说是HandlerThread的一个最佳实践。他们的源码都没有什么深度,跟Handler相关的几个类都差很远,但是给我们提供的编码思路却是很有用的。

OkHttp源码结构分析

基本概念

之前一篇简单介绍了OkHttp的概况和使用方式,以及如何与Volley一起使用,知其然也要知其所以然,这一篇大概介绍下这个网络框架的源码结构。基于OkHttp 3.5.0

基本流程

OkHttp源码结构图

这个框架作为Android开发者应该是很熟悉的。入口的构建是建造者模式,在请求的时候手动构建一个Request,通过OkHttpClientnewCall()方法发起请求,获取Response,也就是请求最终的结果。代码如下:

1
2
3
4
5
6
7
8
9
10
11

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();

Response response = client.newCall(request).execute();
return response.body().string();
}

主要组件

OkHttpClient.Bulider

  • 负责构建OkHttpClient,Square官方的建议是全局共用一个OkHttpClient,实际上OkHttpClient虽然有public的狗仔方法,内部也是传入一个默认的Builder.

OkHttpClient

  • 处理配置这个client的参数,核心方法其实就一个:
1
2
3
4

@Override public Call newCall(Request request) {
return new RealCall(this, request, false);
}

Request

  • 这个类只用来存储请求的数据,不做实际的事情,具体请求的发起由下一个RealCall来做

RealCall

  • RealCall实现了Call接口,关于这个接口的作用,注释已经说清楚了,RealCall是唯一的实现,也就是OkHttp实际用来请求的类

A call is a request that has been prepared for execution. A call can be canceled. As this object
represents a single request/response pair (stream), it cannot be executed twice.

  • 那么怎么干活呢?Call这个接口有两个关键的实现方法,execute()enqueue(),方法名意思已经很明确了,一个同步一个异步,我们只需要看这两个方法的实现就可以了:
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

@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);
}
}

@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

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);
}
}
}

同步方法很直接,通过getResponseWithInterceptorChain()直接取Response,异步方法则间接一点,包装一个Runnable扔给线程池,本质上最终使用的也是getResponseWithInterceptorChain()。然后我们就要看这个方法是何方神圣了:

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

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);
}

熟悉OkHttp的同学都知道OkHttp有一个很厉害的东西,叫Interceptor,只要实现了这个接口,就可以在一个请求的RequestResponse之间做很多文章,比如写个LogInterceptor打印请求的各种信息,实现也特别优雅,但是看了源码才知道,Interceptor贯穿了请求的全部,无论是重试机制、缓存还是具体请求,实际上都是通过Interceptor来做的。那么接下来就分析源码中用到的这些Interceptor.

RetryAndFollowUpInterceptor

  • 负责重试和重定向

BridgeInterceptor

  • 负责把请求转换为发送给服务器的请求,把服务器返回的结果包装成我们接收到的Response

CacheInterceptor

  • 缓存命中时则直接返回结果,未命中时则在网络请求结果返回时更新缓存

ConnectInterceptor

  • 负责与服务器建立连接

CallServerInterceptor

  • 负责向服务器发送请求,从服务器读取返回结果

环环相扣,是一个典型的责任链模式,这些Interceptor与我们平时写的简单的Interceptor本质是一样的,都是实现一个intercept(Chain chain)方法,然后在RequestResponse上做文章,但是套在一起,就实现了一个完整的网络请求过程,同时还完成了重试、缓存这些工作。具体的代码就不分析了···因为具体发起请求和读取返回结果又是HttpCodecOkio这两个大家伙的工作,分析起来又是大把的时间耗在里面了(都是泪)。有兴趣的同学可以读读看。如果只是分析OkHttp大的源码结构的话,这几个类就足够了。

在项目中使用OkHttp

What

对于OkHttp的简介就直接复制官网了:

HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth.

OkHttp is an HTTP client that’s efficient by default:

  • HTTP/2 support allows all requests to the same host to share a socket.
  • Connection pooling reduces request latency (if HTTP/2 isn’t available).
  • Transparent GZIP shrinks download sizes.
  • Response caching avoids the network completely for repeat requests.
  • Supports both synchronous blocking calls and async calls with callbacks.

OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and for services hosted in redundant data centers. OkHttp initiates new connections with modern TLS features (SNI, ALPN), and falls back to TLS 1.0 if the handshake fails.

Using OkHttp is easy. Its request/response API is designed with fluent builders and immutability. It supports both synchronous blocking calls and async calls with callbacks.

OkHttp supports Android 2.3 and above. For Java, the minimum requirement is 1.7.

Why

Android为我们提供了两种HTTP交互的方式:HttpURLConnectionApache-HTTPClient,虽然两者都支持HTTPS,流的上传和下载,配置超时,IPv6和连接池,已足够满足我们各种HTTP请求的需求。但更高效的使用HTTP可以让应用运行更快、更节省流量。而OkHttp就是为此而生。

那么除了更cool之外,为什么要用OkHttp替换掉项目里用了很久的HttpClientHttpURLConnection

首先,从实际使用的角度看,OkHttp除了具备HttpClientHttpURLConnection具有的功能以外,

  • 本身支持同步和异步请求,OkHttpRequestReponse现在都用了建造者模式,写起来更清晰,即使不封装代码量也并不大

一个典型的OkHttp异步请求:

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

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Call call, IOException e) {
e.printStackTrace();
}

@Override public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

Headers responseHeaders = response.headers();
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}

System.out.println(response.body().string());
}
});
}

  • 共享同一个Socket来处理同一个服务器的所有请求,利用连接池技术减少请求延迟,缓存响应数据来减少重复的网络请求
  • 支持GZIP,不需要我们手动处理GZIP
  • 自带更强到的IO操作框架Okio
  • 从很多常用的连接问题中自动恢复
  • 如果服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP
  • 提供了强大的Interceptors(拦截器),我们可以通过注册自己的Interceptors来对每次请求进行监控和改写,比如最基础的打印每次请求的基本信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

Log.d("request url:",request.url());

Response response = chain.proceed(request);

Log.d("reponse url:",response.request().url());

return response;
}
}

另外从外部因素来说,Google官方从Android4.4的时候已经开始使用OkHttp,sdk23彻底抛弃了HttpClient而采用OkHttp。许多新的强大的开源库也默认支持OkHttp作为网络传输层,例如比Volley更快的Retrofit

OkHttp目前的这些优势应该已经足够有理由替换掉HttpClientHttpURLConnection了(单说网络请求传输速度,OkHttp并没有更快,因为HttpClient已经很强大了,它的优势主要还是在于配置简单,易用以及官方和开源社区的支持)。

How

比较典型的是替换Volley中的HttpStack

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
public class OkHttpStack implements HttpStack {

private OkHttpClient mOkHttpClient;

private final UrlRewriter mUrlRewriter;

public interface UrlRewriter {
public String rewriteUrl(String originalUrl);
}


public OkHttpStack(OkHttpClient okHttpClient) {
this(null, okHttpClient);
}

public OkHttpStack(UrlRewriter urlRewriter, OkHttpClient okHttpClient) {
this.mOkHttpClient = okHttpClient;
this.mUrlRewriter = urlRewriter;
}

@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError {
//加工url,需要UrlRewriter
String url = request.getUrl();
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
throw new IOException("URL blocked by rewriter: " + url);
}
url = rewritten;
}

//设置header
okhttp3.Request.Builder okHttpRequestBuilder = new okhttp3.Request.Builder();
Map<String, String> headers = new HashMap<String, String>();
headers.putAll(request.getHeaders());
headers.putAll(additionalHeaders);
for (String headerName : headers.keySet()) {
okHttpRequestBuilder.header(headerName, headers.get(headerName));
}

//设置connectionParameters
setConnectionParametersForRequest(okHttpRequestBuilder, request);

//发起请求
okhttp3.Request okHttpRequest = okHttpRequestBuilder.url(url).build();
Response okHttpResponse = mOkHttpClient.newCall(okHttpRequest).execute();

//转换Response
StatusLine responseStatus = new BasicStatusLine(parseProtocol(okHttpResponse.protocol()), okHttpResponse.code(), okHttpResponse.message());
BasicHttpResponse response = new BasicHttpResponse(responseStatus);
response.setEntity(entityFromOkHttpResponse(okHttpResponse));
Headers responseHeaders = okHttpResponse.headers();
for (int i = 0, len = responseHeaders.size(); i < len; i++) {
final String name = responseHeaders.name(i), value = responseHeaders.value(i);
if (name != null) {
response.addHeader(new BasicHeader(name, value));
}
}
return response;
}

private static HttpEntity entityFromOkHttpResponse(Response r) throws IOException {
BasicHttpEntity entity = new BasicHttpEntity();
ResponseBody body = r.body();
entity.setContent(body.byteStream());
entity.setContentLength(body.contentLength());
entity.setContentEncoding(r.header("Content-Encoding"));

if (body.contentType() != null) {
entity.setContentType(body.contentType().type());
}
return entity;
}

private static ProtocolVersion parseProtocol(final Protocol protocol) {
switch (protocol) {
case HTTP_1_0:
return new ProtocolVersion("HTTP", 1, 0);
case HTTP_1_1:
return new ProtocolVersion("HTTP", 1, 1);
case SPDY_3:
return new ProtocolVersion("SPDY", 3, 1);
case HTTP_2:
return new ProtocolVersion("HTTP", 2, 0);
}

throw new IllegalAccessError("Unkwown protocol");
}

private void setConnectionParametersForRequest(okhttp3.Request.Builder builder, Request<?> request) throws IOException, AuthFailureError {
byte[] postBody = null;
switch (request.getMethod()) {
case Request.Method.DEPRECATED_GET_OR_POST:
postBody = request.getBody();
if (postBody != null) {
builder.post(RequestBody.create(MediaType.parse(request.getBodyContentType()), postBody));
} else {
builder.get();
}
break;
case Request.Method.GET:
builder.get();
break;
case Request.Method.DELETE:
builder.delete();
break;
case Request.Method.POST:
postBody = request.getBody();
if (postBody == null) {
builder.post(RequestBody.create(MediaType.parse(request.getBodyContentType()), ""));
} else {
builder.post(RequestBody.create(MediaType.parse(request.getBodyContentType()), postBody));
}
break;
case Request.Method.PUT:
postBody = request.getBody();
if (postBody == null) {
builder.put(RequestBody.create(MediaType.parse(request.getBodyContentType()), ""));
} else {
builder.put(RequestBody.create(MediaType.parse(request.getBodyContentType()), postBody));
}
break;
case Request.Method.HEAD:
builder.head();
break;
case Request.Method.OPTIONS:
builder.method("OPTIONS", null);
break;
case Request.Method.TRACE:
builder.method("TRACE", null);
break;
case Request.Method.PATCH:
postBody = request.getBody();
if (postBody == null) {
builder.patch(RequestBody.create(MediaType.parse(request.getBodyContentType()), ""));
} else {
builder.patch(RequestBody.create(MediaType.parse(request.getBodyContentType()), postBody));
}
break;
default:
throw new IllegalStateException("Unknown method type.");
}
}
}

Android Gradle DexOptions

最近在Gradle的dexOptions这部分踩了很多坑,浪费了很多时间。这部分的东西其实官方文档和StackOverFlow上基本都讲清楚了,所以以翻译和记录为主。

Java和Android在编译过程中的区别

当编译一段典型的Java代码时,会产生*.class文件,一个class文件包含标准Java字节码,被我们常见的JVM执行。

Android的编译过程则有所不同,编译器不会产生class文件,而会产生.dex文件(这个大家应该都不陌生了),.dex文件会被Android Virtual Machine (也就是我们常说的dalvik虚拟机)执行,而不是被JVM执行。
所以dexOptions是一个确定java代码转换为Android代码的gradle配置,配置项大概包括以下几个:

1
2
3
4
5
6
7
8
android {
dexOptions {
jumboMode true
incremental true
javaMaxHeapSize "4g"
preDexLibraries true
}
}

然后对这几个配置项大概做一个解释:

boolean incremental

是否对dex开启增量模式配置,这个选项有许多限制和问题,而且有可能不起作用,谨慎使用。

boolean jumboModeal

是否对dex开启特大模式(直接翻译的···)

String javaMaxHeapSize

设置java最大堆内存的值,必须是1024M的倍数

boolean preDexLibraries

是否将引用的库提前转换为dex,开启这个选项可以加速增量模式,但是在clean项目的时候会变慢。

在一般情况下(注意是一般情况),将这几个配置设为true并且增大javaMaxHeapSize的值(在电脑配置允许的情况下),可以显著加快项目build的速度,当然偶尔也会遇到一些奇怪的BUILD FAILED的问题,这个就只能看具体情况了。

HotFix

背景

Android的项目结构越来越大,相对应出现Bug的情况也越来越多,传统机制下每当出现一个紧急Bug都必须重新打包、测试、发布,用户需要下载、覆盖安装,成本很高,所以很多团队都在寻找方法,通过补丁的形式修复Bug,不用重新发布再让用户下载安装。

我们的Android客户端目前的问题是一些比较重要的逻辑(比如支付),在出现Bug的时候需要及时修复,等不及再发一个包到市场上等着用户手动安装更新,所以需要引入一套HotFix的机制。

原理

现在流行的HotFix方案的原理基本是一致的,主要就是依靠Android的ClassLoader机制。Android中加载类一般用两种:

PathClassLoader

Provides a simple {@link ClassLoader} implementation that operates
on a list of files and directories in the local file system, but
does not attempt to load classes from the network. Android uses
this class for its system class loader and for its application
class loader(s).

DexClassLoader.

A class loader that loads classes from {@code .jar} and
{@code .apk} files containing a {@code classes.dex} entry.
This can be used to execute code not installed as part of an application.

这两个类都继承自BaseDexClassLoader,再看这个父类的源码:

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

public class BaseDexClassLoader extends ClassLoader {

private final String originalPath;

private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent)
{

super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
}

再看DexPathListfindClass方法:

1
2
3
4
5
6
7
8
9
10
11
12
13

public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}

再看DexFile的相关源码:

1
2
3
4
5

public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);

可以看到BaseDexClassLoader类里有一个pathList对象,这个对象其实就是一个包含多个DexFile的有序集合,加载类的时候其实就是按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。所谓的HotFix其实就是在这个过程中,把修改后的类文件打包一个dex文件,插入到pathList的第一个位置,在这个过程中如果出现了重复的类,最终会使用第一个找到的类,所以这时候目标类就已经被替换掉了。

存在的问题

CLASS_ISPREVERIFIED

如果不加处理使用以上的方法,那么在加载补丁的时候首先会出现这个异常:

1
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation

因为在DexPrepare.cpp将dex转化成odex的过程中,会在DexVerify.cpp进行校验,验证如果static方法、private方法、构造函数等,直接引用到的类(第一层级关系,不会进行递归搜索)和当前类是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志。所以要想办法阻止相关类被打上CLASS_ISPREVERIFIED标志。
最直接的方法就是改变代码结构,使要热修复的类引用其他dex中的类,当然这样灵活性就降低了。因为bug无处不在,出现bug的时候我们是不知道要修复哪些类的。
另外就是用工具javassist或者ASM进行动态代码注入。

Android 6.0以上的权限

因为在6.0以后一些敏感权限是需要动态申请的,热修复因为都需要下载或者生成一个patch补丁包放在本地,所以是需要在这时候申请读写SD卡这一组权限的。

ProGuard

在给目标类进行动态代码注入的时候,是通过反射根据包名和类名获取Class的,如果这时候代码已经被混淆的话,包名和类名都发生了变化,这个获取过程出错的几率比较大。

HotFix

我使用的框架是GitHub上dodola的HotFix这个项目,同类的项目有Nuwa,在Android端进行热修复的实现思路都是类似的。这一部分看项目源码就可以。

如何解决CLASS_ISPREVERIFIED的问题?
文档里说的比较清楚了:

解决的方法就是在类中引用一个其他dex中的类,但是源码方式的引用会将引用的类打入同一个dex中,所以我们需要找到一种既能编译通过并且将两个互相引用的类分离到不同的dex中,于是就有了这个动态的代码植入方式。

不同解决方案的比较

HotFix

  • 代码虽然写的比较死,但是容易理解,可定制性强
  • 在打包的时候就需要给目标类动态注入代码,相当于打一个TAG,避免出现CLASS_ISPREVERIFIED的问题。如果给项目所有的类都这么做的话工作量比较大。而且补丁也需要自己制作。

Nuwa

  • 自动化做的比较好,制作补丁有专门的gradle插件。
  • 坏消息是作者似乎不准备再维护这个项目了,所以gradle版本升级之后它的gradle插件就不能用了。GitHub上一些人提了issue作者也不准备解决了,坑比较多。

DroidFix

  • 原理和HotFix一致,包括对CLASS_ISPREVERIFIED的处理方法。
  • 对接口的热修复支持不好。
  • 这个项目的源码我没仔细看,有待研究。

RocooFix

  • HotFix的作者的另一个项目,支持重启热修复和实时热修复。实时热修复用的是另一个开源项目Legend。我们暂且只用重启热修复这部分代码。
  • 按照项目描述的是无需关注混淆问题,无需手动制作补丁,而且作者在及时填坑。

补丁版本管理

阿里的AndFix现在给出的方案是每个补丁对应的文件中有个Create-Time的字段,根据这个字段按照顺序打补丁。

安全性

目前的方案,补丁是下载下来放在本地一个目录下的,用户是可以手动替换掉补丁的。可以通过对补丁包的MD5进行验证避免补丁被恶意替换。

总结

  • 我们目前的项目实现热修复的话,用今天讲的这种原理理论上就可以了。
  • 因为现在项目代码不混淆,所以不需要考虑这方面的问题。但是项目现在使用第三方加固的话,尚不清楚加固的原理,根据一些用过热修复的人说的,一些第三方加固会将Android原有的PathClassLoader替换为他们自己的loader,这样的话就需要做单独处理了。
  • CLASS_ISPREVERIFIED用目前通用的动态注入字节码的方式就可以处理。
  • 本地制作补丁包实现自动化写一个脚本或者gradle插件。
  • 多个补丁的版本管理和安全问题也需要后端的配合。
  • 紧急情况可以使用这个方案,一次加载多个dex补丁必然导致性能的问题。
  • 其他的问题只能是在项目里真正使用之后再寻求解决方案了。

参考

延伸

Android Transitions

Transition

从Android 4.4 KitKat开始,有了Transitions API,在兼容包中也有Transitions API,因此可以在几乎所有的安卓设备上使用。

Transition API中,出现了诸如场景(Scene)、场景过渡(Transition)这样的概念,场景本质上是对ViewGroup的一层封装,描述了自己以及所有View对象的的状态。而过渡(Transition)则是一种这样的机制:读取不同场景之间View属性的变化,从而产生让这种变化看起来平滑的动画。

当一个场景改变的时候,transition主要负责:

(1)捕捉每个View在开始场景和结束场景时的状态。

(2)根据两个场景(开始和结束)之间的区别创建一个Animator。

Transition API提供的特色有:

组级别的动画:可以将整个View树作为整体动画,你只需指定ViewGroup,它的各个元素就会自动应用动画
内置动画:内置简单的动画,比如dissolution,darkening,resizing,movement等等。
对资源文件的支持:你可以不必写代码,在资源文件中创建动画。
回调:提供掌控动画过程的所有必要的回调方法。

缺点:

在那些不在UI线程中工作的View,比如SurfaceView或者TextureView中使用的时候不流畅。
AdapterView,比如ListView,当你需要针对列表中某个单独的元素使用动画的时候。
偶尔会在resize TextView的时候会出现同步的问题:在另一个对象的改变结束的之前,字体会提前出现在下一个场景中。

Activity的切换

首先在setContentView之前调用

1
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);

或者在Activity对应的主题里设置

1
<item name="android:windowContentTransitions">true</item>

设置元素切换动画的方式

1
2
<item name="android:windowSharedElementEnterTransition">@android:transition/move</item>
<item name="android:windowSharedElementExitTransition">@android:transition/move</item>

销毁Activity

1
finishAfterTransition();

设置非共享元素的动画

  • (1)setExitTransition() - 当A start B时,使A中的View退出场景的transition

  • (2)setEnterTransition() - 当A start B时,使B中的View进入场景的transition

  • (3)setReturnTransition() - 当B 返回 A时,使B中的View退出场景的transition

  • (4)setReenterTransition() - 当B 返回 A时,使A中的View进入场景的transition

Content Transition

  • Activity A 调用startActivity().

    1.framework遍历A的View树,确定当A的exit transition运行时哪些view会退出场景(即哪些view是transitioning view)。

    2.A的exit transition捕获A中transitioning view的开始状态。

    3.framework将A中所有的transitioning view设置为INVISIBLE。

    4.A的exit transition捕获到A中transitioning view的结束状态。

    5.A的exit transition比较每个transitioning view的开始和结束状态,然后根据前后状态的区别创建一个Animator。Animator开始运行,同时transitioning view退出场景。

  • Activity B启动.

    1.framework遍历B的View树,确定当B的enter transition运行时哪些view会进入场景,transitioning view会被初始化为INVISIBLE。

    2.B的enter transition捕获B中transitioning view的开始状态。

    3.framework将B中所有的transitioning view设置为VISIBLE。

    4.B的enter transition捕获到B中transitioning view的结束状态。

    5.B的enter transition比较每个transitioning view的开始和结束状态,然后根据前后状态的区别创建一个Animator。Animator开始运行,同时transitioning view进入场景。

Fragment的切换

  • Content的exit, enter, reenter, 和return transition需要调用fragment的相应方法来设置,或者通过fragment的xml属性来设置。

  • 共享元素的enter和return transition也n需要调用fragment的相应方法来设置,或者通过fragment的xml属性来设置。

  • 在Fragment commit之前,共享元素需要通过调用addSharedElement(View, String) 方法来成为FragmentTransaction的一部分。

自定义Content Transition

可以借鉴Android中已有的实现Fade,Slide,Explode

Reveal

1
ViewAnimationUtils.createCircularReveal(View view,int centerX,  int centerY, float startRadius, float endRadius)

Suggestion: use tools:overrideLibrary=”” to force usage

Android开发在build的时候有时会报如下错误:

1
Error:Execution failed for task ‘:app:processProductDebugManifest’.
Manifest merger failed : uses-sdk : minSdkVersion 14 cannot be smaller than version 18 declared in library......
Suggestion: use tools:overrideLibrary=”xx.xxx.xx” to force usage

错误原因是当前的Build Variant(即错误提示中的ProductDebug)的minSdkVersion小于它所依赖的包的minSdkVersion,所以报了这个错。解决方案就像Suggestion告诉的,在对应的AndroidManifest.xml文件里添加一个<uses-sdk tools:overrideLibrary="xxx.xxx.xxx"/>的标签,中间填上报错的依赖库的包名,如果有多个这样的依赖库的话就填多个包名,用逗号分开。

前面是遇到这个问题常用的解决方法,但是在有些情况下得注意这个标签是否填对了地方。比如在项目有多个variant时,会针对不同的variant有不同的包,不同的包的依赖也是不同的,那么就应该针对某个包写一个单独的AndroidManifest.xml来解决前面那种问题。一般情况下这样解决了之后就不会有问题了。

但是还有特殊情况。比如针对不同的flavor做了两个针对的test包,androidTestFlavor1androidTestFlavor2,并且配置了minSdkVersion有冲突的依赖,而androidTest包没有有效代码所以被删掉了。这时候即使按照前面的解决方案给androidTestFlavor1androidTestFlavor2对应的AndroidManifest.xml配置了tools:overrideLibrary,在Build的时候还是会报如下错:

1
Error:Execution failed for task ‘:app:processProductDebugAndroidTestManifest’.

也就是说明明原有的androidTest包里已经被删掉了,但是gradle还是会跑其对应的taskapp:processProductDebugAndroidTestManifest,这时候我们只需要重新把androidTest包找回来,里面什么都没有只放一个AndroidManifest.xml,并配置对应的tools:overrideLibrary,就可以Build成功了。

然后对比了几个不同的项目Build的message,无论有没有androidTest,都会跑对应当前variant的这个task,只要存在前面的minSdkVersion的冲突问题并且没有在androidTest对应的AndroidManifest.xml里解决,都会报错。

研究了半天对这个问题并没有找到比较好的解决方案,能解决的方法一种是如果存在以上问题就保留androidTest这个包和对应的AndroidManifest.xml;另一种是如果这个flavor就是专门为测试用的话,可以在build.gradle文件里给对应的flavor设置不同于其他flavor的minSdkVersion,如下:

productFlavors {
     flavor1 {
         ......
       }
     flavor2 {
         ......
     }
     testflavor {
         minSdkVersion xx
     }
  }

如果项目里遇到类似的问题基本就是这个情况。这两个方法只能算是妥协,但是如果项目的build是默认必须跑这个task的话应该也没有更好的办法能解决了。

一年总结

这篇总结从过年拖到生日,再拖到现在也是醉了。本来是总结2015年的,现在正好是我下定决心回到北京整整一年,想到哪写到哪,干脆就总结一下这一年吧。

流水账

先说说这一年大概都干了什么。
去年三月份可以说是经过很长时间很复杂的思想斗争,终于决定还是回到北京吧,不能再在那个摇摇欲坠的行业和国企待下去了(虽然看起来中国石油永远都不会倒),做了决定之后反而是轻松了许多,反正是背水一战了,然后就是开始业余时间把代码拾起来开始干,为了大Google的信仰选择了Android,也是因为自己对这个敢说比较了解。学习的过程说实话没觉得多累,每天下班就是码代码,到五月底的时候竟然感觉自己可以提前走了,再加上当时实在待不下去了,于是任性一次选择了裸辞。
六七月份都在办辞职的事情,这两个月反而是遇到了许多麻烦。首先是外界的压力,毕竟当时进了设计院费了很大劲,而且中国石油的牌子摆在那儿,可以说是没有人支持我离开那里,所有事情纯粹靠我自己的判断。另外一点是辞职手续比我想象的慢好多,算是真正见识了国企的办事效率,当然也让我更加坚定了离开的想法(辞职的所有手续时隔半年才全部办妥)。麻烦归麻烦,好在终于顺利离开了。
七月底,回到熟悉的帝都,当时竟然没什么胆怯的,大概是因为是真正放下包袱了吧,接受了所有的沉没成本,从头再来!因为心态还不错,找工作也挺顺利的,三天时间把工作定了,当然是略微有点草率了,但是在当时的情况下基本算是比较好的结果了,所以不后悔。

关于我的决定

为什么从国企出来?理想的说,那个环境确实不适合我,我应该是最不应该去国企的那种人,可是却被开了个不大不小的玩笑;务实的说,在国企闲着的时候我不断分析经济和行业形势,最终做出了三个判断,一是顺势而为,二是国内能源将死,三是要顺势恐怕还是得第三产业。再加上自己对做程序觊觎已久,做出这个决定其实并不难。
截至目前,我的判断和决定应该都没有大的错误,不要脸的说,看了那么多书,走了那么多弯路,似乎终于开始起作用了。

关于三观

回到北京之后这不到一年,我的三观基本还是稳固的,毕竟之前受到了很多次剧烈的冲击,短期内不会有大的改变了。
先说最俗的,关于钱。这一年除了辞职转行,同时也经历了大牛市和股灾,手上配置着基金和各种P2P,关于理财的东西也看了不少,而且也坚持记账快三年了,习惯还是不错的。对钱的态度还是不变:不断追求有可能的财务自由。无论何时投资理财都是稳字当头。总的来说在钱上对自己还是有自信的。
再说不俗的,关于感情,主要是爱情。去年九月份机缘巧合认识了现在的女朋友,感觉非常对自己的胃口,于是赌了一把,同样是到目前为止应该是赌对了,两个人三观很合,没有大的问题,理论上来说未来也不会有太大的分歧,我自己也少有的在自己的规划中开始加上其他人的名字,大概是真的遇到对的人了吧。

关于困难

鸡汤好喝,奈何现实没那么好,所以困难当然是有的。辞职,找工作,租房,跳槽,这一年处理的事情也是够多的。很多遇到的困难当时其实都没觉得是困难,北京比我难的人多了去了,印象比较深刻的事情有两个:
一个是辞职的时候,拿着个傻逼单子得让全单位的部门领导签字,签到最后一个的时候,那个领导说他这几年没见过正式工辞职的,说了句“年轻人不要不知好歹”。我当时并没有大的反应,但是心里只说了一句:我以后要打你们所有人的脸,我要比你们都强。同样的事情还挺多的,不得不说一句,环境造就人,那个环境里的人就是井底之蛙,他们对我辞职各种各样的反应我也都能理解了。
第二个是找工作那几天,来北京房子没租好,住在北航一个博士宿舍,连续几晚上被北航的蚊子咬得没睡着,白天迷迷糊糊还得跑遍北京城面试,当时还是挺感慨的,不过这事对我来说没什么大不了的。
困难没那么多,有时候看见好多北漂矫情得说自己怎么怎么样了还是挺无语的,吃不上饭没衣服穿那才叫困难,看不到希望那才叫困难,这样的人在中国到处都是,我们这点困难不算什么。

关于收获

经历即财富,虽然我不怎么认同这句鸡汤,但是这一年确实长进很大,收获很多,各方面。最大的长进就是埋藏心底的野心终于找回来了,命运开始掌握在自己手里,有了这一点,其他的收获有多少其实也无所谓了。

关于自己的弱点

这个是要说的,不能让美好掩盖了隐患。
在技术上,还是缺乏探索和追求完美的精神,虽然在这上面比以前好了不少,但是还是不够,尤其是跟周围优秀的同事相比。这个弱点不改的话以后可能酿成大错,不仅是对待工作,对待人生也应该追求极致。
感情上缺乏耐心,这个也是个问题,大概是因为自己做了一些事情后有些自负了,不能够正确面对他人了,生活是生活,事业是事业,还是应该用平常心对待的。
拖延症······这个只能是继续改进了,比以前好了点,但是还差得远。

接下来一年

说了这么多过去的事情也是挺感慨的,终于该展望一下未来了。
首先是自己的工作。软件开发的世界瞬息万变,需要不断学习和抬头理清方向,当然这些是我当时选择这个行业的原因之一,我愿意拥抱变化。技术上方向基本不变,自己的目标是做一个优秀的移动开发工程师而不仅仅是Android Developer,所以除了Android的核心知识,JavaScript和React Native是这一年的重点,希望年底能够有一定的成果吧。
另外就是在北京的生活了。这个暂时没什么可说的,还需要等两年才能有定论。

总结

本来以为自己有好多话要说,写出来其实也没那么多,大概是写代码以后自己也开始追求简单明了了吧,好事儿。

最后谈点理想化的。曾经有那么一段时间,被现实锤得不行的我也快接受了自己也许同样是个平庸的人。但是现在我不会在那么想了,我认识的周围所有人只有我迈出了这一步,我做的每个决定越来越成熟,我的野心比别人更强,我比同样处境下的大多数人都做的更好,我在不断进步,也在不断强大。

我永远都不会接受自己是个普通人。

以我刺鸡汤结尾

鸡汤

RelativeLayout的gravity和ignoreGravity

Android开发中使用最多的ViewGroup应该就是RelativeLayout了,虽然众所周知它在Measure的时候会测量两次子View,耗时会略微多一点,但是毕竟比其他布局更加灵活,熟练使用的话可以大大减少布局的嵌套,避免IDE提示你TooDeepLayout的时候才反应过来已经嵌套了太多的ViewGroup.

使用RelativeLayout的时候常用的应该就是给子View设置一些属性,例如子View相对于父Layout的layout_centerInParent,layout_centerHorizontallayout_alignParentTop,layout_alignParentLeft等等这类属性,以及子View相对于其他子View的layout_above,layout_toLeftOf和类似的属性,常用RelativeLayout的开发者应该对这些属性都很熟悉了,能都做到熟练使用的话一般的布局应该都不成问题了.所以就不在这里废话了.今天总结的主要是RelativeLayout一个不太常见的属性–gravity(或者是我和周围的同事不太常用的).

gravity属性

有一天你的PM让你实现一个界面,大概是这样:

gravity示例

要求是中间这几个button占用的空间不论button怎么变化始终都在父布局的中间.

看起来并不难,父布局用一个RelativeLayout,写一个width和height都为wrap_content的RelativeLayout设置属性layout_centerInParent为true.布局中second button放在first button的右边,third button放在first button下面。这样不论三个button大小怎么变,他们所占的空间始终是子布局的RelativeLayout的大小,所以始终在父布局的正中间.如果觉得性能不够好的话也可以把父布局设为FrameLayout,然后子RelativeLayout设置的属性为layout_gravity="center",也可以达到同样的效果.
代码如下:

main.xml

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">

<Button
android:text="first button"
android:id="@+id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

<Button
android:text="second button"
android:id="@+id/btn_2"
android:layout_toRightOf="@id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

<Button
android:text="third button"
android:id="@+id/btn_3"
android:layout_below="@id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

</RelativeLayout>
</RelativeLayout>

虽然是两层嵌套,但是达到了我们的效果,这也算是RelativeLayout的强大之处.不过这个界面并不复杂还好,如果说一个界面中存在很多这种情况,积少成多,都用这种方法解决可能就有点性能上的问题了.

这时候就是gravity出场的时候了,我们把xml代码修改一下,去掉子RelativeLayout,给父布局加上gravity="center"的属性,代码如下:

main.xml

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">


<Button
android:text="first button"
android:id="@+id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>


<Button
android:text="second button"
android:id="@+id/btn_2"
android:layout_toRightOf="@id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>


<Button
android:text="third button"
android:id="@+id/btn_3"
android:layout_below="@id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>


</RelativeLayout>

一层ViewGroup,就达到了同样的效果~~

这样使用的优势是什么呢?比如说如果在保持几个子View之间相对位置不变的情况下,要把这个整体放在父布局底部居中,如图:

second

平时遇到这样的情况会怎么做呢?仍然是两层嵌套?或者是先定好third button的位置,再让其他两个View依赖于third button?似乎都不是很好的解决方案.而使用gravity的话只需要设定一个gravity="bottom|center_horizontal",效果就达到了~

然而需求永远大于实现,这时候你的PM又说了,除了这个,还有一个单独的控件,要放在界面上不依赖其他控件,但依赖于父布局的位置.但是这时候我们已经给父布局设定了一个gravity了,看起来所有的子View都会被父布局的属性所影响,怎么解决呢?说实话我看了半天源码也不是很明白Google工程师是怎么想的,但是呢他们就是提供了这么一个属性ignoreGravity,猜也能猜出来,它能让gravity失效,还是用代码说话吧~

比如布局是这样的:

second

代码如下:

main.xml

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:ignoreGravity="@+id/btn_4"
>

<Button
android:text="first button"
android:id="@+id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

<Button
android:text="second button"
android:id="@+id/btn_2"
android:layout_toRightOf="@id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

<Button
android:text="third button"
android:id="@+id/btn_3"
android:layout_below="@id/btn_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

<Button
android:text="forth button"
android:id="@id/btn_4"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

</RelativeLayout>

这时候我们可以看到forth button已经不再受gravity属性的影响了,这时候就可以随意给forth button设置相对父布局的一些属性了.

但是!还没有完,为啥第三张图中的三个button我都放在左下角,也就是gravity="bottom",而不是继续用gravity="bottom|center_horizontal"呢,因为这时候突然不生效了,也就是发生了一些意外的布局冲突,没有达到我们想要的效果.源码之前了无秘密,说了这么多还是得靠源码解决一些问题呀,知其然也要知其所以然,还是研究下RelativeLayout的源码知道其基本的原理比较好.看看ignoreGravity相关的:

RelativeLayout.java

1
2
3
if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) {
ignore = findViewById(mIgnoreGravity);
}

源码中,关于ignoreGravity比较关键的就这两句,写的很明白:只有在gravity不为center_horizontal也不为center_vertical的时候,这个ignoreGravity才能切实发挥作用,至于具体的原因,就得继续深扒RelativeLayout的源码了,本人对RelativeLayout的研究还是不够,所以不敢自己乱加猜测了.

当然了,如果子View只是互相依赖的话这样用就没问题,但是如果给子View使用了相对于父布局的一些属性的话可能会有一些冲突,这个就看大家使用的时候怎么取舍了,毕竟是一个不那么常用的属性.不过这个gravity也足够帮我们解决很多问题了吧.