AndroidHandler机制总结

1.What?

Handler机制是Android中的线程间通信方式。在Android中由于主线程是不安全的,因此我们所有涉及到UI的操作必须在主线程中执行。这就导致线程间频繁的通讯,比如我们常常需要从网络上获取数据再加载到UI上显示,这样一个过程就必然涉及到线程间的通讯:主线程中创建新线程->在新线程中发起网络请求->获得网络数据->后台处理后回调通知UI线程(线程间通信)并把数据发送给UI线程->UI线程中调用UI相关方法,重新加载数据到UI上。

在以上这个过程中,没有一套良好的线程间通讯方式是不行的,Android为我们提供了Handler来解决这个问题,本篇就来总结回顾Handler相关的内容。

2.How?

先来看一看Handler的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LooperThread extends Thread {  
public Handler mHandler;

public void run() {
Looper.prepare();

mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};

Looper.loop();
}
}

可以看到,一个线程中持有一个Handler,在run方法中首先要初始化Looper,调用它的prepare()方法,并在下面开始Looper的循环,为什么要使用Looper、Looper和Handler有什么关系,这在接下来讲。

3.Why?

接下来我们分析Handler内部的实现机制。

在开始之前,首先让我们搞清楚Handler涉及到哪些类。

Handler类图

Handler、Looper、MessageQueue、Message、Thread,这就是Handler机制中的主要类。

Thread位于最底层,它持有一个Looper对象和一个MessageQueue对象,Looper以及MessageQueue为上层的Handler底层实现,对于一般开发,我们仅仅需要使用Handler就已经足够。

这几个玩意都是干什么的?

让我们先来看看每个类的注释:

Handler

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
/**
* A Handler allows you to send and process {@link Message} and Runnable
* objects associated with a thread's {@link MessageQueue}. Each Handler
* instance is associated with a single thread and that thread's message
* queue. When you create a new Handler, it is bound to the thread /
* message queue of the thread that is creating it -- from that point on,
* it will deliver messages and runnables to that message queue and execute
* them as they come out of the message queue.
*
* <p>There are two main uses for a Handler: (1) to schedule messages and
* runnables to be executed as some point in the future; and (2) to enqueue
* an action to be performed on a different thread than your own.
*
* <p>Scheduling messages is accomplished with the
* {@link #post}, {@link #postAtTime(Runnable, long)},
* {@link #postDelayed}, {@link #sendEmptyMessage},
* {@link #sendMessage}, {@link #sendMessageAtTime}, and
* {@link #sendMessageDelayed} methods. The <em>post</em> versions allow
* you to enqueue Runnable objects to be called by the message queue when
* they are received; the <em>sendMessage</em> versions allow you to enqueue
* a {@link Message} object containing a bundle of data that will be
* processed by the Handler's {@link #handleMessage} method (requiring that
* you implement a subclass of Handler).
*
* <p>When posting or sending to a Handler, you can either
* allow the item to be processed as soon as the message queue is ready
* to do so, or specify a delay before it gets processed or absolute time for
* it to be processed. The latter two allow you to implement timeouts,
* ticks, and other timing-based behavior.
*
* <p>When a
* process is created for your application, its main thread is dedicated to
* running a message queue that takes care of managing the top-level
* application objects (activities, broadcast receivers, etc) and any windows
* they create. You can create your own threads, and communicate back with
* the main application thread through a Handler. This is done by calling
* the same <em>post</em> or <em>sendMessage</em> methods as before, but from
* your new thread. The given Runnable or Message will then be scheduled
* in the Handler's message queue and processed when appropriate.
*/

让我先来翻译一遍:

Handler允许你发送和处理Message与在MessageQueue中实现了Runnable接口的对象(以下简称Runnable对象)。

每一个Handler实例与一个Thread和Thread内包含的message queue相关联。

当你创建一个新的Handler实例,它会被绑定到它所在的Thread以及其message queue。从创建以后,Handler实例会把message和Runnable对象发送到message queue,并且在它们从message queue中出队后执行它们。

Handler有两个主要的功能:(1)调度message和Runnable对象使它们在未来被执行。(强调时间,如延时发送信息)(2)将一个动作送入队列,使它能够在其它线程被执行。(强调线程)

Handler的消息调度功能在post方法、postAtTime方法、postDelayed方法、sendEmptyMessage方法、sendMessage方法、sendMessageAtTime方法、sendMessageDelayed方法中完成。post开头的一类方法中调度的Runable对象会在message queue准备好后马上执行,sendMessage开头的一类方法允许你传递一个带有bundle(额外数据)的message,这个bundle在Handler的handleMessage中被回调处理,因此我们需要自己重写这个handleMessage。

当发送给Handler,你可以决定当message queue准备好就马上执行,或者是延迟一段时间再执行。后缀为Delayed的方法就是这样使用的。

当你的app中一个进程创建时,这个进程中的主线程会拥有一个专用的message queue,主线程关注管理顶层对象(activity、broadcast receiver等)和它们创建的所有windows。(也就是说主线程默认有一个message queue,另一层意思就是其它线程默认是没有message queue的,需要我们手动创建。)你可以创建自己的线程,并通过Handler和主线程交互,通过使用之前提到的post、sendMessage等方法来实现。发送给主线程的message和Runnable对象将会在合适的时候被处理。

通过这一段注释,我们对Handler的作用已经有了大致的印象。Handler把Message或者Runnable对象发送到一个线程的Message Queue中,Message可以携带数据、Runnable对象可以自己决定执行内容,我们还可以自己决定是即刻调用还是延迟调用,Message Queue中含有大量的消息,它们在合适时候就会出队列并被执行。

接下来是MessageQueue

1
2
3
4
5
6
7
8
9
/**
* Low-level class holding the list of messages to be dispatched by a
* {@link Looper}. Messages are not added directly to a MessageQueue,
* but rather through {@link Handler} objects associated with the Looper.
*
* <p>You can retrieve the MessageQueue for the current thread with
* {@link Looper#myQueue() Looper.myQueue()}.
*/
public final class MessageQueue {

MessageQueue是一个持有将被Looper分发的message的list的底层类。Message不能被直接添加到MessageQueue中,而是通过与MessageQueue对应的Looper所绑定的Handler。

你可以通过Looper.myQueue方法来得到当前线程的MessageQueue。

最后是Looper

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
/**
* Class used to run a message loop for a thread. Threads by default do
* not have a message loop associated with them; to create one, call
* {@link #prepare} in the thread that is to run the loop, and then
* {@link #loop} to have it process messages until the loop is stopped.
*
* <p>Most interaction with a message loop is through the
* {@link Handler} class.
*
* <p>This is a typical example of the implementation of a Looper thread,
* using the separation of {@link #prepare} and {@link #loop} to create an
* initial Handler to communicate with the Looper.
*
* <pre>
* class LooperThread extends Thread {
* public Handler mHandler;
*
* public void run() {
* Looper.prepare();
*
* mHandler = new Handler() {
* public void handleMessage(Message msg) {
* // process incoming messages here
* }
* };
*
* Looper.loop();
* }
* }</pre>
*/
public final class Looper {

这个类用于一个线程运行message的循环。一个线程在默认情况下没有与它们绑定,所以在要循环的线程中使用Looper.prepare方法,并使用Looper.loop方法来处理message直到循环结束。

Looper和message loop的相互作用大部分通过Handler来完成。

以上几个类的基本注释都翻译完了,看完这几类的注释,我觉得其实已经比较清楚了。

让我来简单的总结一下

Handler机制要实现的无非就是线程间的通信,那么以什么样的方式实现好呢?

很简单,每一个线程持有一个MessageQueue,在线程内使用一个Looper对象来让MessageQueue里的Message流动起来。Looper和MessageQueue本身都不能跨越线程,Handler就是一个联系不同线程中的MessageQueue和Looper的中介。Handler本身可以说是一个跨线程的对象,为什么这样说呢?因为Handler可以发送和接受处理Message,而且发送和接受所在的线程可以不同!Handler这个对象在另外一个线程使用时,发送Message到创建它的线程中的MessageQueue中,然后经由Looper对MessageQueue的处理,在合适的时机取出MessageQueue,传递给Handler处理。

一句话:多个线程使用Handler,任何一个线程都可以使用Handler发送Message到创建它的线程,这个Message也只能被创建Handler的线程处理。

Handler机制的基本原理就是这样。

下面我们来看一个简单的Demo。

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
package com.zuliangwang.learnhandler;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
Log.d("TestActivity","receive");
}
};



new TestThread().start();
new TestThread2(handler).start();
}
}

我在一个Activity(主线程)里创建了两个线程并启动它们,同时我在主线程里创建了一个Handler,因此这个Handler依附于主线程,那么我在另外的线程里发送Message,处理它的也应该是主线程,OK,下面让我们来验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.zuliangwang.learnhandler;

import android.util.Log;

/**
* Created by zuliangwang on 16/11/22.
*/


public class TestThread extends Thread{
@Override
public void run() {
Log.d("TestThread","begin");
Log.d("TestThread","end");
}
}

这个TestThread是一个空的Thread,我创建这个Thread是为了验证Looper的作用!下面我会继续说。

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
package com.zuliangwang.learnhandler;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

/**
* Created by zuliangwang on 16/11/22.
*/


public class TestThread2 extends Thread{

public TestThread2(Handler handler) {
mHandler = handler;
}

Handler mHandler;

@Override
public void run() {
Log.d("TestThread2","id = "+getId());
Log.d("TestThread2","begin");
Looper.prepare();
Log.d("TestThread2","loop");


mHandler.post(new Runnable() {
@Override
public void run() {
Log.d("TestThread2","run"+Thread.currentThread().getId());;
}
});
mHandler.sendEmptyMessage(3);

//死循环方法
Looper.loop();
Log.d("TestThread2","end");
}
}

可以看到,我在TestThread2中使用主线程创建的Handler发送了一个Runnable对象和一个空的Message。

result输出的结果如图,我来一点点分析。

1.TestThread中输出了begin和end,证明run方法跑完了。

2.TestThread2中输出了TestThread2的ID为135,而下面run的却是1,看到了吗?两者线程ID不同!这说明什么,handler调用post方法发送的Runnable对象在另外一个线程中运行,而ID为1的线程是谁?我不说大家都应该猜得到,就是主线程!也就是说这个Handler发送的Runnable最后进入了主线程的MessageQueue并在主线程运行,这和之前说明的原理是吻合的。

3.然后再来看看TestThread输出的begin和loop,可以看到,它最后没有输出end,也就是说run方法没有正常运行结束,这是为什么呢?因为Looper!

4.最后可以看到在Activity中收到了TestThread2发送的Message。

以上和我们之前的分析完全吻合,Handler的原理到这里就搞清楚了。

下面让我们看看一直说的Looper是怎么回事,为什么TestThread2的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
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

final long traceTag = me.mTraceTag;
if (traceTag != 0) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
try {
msg.target.dispatchMessage(msg);
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();
}
}

看到了这行吗? for (;;) Looper内的loop方法是一个死循环,这是为什么呢?想一想一个正常的线程应该是什么样的?从被创建到运行完run方法里面的代码,然后正常退出,那么我怎么样在线程中构建出UI界面这种永不结束的效果呢?为什么Android的UI可以在主线程中一直存在?很简单,因为主线程默认调用了Looper的loop方法,永不结束的从MessageQueue里取出Message处理,而我们刷新UI也就是向这个MessageQueue发送了一条Message。

到这里Handler机制就分析完了,具体的代码细节也不说了,大概的流程和逻辑现在应该很清楚了。

Java线程锁总结

今天复习Java的多线程基本知识,总结一下线程安全方面的内容。
首先来看一段程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Created by zuliangwang on 16/11/20.
*/

public class TicketDemo {
public static void main(String args[]){
Ticket ticket = new Ticket();

Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
Thread thread4 = new Thread(ticket);

thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Ticket implements Runnable {
private int num = 100;

public Ticket() {
}

public void run() {
while(true) {
if(this.num > 0) {
try {
Thread.sleep(10L);
System.out.println(Thread.currentThread().getName() + ".....sale..." + this.num--);
} catch (InterruptedException var5) {
Thread.currentThread().interrupt();
}
}
}
}
}

这是一个模拟卖票的多线程程序,是学习多线程的一个经典问题。这个时候没有对多线程做任何安全保障,因为四个线程同时访问一个Ticket对象,所以会发生多线程的数据异常。
先来看看最后的结果:
result1

可以看到,线程0和1访问Ticket对象,出现了两次为100的情况,这显然是有问题的。

为什么会出现这种情况呢?因为线程0和线程1同时访问同一个Ticket对象,当线程0首先执行后还没有执行到num–时线程1就已经开始执行了run内代码,这时候由于num变量的值没有更新,所以线程0和线程1同时拥有一份值为100的num的拷贝,这样就导致后面的逻辑全部出问题。

这种情况就是典型的线程安全处理不当导致的问题。

那么如何解决这个问题呢?

Java中针对线程安全有以下几种解决办法:

(1)Synchronized关键字。

Java中的每个对象都内置了一个锁。当一个方法前加上Synchronized关键字后它在同一时间就只能被一个线程执行。

例如:

1
2
3
Synchronized function(){
a++;
}
1
2
3
4
5
lock();
function(){
a++;
}

unlock();

前者将会被编译器编译成类似后者的样子。

这样上锁后对象将保护这个方法,当上锁后解锁前任何对象都不能再执行这个方法。

同时,Synchronized也可以对代码块上锁。

用法如下:

1
2
3
4
Object obj = new Object();
synchronized(obj){
//需要同步的代码
}

这里的对象可以是任何对象,因为我们刚才提到了synchronized实际是使用对象的内部锁,这个其实就是需要一个锁。

(2)volatile(错误方法)

在以前我一直以为volatile是对一个变量值上锁,今天自己动手试了一下,居然不对!果然还是实践出真知。

我们先来看看结果:

我首先修改private volatile num=100;把num变量设置为volatile变量。

result2

这是结果,可以很明显的看到两个88。这是什么原因呢?我查了一些资料:

volatile Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量 的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示JVM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。 由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟C中的一样 禁止编译器进行优化.

注意:如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对longdouble的简单操作之外,volatile并不能提供原子性。 所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生。

volatile并不是真正的锁住了一个变量,它强制要求线程使用对象的变量时不进行拷贝,而是直接修改这个对象的变量值,但是因为修改变量值这个操作本身不是一个原子操作,所以仍然会出现问题!

实践出真知。

(3)使用Lock。

Java在早期加锁操作只有synchronized关键字,但是后来又加入了使用Lock对象方法。

Lock是一个接口,它的实现类有以下几种:

hier

多的不说了,我现在也只了解一个实现类RentrantLoock。

简单看一下用法:

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
/**
* Created by zuliangwang on 16/11/20.
*/

public class Ticket implements Runnable {

private Lock ticketLock = new ReentrantLock();
private volatile int num = 100;
@Override
public void run() {

while (true){
ticketLock.lock();
if (num>0){
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+".....sale..."+num--);
}
catch (InterruptedException e){
Thread.currentThread().interrupt();
}
finally {
ticketLock.unlock();
}

}
}
}
}

修改了Ticket中的代码,对里面的一部分代码上锁。

Lock的原理和synchronized差不多,只不过因为我们可以手动上锁解锁,因此灵活度更高,需要注意的是合理使用,避免死锁。

基本内容就到这里,更深入的等以后碰上生产环境再说。

Android Studio Instant Run总结

Android Studio在2.0以后加入了一个很实用的功能:Instant Run
这个功能可以加速我们App的编译,在项目做大了以后,能省去很多时间。早期版本有很多Bug,但是现在已经好多了。今天大概总结一下Instant Run的几个类型。
Instant Run的原理无非就是增量编译,每次编译根据修改的代码决定去重新编译哪些文件,避免每次都从头到尾开始编译。
那么Instant Run的类型也就和编译哪些文件有关。
1.Hot Swap
这种方式是几种类型中最快的一种。使用这种方式编译,App甚至不用重新启动。当我们仅仅修改了方法内部的代码后就会以这种方式重新编译。
2.Warm Swap
这种方式的速度次于Hot Swap,编译完成后App不用重新安装和重新启动,但是需要启动当前的Activity,因此我们会看到屏幕闪了一下。当我们仅仅修改或者删除了一个现有的资源文件时就会以这种形式编译。
3.Cold Swap
这种方式的速度又次于Warm Swap,编译完成后不会生成新的apk,而仅仅只生成一个dex文件,因此不需要重新安装,但需要重新启动,因为需要重新加载这个dex文件,由于这种方式基于multidex机制实现,而Android在5.0以上才使用multidex,因此当目标机是5.0以下时这种方式就不能正常工作。
以下是使用Cold Swap的情况。

添加、删除或修改一个注解
添加、删除或修改一个字段
添加、删除或修改一个方法
添加一个类
修改一个类的继承结构
修改一个类的接口实现
修改一个类的static修饰符
涉及资源文件id的改动

4.Full Swap
这种方式就不用多说了,重新编译整个Apk,然后安装。当以上所有方式都不满足时就到了这一步。

注意,因为Instant Run本身的局限性,其中一些方式不会重新启动App,因此有些初始化代码可能不会被执行,这可能会引起很多问题。
我不清楚这个问题现在解决了没有。

以上总结来自 Android Studio新功能解析,你真的了解Instant Run吗?–郭霖
感谢原作者郭神!

Android namespace总结

在Android工程中以xml为布局文件方便快速进行布局的创建,namespace在xml中是不可或缺的一部分,但是这个玩意到底是什么意思?

1
2
3
4
5
6
7
8
9
10
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />

</RelativeLayout>

首先来看一个最简单的例子,在RekativeLayout中声明了namespace:android。
它的格式是这样的:
xmlns:xxx="http://schemas.android.com/apk/yyy"
xmlns(xml namespace)表示xml命名空间。后面的xxx是这个命名空间的一个代号,方便后面使用,在=后面是一个URI,它代表这个命名空间的具体位置。
xmlns:android="http://schemas.android.com/apk/res/android"
这句声明就表示我要使用URI为http://schemas.android.com/apk/res/android的命名空间,这里面http://schemas.android.com/是Android中固定的写法,表示Android命名空间,后面的/apk/res/android表示我要使用android默认的属性值。
通过namespace可以避免多个同名attribute的冲突。
常用的几个namespace有以下三个:
(1)xmlns:android="http://schemas.android.com/apk/res/android
这个不用多说,只要做过Android开发的都知道。
(2)xmlns:tools=”http://schemas.android.com/tools
可能大家对这个tools不太熟悉。这个namespace里面的东西在我们打包生成apk时不会被加入包中,正如它的名字所示,它仅仅是Android Studio提供给我们调试使用的tools,通过它设置一些attr可以很方便的在android studio中的ui预览界面调试。
(3)xmlns:app="http://schemas.android.com/apk/res-auto"
关于这个命名空间,我查了一些资料。

The app namespace is not specific to a library, but it is used for all attributes defined in your app, whether by your code or by libraries you import, effectively making a single global namespace for custom attributes - i.e., attributes not defined by the android system.
In this case, the appcompat-v7 library uses custom attributes mirroring the android: namespace ones to support prior versions of android (for example: android:showAsAction was only added in API11, but app:showAsAction (being provided as part of your application) works on all API levels your app does) - obviously using the android:showAsAction wouldn’t work on API levels where that attribute is not defined.

可以看到这个namespace是提供给我们方便的使用自定义属性而设置的,它不针对某一个lib,而是提供给所有自定义属性使用,任何来自lib或是我们自定义的属性都可以通过这个namespace快速引用,省去再创建一个namespace。

OpenGL入门学习

今天心血来潮突然想学习一下图形相关的内容,我听说过的这方面的东西就只有OpenGL和OpenCV了。恰好Android上的VR涉及到了OpenGL的一些内容,今天就开始记录我的OpenGL学习内容。
1.什么是OpenGL
OpenGL是一套开源的、跨平台的绘图API。在应用上,OpenGL常用于CAD、虚拟实境、科学可视化程序和电子游戏开发。它可以绘制2D及3D图形。需要注意的是OpenGL通常要搭配窗口库,如QT,因为OpenGL本身不处理窗口的创建和鼠标键盘输入,因此需要我们自己动手来处理这些问题。
2.OpenGL的版本变迁
OpenGL的第一个版本:
OpenGL 1.0
发布时间: 1992年1月
以下截取自某博客:
OpenGL 1.0~OpenGL 1.5是经典的固定管线时代;OpenGL 2.0~OpenGL 2.1是固定管线和可编程管线并存的时代;OpenGL 3.0~OpenGL 4.x开始是可编程管线崛起的时代。在出现可编程管线的那个时代。
就我现在接触到的的资料来看,现代使用的OpenGL版本至少在3.0以上。
OpenGL各个版本很混乱,在各个版本中会出现严重的断层。
本次我学习使用的OpenGL为3.2。
注:Android中使用的OpenGL为OpenGL ES,这是一套专门为嵌入式设备准备的API,它是OpenGL的子集。
官网传送门
它的使用和一般的OpenGL使用是有比较大的区别的。这些内容留置我以后整理。
3.环境配置
由于Mac的Xcode内置了OpenGL的库,我没有特意去配置开发环境,从其他地方找来的example可以直接编译并运行。
4.学习之前要先了解Shader是什么。
Wiki:

着色器(英语:shader)应用于计算机图形学领域,指一组供计算机图形资源在执行渲染任务时使用的指令,用于计算图像的颜色或明暗。但进来,它也能用于处理一些特殊效果,或者视频后处理。通俗地说,着色器告诉电脑如何用特有的一种方法去绘制物体。
程序员将着色器应用于图形处理器(GPU)的可编程流水线,来实现三维应用程序。这样的图形处理器有别于传统的固定流水线处理器,为GPU编程带来更高的灵活性和适应性。以前固有的流水线只能进行一些几何变换和像素灰度计算。现在可编程流水线还能处理所有像素、顶点、纹理的位置、色调、饱和度、明度、对比度并实时地绘制图像。着色器还能产生如模糊、高光、有体积光源、失焦、卡通渲染、色调分离、畸变、凹凸贴图、边缘检测、运动检测等效果

从程序开发者的角度来看:
Shader是使用GLSL(OpenGL Shading Language)或HLSL(High Level Shader Language)编写的程序,它运行于GPU上,专注于对图像相关的数据计算的处理。Shader是一个通用的概念,它不受限于某个框架。

编写OpenGL程序时需要同时编写OpenGL主程序和Shader程序。早期的OpenGL使用时可以选择有没有Shader,但是现在更高版本的OpenGL已经是必须使用Shader了。

在OpenGL中Shader分为以下几种(包括但不限于)。
(1)Fragment Shader
Fragment Shader用于计算每个需要绘制的像素点的颜色。
(2)Geometry Shader
Geometry Shader用于处理点、线、面的几何坐标变换。
(3)Vertex Shader
Vertex Shader用于计算将目标点坐标变换为实际屏幕上显示的二维坐标。
下面就正式开始学习OpenGL吧!

4.Hello Triangle!
从一个最简单的绘制三角形的example开始,初步了解使用OpenGL的流程。

1
2
3
4
// initialise GLFW
glfwSetErrorCallback(OnError);
if(!glfwInit())
throw std::runtime_error("glfwInit failed");

初始化GLFW。GLFW是什么呢?开始提到OpenGL的使用必须搭配窗口图形库,GLFW是一个轻量级的跨平台的为OpenGL设计的窗口库。
在使用OpenGL前,首先要创建一个窗口。这里初始化GLFW。

1
2
3
4
5
6
7
8
9
// open a window with GLFW
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
gWindow = glfwCreateWindow((int)SCREEN_SIZE.x, (int)SCREEN_SIZE.y, "OpenGL Tutorial", NULL, NULL);
if(!gWindow)
throw std::runtime_error("glfwCreateWindow failed. Can your hardware handle OpenGL 3.2?");

在这里使用GLFW打开一个窗口。

1
2
// GLFW settings
glfwMakeContextCurrent(gWindow);

为当前窗口设置一个OpenGL上下文,类似于Android中的Context。

1
2
3
4
// initialise GLEW
glewExperimental = GL_TRUE; //stops glew crashing on OSX :-/
if(glewInit() != GLEW_OK)
throw std::runtime_error("glewInit failed");

这里初始化GLEW。什么是GLEW?在现代OpenGL版本里,不能直接使用#include< … >这样的静态链接的方式调用OpenGL的API,它的API在运行时动态调用。GLEW就是我们动态调用OpenGL的入口。

1
2
3
4
5
6
7
// loads the vertex shader and fragment shader, and links them to make the global gProgram
static void LoadShaders() {
std::vector<tdogl::Shader> shaders;
shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("vertex-shader.txt"), GL_VERTEX_SHADER));
shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("fragment-shader.txt"), GL_FRAGMENT_SHADER));
gProgram = new tdogl::Program(shaders);
}

自定义一个LoadShaders方法来初始化shader。
在之前提过的,shader以GLSL书写,这里shader的具体代码存在一个txt文件里,通过这个函数来加载shader文件里的代码。

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
// loads a triangle into the VAO global
static void LoadTriangle() {
// make and bind the VAO
glGenVertexArrays(1, &gVAO);
glBindVertexArray(gVAO);

// make and bind the VBO
glGenBuffers(1, &gVBO);
glBindBuffer(GL_ARRAY_BUFFER, gVBO);

// Put the three triangle verticies into the VBO
GLfloat vertexData[] = {
// X Y Z
0.0f, 0.8f, 0.0f,
-0.8f,-0.8f, 0.0f,
0.8f,-0.8f, 0.0f,
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

// connect the xyz to the "vert" attribute of the vertex shader
glEnableVertexAttribArray(gProgram->attrib("vert"));
glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);

// unbind the VBO and VAO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}

这里是真正绘制三角形的代码。
一点点看。
// make and bind the VBO glGenBuffers(1, &gVBO); glBindBuffer(GL_ARRAY_BUFFER, gVBO);
// make and bind the VAO glGenVertexArrays(1, &gVAO); glBindVertexArray(gVAO);
这里创建一个VBO对象和VAO对象,并把它和OpenGL绑定
VBO和VAO是什么呢?
VAO(Vertex Array Object)和VBO(Vertex Buffer Object)是C++程序和Shader程序传递数据使用的对象。
VBO存储要传递给Shader的数据,但是这里的数据没有类型。VAO对VBO和Shader进行连接,它描述了VBO存储的数据的类型,以及该传递个Shader的数据。

// Put the three triangle verticies into the VBO GLfloat vertexData[] = { // X Y Z 0.0f, 0.8f, 0.0f, -0.8f,-0.8f, 0.0f, 0.8f,-0.8f, 0.0f, }; glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData),vertexData, GL_STATIC_DRAW);
这里向VBO添加数据,因为要绘制三角形,所以给VBO添加三个点的坐标值。
// connect the xyz to the "vert" attribute of the vertex shader glEnableVertexAttribArray(gProgram->attrib("vert")); glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);
调用VAO配置数据。这里设置变量的类型为vert。

// unbind the VBO and VAO glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);
最后对VBO和VAO解除绑定,因为以后不再需要它们了,这样也可以避免对数据误操作。

1
2
3
4
5
6
7
8
// run while the window is open
while(!glfwWindowShouldClose(gWindow)){
// process pending events
glfwPollEvents();

// draw one frame
Render();
}

数据设定完后调用Render函数开始渲染绘图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// draws a single frame
static void Render() {
// clear everything
glClearColor(0, 0, 0, 1); // black
glClear(GL_COLOR_BUFFER_BIT);

// bind the program (the shaders)
glUseProgram(gProgram->object());

// bind the VAO (the triangle)
glBindVertexArray(gVAO);

// draw the VAO
glDrawArrays(GL_TRIANGLES, 0, 3);

// unbind the VAO
glBindVertexArray(0);

// unbind the program
glUseProgram(0);

// swap the display buffers (displays what was just drawn)
glfwSwapBuffers(gWindow);
}

glClearColor(0, 0, 0, 1); // black glClear(GL_COLOR_BUFFER_BIT);
清空屏幕,让窗口背景变成黑色。
``
// bind the program (the shaders)
glUseProgram(gProgram->object());

// bind the VAO (the triangle)
glBindVertexArray(gVAO);

``
通知OpenGL使用VAO和shader。

为什么实际电路总用与非门和或非门

做组原实验时发现Alu的实现电路总是要从与门、或门这样的简单电路推导成与非门和或非门实现。
有点困惑,后来找到了答案。
有如下几个原因:
(1)考虑电子元件的成本,也许从逻辑上来看与非门和或非门比与门和非门更复杂,但是实际上由于Mos管的物理结构,实现与非门和或非门需要的元件其实更少,成本更低,而简单的与门和或门其实在结构上比前者更复杂。
(2)或非门和与非门具有逻辑完备性,任何一个门通过组合可以实现任意电路,而与门和或门不具有这样的能力。
(3)仍然和电子元件的物理结构有关,与非门和或非门实际运行效率比与门和或门更高。

Android Studio查看类、接口继承实现关系

在查看源码的时候,经常碰到需要找父类子类继承关系和接口的实现类等问题。Android Studio提供了两个方法可以快速查看这些关系。
1.查看类继承关系。使用Android Studio的Hierarchy工具,在右边点开即可。
2.查看接口实现。
Android Studio提供了一个叫Implementations的快捷键功能,在Mac下默认是alt+command+b,会自动查找当前接口的实现类。三个键太难按,我稍作修改,重置了快捷键改成command+i。
以后再也不用怕接口找不到实现类了。

Glide学习记录(2)

本篇继续上次未完成的Glide学习记录。
上次说到了Glide的with()方法,Glide是如何实现图片加载结合组件声明周期的,这次继续讲解之后的方法。
glide通常的使用方式:

1
2
3
4
5
6
7
Glide
.with(myFragment)
.load(url)
.centerCrop()
.placeholder(R.drawable.loading_spinner)
.crossFade()
.into(myImageView);

这是来自Glide的官方示例。
下面我们来看一看load方法做了些什么。

1
2
3
4
5
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static RequestManager with(android.app.Fragment fragment) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(fragment);
}

可以看到,调用with方法后返回了一个RequestManager实例,现在深入RequestManager内部看一看load方法。
在这之前首先来看一看RequestManager到底是个什么东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* A class for managing and starting requests for Glide. Can use activity, fragment and connectivity
* lifecycle events to intelligently stop, start, and restart requests. Retrieve either by
* instantiating a new object, or to take advantage built in Activity and Fragment lifecycle
* handling, use the static Glide.load methods with your Fragment or Activity.
*
* @see Glide#with(android.app.Activity)
* @see Glide#with(android.support.v4.app.FragmentActivity)
* @see Glide#with(android.app.Fragment)
* @see Glide#with(android.support.v4.app.Fragment)
* @see Glide#with(Context)
*/
public class RequestManager implements LifecycleListener

按照注释说明RequestManager是一个管理开启Request的类,这个可以智能的开启、停止、重新发起一个Request,我们可以看到这个类继承于LifecycleListener,在上一章的学习里我们已经知道了它的实例在with方法中被创建并被加入到子Fragment中,因而能实现智能的控制Request。
OK,接下来就去看load的具体内容。

1
2
3
4
5
6
7
8
9
/**
* A helper method equivalent to calling {@link #asDrawable()} and then {@link
* RequestBuilder#load(Object)} with the given model.
*
* @return A new request builder for loading a {@link Drawable} using the given model.
*/
public RequestBuilder<Drawable> load(@Nullable Object model) {
return asDrawable().load(model);
}

可以看到这是一个工具方法,它等效于两个方法:
(1)构造方法asDrawalbe()
(2)load(model)
一步一步深入这两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Attempts to always load the resource using any registered {@link
* com.bumptech.glide.load.ResourceDecoder}s that can decode any subclass of {@link Drawable}.
*
* <p> By default, may return either a {@link android.graphics.drawable.BitmapDrawable} or {@link
* GifDrawable}, but if additional decoders are registered for other {@link Drawable} subclasses,
* any of those subclasses may also be returned. </p>
*
* @return A new request builder for loading a {@link Drawable}.
*/
public RequestBuilder<Drawable> asDrawable() {
return as(Drawable.class).transition(new DrawableTransitionOptions());
}

按照注释,asDrawlbe类会尝试使用所有注册的ResourceDecoder去解码资源,这个ResourceDecoder可以解码任何Drawble的子类。在默认情况下,它会返回BitmapDrawble和GifDrawable两者中的一个,但是如果你手动注册额外的解码器,那么资源就可以被解码成Drawable的任何子类。
这里有几点需要说明:
首先,通过网络调用获取到的图片传到本机上时应该只是一组二进制序列(图片通常使用Base64传输),而解码器就是决定如何解析它的工具,Glide的解码器还决定它以什么实例返回。
可以看到,Glide给我们提供了充分的自由,这个ResourceDecoder接口可以被我们手动实现然后注册,只要我们需要的返回类的形式是Drawalbe的子类,那么它都可以满足。
接下来是ReuqestBuilder的load方法:

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

/**
* Sets the specific model to load data for.
*
* <p> This method must be called at least once before
* {@link #into(com.bumptech.glide.request.target.Target)} is called. </p>
*
* @param model The model to load data for, or null.
* @return This request builder.
*/
@SuppressWarnings("unchecked")
public RequestBuilder<TranscodeType> load(@Nullable Object model) {
return loadGeneric(model);
}

这里使用了工厂模式,它的注释说了很多,但是其实这里没有做什么事情,只是把内部的model的设置成传入的model。

1
2
3
4
5
private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
this.model = model;
isModelSet = true;
return this;
}

到了这里load方法就结束了,通过之前的分析,load方法内部其实没有什么真正的业务,它通过RequestManager生成了一个RequestBuilder实例并对这个实例做了一些配置。
接下来我们先来看一看into方法,实际看一看从RequestBuilder到图片加载完成的过程。

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
/**
* Sets the {@link ImageView} the resource will be loaded into, cancels any existing loads into
* the view, and frees any resources Glide may have previously loaded into the view so they may be
* reused.
*
* @see RequestManager#clear(Target)
*
* @param view The view to cancel previous loads for and load the new resource into.
* @return The
* {@link com.bumptech.glide.request.target.Target} used to wrap the given {@link ImageView}.
*/

public Target<TranscodeType> into(ImageView view) {
Util.assertMainThread();
Preconditions.checkNotNull(view);

if (!requestOptions.isTransformationSet()
&& requestOptions.isTransformationAllowed()
&& view.getScaleType() != null) {
if (requestOptions.isLocked()) {
requestOptions = requestOptions.clone();
}
switch (view.getScaleType()) {
case CENTER_CROP:
requestOptions.optionalCenterCrop(context);
break;
case CENTER_INSIDE:
requestOptions.optionalCenterInside(context);
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
requestOptions.optionalFitCenter(context);
break;
//$CASES-OMITTED$
default:
// Do nothing.
}
}

return into(context.buildImageViewTarget(view, transcodeClass));
}

先看注释,调用into方法后会取消之前所有的Request并且清除之前Glide加载进这个view的内,因此我们可以放心的多次调用into。
再看into方法的逻辑,这里它也没有做什么真正的逻辑,可以看到,它根据imageview的ScaleType去配置了requestBuilder中的requestOption,这个RequestOption之后我们再详细去看,现在先跳到下一步。
最后调用了重载的另一个into方法,它的参数是一个Target,这里调用了GlideContext里的一个方法生成了一个Target传入。
现在来看看参数为Target的into方法。

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
/**
* Set the target the resource will be loaded into.
*
* @param target The target to load the resource into.
* @return The given target.
* @see RequestManager#clear(Target)
*/

public <Y extends Target<TranscodeType>> Y into(@NonNull Y target) {
Util.assertMainThread();
Preconditions.checkNotNull(target);
if (!isModelSet) {
throw new IllegalArgumentException("You must call #load() before calling #into()");
}

Request previous = target.getRequest();

if (previous != null) {
requestManager.clear(target);
}

requestOptions.lock();
Request request = buildRequest(target);
target.setRequest(request);
requestManager.track(target, request);

return target;
}

这个方法挺容易理解的,首先判断是不是在主线程,如果不是就直接抛出异常,也就是说into方法只能在主线程被调用。接下来也是做一些检查,target是不是空、执行前有没有执行with()方法。然后清除target之前的request,最后调用lock方法对这个request上锁,不能再对request做任何的改动,Glide也没有提供直接解锁的方法,唯一的解锁方法就是调用clone再创建一个requestOption。这样设计的用意就是使request不能复用。
最后创建新的reuqest把target的request设置成当前request然后使用reuqestManager的track方法执行request。

1
2
3
4
void track(Target<?> target, Request request) {
targetTracker.track(target);
requestTracker.runRequest(request);
}

可以看到,真正执行网络操作的内容就在runRequest下面。

1
2
3
4
5
6
7
8
9
10
11
/**
* Starts tracking the given request.
*/

public void runRequest(Request request) {
requests.add(request);
if (!isPaused) {
request.begin();
} else {
pendingRequests.add(request);
}
}

这里执行真正的加载逻辑,因为Request只是一个接口,现在要回头去找它的真正实现类。
我们回到上一个方法里找到创建request的方法。在上一个into方法里有一个buildRequest方法。

1
2
3
4
private Request buildRequest(Target<TranscodeType> target) {
return buildRequestRecursive(target, null, transitionOptions, requestOptions.getPriority(),
requestOptions.getOverrideWidth(), requestOptions.getOverrideHeight());
}

这个方法只是一个中转方法,真正的实现在buildRequestRecursive里,接下来深入这个方法的内部。

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
private Request buildRequestRecursive(Target<TranscodeType> target,
@Nullable ThumbnailRequestCoordinator parentCoordinator,
TransitionOptions<?, ? super TranscodeType> transitionOptions,
Priority priority, int overrideWidth, int overrideHeight) {

if (thumbnailBuilder != null) {
// Recursive case: contains a potentially recursive thumbnail request builder.
if (isThumbnailBuilt) {
throw new IllegalStateException("You cannot use a request as both the main request and a "
+ "thumbnail, consider using clone() on the request(s) passed to thumbnail()");

}

TransitionOptions<?, ? super TranscodeType> thumbTransitionOptions =
thumbnailBuilder.transitionOptions;
if (DEFAULT_ANIMATION_OPTIONS.equals(thumbTransitionOptions)) {
thumbTransitionOptions = transitionOptions;
}

Priority thumbPriority = thumbnailBuilder.requestOptions.isPrioritySet()
? thumbnailBuilder.requestOptions.getPriority() : getThumbnailPriority(priority);

int thumbOverrideWidth = thumbnailBuilder.requestOptions.getOverrideWidth();
int thumbOverrideHeight = thumbnailBuilder.requestOptions.getOverrideHeight();
if (Util.isValidDimensions(overrideWidth, overrideHeight)
&& !thumbnailBuilder.requestOptions.isValidOverride()) {

thumbOverrideWidth = requestOptions.getOverrideWidth();
thumbOverrideHeight = requestOptions.getOverrideHeight();
}

ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
Request fullRequest = obtainRequest(target, requestOptions, coordinator,
transitionOptions, priority, overrideWidth, overrideHeight);

isThumbnailBuilt = true;
// Recursively generate thumbnail requests.
Request thumbRequest = thumbnailBuilder.buildRequestRecursive(target, coordinator,
thumbTransitionOptions, thumbPriority, thumbOverrideWidth, thumbOverrideHeight);

isThumbnailBuilt = false;
coordinator.setRequests(fullRequest, thumbRequest);
return coordinator;
} else if (thumbSizeMultiplier != null) {
// Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse.
ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
Request fullRequest = obtainRequest(target, requestOptions, coordinator, transitionOptions,
priority, overrideWidth, overrideHeight);

BaseRequestOptions<?> thumbnailOptions = requestOptions.clone()
.sizeMultiplier(thumbSizeMultiplier);

Request thumbnailRequest = obtainRequest(target, thumbnailOptions, coordinator,
transitionOptions, getThumbnailPriority(priority), overrideWidth, overrideHeight);


coordinator.setRequests(fullRequest, thumbnailRequest);
return coordinator;
} else {
// Base case: no thumbnail.
return obtainRequest(target, requestOptions, parentCoordinator, transitionOptions, priority,
overrideWidth, overrideHeight);

}
}

前面那些复杂的逻辑先不看,直接跳到最后一行,可以看到reuqest的实现类是SingleRequest。
深入看一下这个类的逻辑

1
2
3
4
5
6
7
/**
* A {@link Request} that loads a {@link com.bumptech.glide.load.engine.Resource} into a given
* {@link Target}.
*
* @param <R> The type of the resource that will be transcoded from the loaded resource.
*/

public final class SingleRequest<R> implements Request,

这个类是是一个resource和一个request的结合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public void begin() {
stateVerifier.throwIfRecycled();
startTime = LogTime.getLogTime();
if (model == null) {
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
width = overrideWidth;
height = overrideHeight;
}
// Only log at more verbose log levels if the user has set a fallback drawable, because
// fallback Drawables indicate the user expects null models occasionally.
int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
onLoadFailed(new GlideException("Received null model"), logLevel);
return;
}

status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
onSizeReady(overrideWidth, overrideHeight);
} else {
target.getSize(this);
}

if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
&& canNotifyStatusChanged()) {

target.onLoadStarted(getPlaceholderDrawable());
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}

这里就是刚才begin真正执行的内容。到了这里结果我们又发现真正加载的逻辑是在Target的方法里,而这个Target同样也是一个接口,现在得倒回去找Target的实现类。
重新倒回去into方法,最后可以找到创建Target的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* A factory responsible for producing the correct type of
* {@link com.bumptech.glide.request.target.Target} for a given {@link android.view.View} subclass.
*/

public class ImageViewTargetFactory {

@SuppressWarnings("unchecked")
public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
if (Bitmap.class.equals(clazz)) {
return (Target<Z>) new BitmapImageViewTarget(view);
} else if (Drawable.class.isAssignableFrom(clazz)) {
return (Target<Z>) new DrawableImageViewTarget(view);
} else {
throw new IllegalArgumentException(
"Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
}
}
}

可以看到Target是分类的,和Request的单个实现不同。还记得我们之前说到的asDrawalbe方法吗,那个方法就是设置这个传入的class,最后生成的对应Target对应的就应该是DrawalbeImageViewTarget,这里可以看到还有另一种Target:BitmapImageViewTarget。具体有什么区别过会再说,先看DrawalbeImageViewTarget的内部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* A target for display {@link Drawable} objects in {@link ImageView}s.
*/

public class DrawableImageViewTarget extends ImageViewTarget<Drawable> {

public DrawableImageViewTarget(ImageView view) {
super(view);
}

@Override
protected void setResource(@Nullable Drawable resource) {
view.setImageDrawable(resource);
}
}

没什么内容,看来实现还在父类里。
在它的父类里找到了这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link
* android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}.
*
* @param placeholder {@inheritDoc}
*/
@Override
public void onLoadStarted(@Nullable Drawable placeholder) {
super.onLoadStarted(placeholder);
setResourceInternal(null);
setDrawable(placeholder);
}

注释里说明了这里就仅仅是调用了view的setImageDrawable方法把图片设置进入。那么是什么时候得到这个Drawable的呢?看到这个方法的参数placeholder,我们再回头找它的调用者。
它的调用者正是singleRequest实例,现在转回去看这部分逻辑。

1
2
3
4
5
6
7
8
9
private Drawable getPlaceholderDrawable() {
if (placeholderDrawable == null) {
placeholderDrawable = requestOptions.getPlaceholderDrawable();
if (placeholderDrawable == null && requestOptions.getPlaceholderId() > 0) {
placeholderDrawable = loadDrawable(requestOptions.getPlaceholderId());
}
}
return placeholderDrawable;
}

两个常用IP地址的区别

今天写Socket程序的时候碰到了个本机地址127.0.0.1。有点疑惑,以前我一直以为本机地址是192.168.0.1。在网上找到了一些资料,做一些记录。
(1)IP:127.0.0.1
这个IP确实就是本机地址,只要本机的TCP/IP协议栈安装且正常运行
它就能ping通。通常用这个地址来测试本机TCP/IP是否正常。
(2)IP:192.168.0.1
这个IP不是本机地址,它是你连接使用的路由器的地址!以前我也试过用这个地址来测试路由器是否正常运行,现在想起来,以前是不通原理,瞎弄一通,所以现在才有这个问题。当没有网络时可以试着ping这个IP,如果它能ping通,说明可以连接到路由器,那么问题就不是路由器的硬件问题了。

Socket编程学习记录

Socket编程学习记录
从大一开始就反复接触到Socket这个词,但是一直没有好好的了解一下这方面的东西。
今天学习并整理了一下相关内容。

####1.socket概念
(1)Socket到底是什么?
一句话:Socket是进程通讯的一种方式。
Socket实现的进程通讯不同于信号量、共享内存的本地进程通讯,它提供给我们的是网络上远程地址的主机上的某个进程的通讯。
Socket机制提供的是网络进程通讯。那么具体到一台主机上的Socket编程中的某个Socket实例就可以理解为:IP地址+进程端口号,一个Socket实例等同于网络访问中的标识符,类似于文件标识符,它直接定位到某台主机上的某个进程。
因此网络通讯可以抽象为两台主机上的两个Socket的简单IO操作。
(2)Socket有什么作用?
简化基于CS模式的网络进程通讯,忽略信息传递的复杂过程。在创建本地Socket并获取远程Socket建立通信后,网络进程通讯就等同于对本地文件的简单IO操作,每次传递信息仅需要读写Socket即可。
注意:我看了很多文章提到Socket就一定是TCP那一套。后来又查了一些资料:Socket只是网络通讯的一个管道,它是TCP/IP协议族的应用层抽象,因此不管是TCP还是UDP或者是其它的什么协议都是可以通过Socket来完成的。Socket仅仅是运输数据的一个管道,至于管道里的数据怎么运,有什么格式和规则,那才是协议关心的事情。

####2.Socket编程基础
以下介绍Socket编程的基础函数。
以下均为C语言接口。
Socket编程中Client和Server有很大区别,需要注意,以下均标注。
(1)创建Socket (Client\Server)
创建一个socket实例
int socket(int domain,int type,int protocol);

domain 协议族 IPV4 IPV6 等
type socket类型 ??
protocol 指定协议 使用TCP、UDP、STCP等
type和protocol不能随意组合 有些特定的要求
和打开文件类似,返回值是一个socket标识符

(2)手动绑定地址 (Server)
把地址族中的特定地址赋给socket,用来手动指定socket绑定的地址(如IP+端口号)
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

sockfd socket标识符
addr 指向要绑定给sockfd的协议 如ipv4
addrlen 地址的长度 32位或者16位

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

(3)进入监听状态 (Server)
服务器调用socket()和bind()以后调用listen()监听socket收到的请求。
int listen(int sockfd,int backlog);

sockfd socket标识符
backlog 这个socket可以排队的最大连接个数

(4)发起一个连接请求(Client)
客户端调用socket()后直接使用connect连接服务器。
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

sockfd socket标识符
addr 服务器的socket地址
addrlen socket地址的长度

(4)接受请求 (Server)
服务器监听到请求后调用accept()接收请求,接收完后即可进行网络IO操作,操作方式等同于操作本地文件
int accept(int sockfd,struct *addr,socklen_t *addrlen);

三个参数:socket标识符 客户端的协议地址 协议地址长度
返回值是一个全新的标识符,代表和客户端通信的TCP客户端。

注意,这里的sockfd是服务器上的某个socket,而不是客户端的。客户端在服务器上的Socket标识符由内核负责创建,在通讯被关闭后自动销毁。
(5)读写Socket中的数据 (Client\Server)

1
2
3
4
5
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

这是Socket提供的几个IO函数。
它们的使用方式和一般的IO函数类似,其中最常用的是
recvmsg()/sendmsg()
这一对函数。
由于这几个函数参数比较多,具体的说明留置使用时查询文档。这里不再解释。
(6)关闭Socket (Client\Server)
int close(int fd);
和关闭一个文件类似,每次创建完Socket后需要手动关闭回收。