Learning


  • 首页

  • 归档

  • 标签

  • 关于

freeline databinding-资源文件增量编译出错的原因及解决办法

发表于 2018-09-02

在AS 3.0,使用freeline的过程中,如果使用了databinding,在增量编译资源的文件的时候总是会报错,

提示的内容如下:

​ DataBindingInfo.java:5: 错误: 找不到符号。

首先来看一下这个 DataBindingInfo.java 中到底有些什么内容。

在使用freeline的项目中我们可以找到app/build/freeline/freeline-databinding/app/xxxxxxxxx/java/android/databinding/layouts/DatabindingInfo.java

这个类的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
@BindingBuildInfo(buildId="ef2e64c5-1191-4d5b-9eff-3781e851fe10",
modulePackage="com.devtom.freelinetest",
sdkRoot="/Users/tomliu/Library/Android/sdk",
layoutInfoDir="/Users/tomliu/project/freelinetest/app/build/freeline/freeline-databinding/merged_layoutinfo",
exportClassListTo="",
isLibrary=false,
minSdk=18,
enableDebugLogs=true,
printEncodedError=true)
public class DataBindingInfo {
}

这个类的内容十分的简单,一个叫DataBindingInfo 的类,使用了 一个叫 BindingBuildInfo的注解,这个注解里面有一些其他的属性,但是问题就是处在这个属性上面,我们可以直接在AS中搜索一下 BindingBuildInfo 这个注解。你会看到下面这个代码。

1
2
3
4
5
6
7
8
9
10
11
12
package android.databinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface BindingBuildInfo {
String buildId() default "";
}

是不是一看知道问题在哪里了,BindingBuildInfo 这个注解里面根本都没有除了buildId之外的其他属性,如果在生成的类上面给这个注解强行加上这些属性,自然就报了找不到符号了。问题找到了那么我们需要找到这个 DataBindingInfo 类是怎么生成的。

在freeline当中有一个模块叫做freeline-databinding-cli,我们从这个模块里面入手,这个模块里面的代码很简单,这个模块生成了一个叫做databinding-cli.jar的jar文件,这个jar文件里面包含了一个main 方法,就是jar包的入口了。

然后直接查看类名ExportDataBindingInfo ,就很容易猜到 DataBindingInfo 是通过这个类生成的,我们进入到

DataBindingHelper.getLayoutXmlProcessor().writeInfoClass(sdkDirectory, outputDirectory, null, true, true);

这个方法最终会进入 LayoutXmlProcessor 的 writeInfoClass 方法,LayoutXmlProcessor 在com.databinding.tool这个包当中,这个包是通过gradle引入的,在freeline 当中,如果是使用java 8 ,这个包的版本是2.2.0,如果使用Java7,这个版本是2.1.3, 在这里就以java7为例说明。

在LayoutXmlProcessor当中,writeInfoClass的代码如下:

1
2
3
4
5
6
7
8
9
public void writeInfoClass(File sdkDir, File xmlOutDir, File exportClassListTo, boolean enableDebugLogs, boolean printEncodedErrorLogs) {
Escaper javaEscaper = SourceCodeEscapers.javaCharEscaper();
String sdkPath = sdkDir == null?null:javaEscaper.escape(sdkDir.getAbsolutePath());
Class annotation = BindingBuildInfo.class;
String layoutInfoPath = javaEscaper.escape(xmlOutDir.getAbsolutePath());
String exportClassListToPath = exportClassListTo == null?"":javaEscaper.escape(exportClassListTo.getAbsolutePath());
String classString = "package android.databinding.layouts;\n\nimport " + annotation.getCanonicalName() + ";\n\n" + "@" + annotation.getSimpleName() + "(buildId=\"" + this.mBuildId + "\", " + "modulePackage=\"" + this.mResourceBundle.getAppPackage() + "\", " + "sdkRoot=" + "\"" + (sdkPath == null?"":sdkPath) + "\"," + "layoutInfoDir=\"" + layoutInfoPath + "\"," + "exportClassListTo=\"" + exportClassListToPath + "\"," + "isLibrary=" + this.mIsLibrary + "," + "minSdk=" + this.mMinSdk + "," + "enableDebugLogs=" + enableDebugLogs + "," + "printEncodedError=" + printEncodedErrorLogs + ")\n" + "public class " + "DataBindingInfo" + " {}\n";
this.mFileWriter.writeToFile("android.databinding.layouts.DataBindingInfo", classString);
}

从中不难发现在生成这个类的时候,LayoutXmlProcessor自动的添加上了这些属性。

既然这是databinding版本的问题那么可以在build.gradle 文件的dataBinding 节点设置version 来改变databinding 的版本问题.

但是这个设定只是解决了databinidng compiler的版本,而 BindingBuildInfo 的版本却是我们在项目中gradle 插件的版本决定的,也就是项目根目录下面build.gradle 的 com.android.tools.build:gradle 版本号决定。

因此这里只改模块中databinding 编译的版本号也无法通过编译(需要查看databinidng 编译器的编译的源码查看 BindingBuildInfo 到底用的那个文件)需要同时修改想项目插件的版本,我想很多人应该都把AS 升级到了3.0,那么这里是无法使用2.0版本的gradle插件的。

那么这里的话就只能修改freeLine中compiler的版本的了,那么这之后项目中依赖的compilerCommon版本和freeline当中的一样了,

除此之外ExportDataBinding 中移除了writeClassInfo 方法,所以代码修改如下

1
2
3
4
5
6
7
public class ExportDataBindingInfo {
public static void run(File sdkDirectory, File outputDirectory) {
// dataBindingExportBuildInfo
// TODO: exportClassListTo
DataBindingHelper.getLayoutXmlProcessor().writeEmptyInfoClass();
}
}

但是修改了之后项目通过了,但是运行仍然会崩溃,还需要继续学习研究。

Okhttp学习笔记(持续更新中)

发表于 2018-03-25

之前使用Retrofit的时候把里面的源码过了一遍,retrofit的核心在于帮助使用者构建构建一个个请求,避免我们每次调用网络的时候都需要重新的去创建请求,其底层的网络库的实现采用的是okhttp, 下面是自己看了源码之后的学习笔记(目前还比较凌乱,后面写完了会重新整理)

个人的学习源码的习惯从一个非常简单的事例开始,然后自己跟着这个示例一步步的调试源码,看源码里面到底是怎么一步步实现的的,下面是非常简单的一个例子,使用okhttp 请求访问百度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
OkHttpClient client = new OkHttpClient.Builder().eventListener(new EventListener() {
@Override
public void callStart(Call call) {
super.callStart(call);
}
}).build();
String url = "https://www.baidu.com";
Request request = new Request.Builder().url(url).build();
Response response = null;
try {
response = client.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
if (response.isSuccessful()) {
try {
Log.e(TAG,response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}

Okhttp的核心设计

okhttp的核心设计在于将一个请求的不同阶段分开实现通过 Interceptor 来实现,通过这样的分层,每个层级专注于自己的工作,充分实现了解耦,比如针对是http1还是http2,BridgeInterceptor 只负责添加一些基本的请求头,而不用管TCP 连接到底是复用还是新建的,任何TCP链接实现层的变动都不会影响 BridgeInterceptor的实现。

在默认构建的OkhttpClient中,添加了四个Interceptor ,

BridgeInterceptor 主要用于构建http 请求,在拿到用户的请求之后,会自己添加一些基本的头信息

CacheInterceptor 主要负责应用缓存,通过缓存处理请求和根据响应头内容缓存响应

ConnectInterceptor okhttp中的重点,找到一个可用的链接来发送请求,其中的逻辑比较复杂也是重点学习和分析的对象。

CallServerInterceptor 最终完成与server的交互,向server写入请求和读取请求。

在发送请求的过程中,请求发送的过程中从上往下调用,一步步将请求发送到server,在处理响应的过程中,从下往上调用,一步步将响应交给用户。

Client的构建

在Ok中一个非常重要的对象就是Client,在这个对象当中我们根据自己的需要来定制一些对请求和响应的处理,监听请求的各个阶段,代理的设置,缓存的处理,请求协议的处理,以及在OK中非常重要的拦截器的配置等等,这些实际上都是在Client构建的过程中完成的在示例中我们简单的创建了一个请求事件的监听者,监听请求刚开始发送的事件。

在这个Client对象中,有几个比较重要的成员需要先说一下

  1. Dispatcher 在文档的注释中,这个类的说明是 当异步请求执行时的策略, 也就是异步请求是怎么被调度的,其中是采用ExecutorService 在实现每一个call的调用。
  2. Interceptor ,拦截器,在Client当中为我们默认的添加了一列的拦截器,在拦截器中我们可以对发出去的请求和接收到的响应做修改,这个和 Java web当中的filter很像,都是在中间环节去请求和响应做一些处理。在Interceptor 中还有一个子接口,Chain, 这个接口中定义一个十分重要的方法,
1
Response proceed(Request request) throws IOException;

即处理请求返回响应,同时这个接口也可以间接的说明这些拦截器构成了一个链 来处理这些请求。

请求的构建

一个Http Request 的构建

ok中请求的构建使用过建造者模式创造出来的,非常的简单,其中包括了构建一个请求的必不可少的要素,包括方法,请求地址 , 请求的头部,请求的body. 不指定的方法时候默认使用 GET 方法 , 同时头部默认是一个空的集合,既然默认是GET方法,那么body也就为空了,除此之外每个请求之中还有一个为object类型的tag.

创建一个请求的调用

在ok中,使用call这个接口来表示一个请求/响应 的调用,每个请求只能够执行一次,其中有两个关于执行请求的方法。

1
2
3
4
Response execute() throws IOException;
void enqueue(Callback responseCallback);

execute 方法表示同步执行这个请求,在执行了之后,就会返回这个响应,而enqueue则是异步的执行请求,在请求执行完成之后会调用传入的callback 对象。

在上面的示例代码中,我们直接调用execute方法,让这个方法同步执行,返回一个响应。

先看 client.newCall(request) 这个方法

1
2
3
4
5
6
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}

​ 方法十分简单,返回了一个RealCall 对象,而这个RealCall对象实现了Call这个接口,表示了这是一个请求调用,从上面的这一部分,可以说就完成了一个请求的对象的构建,接下来就是将我们的请求对象发送出去了。

请求的发送

1. TCP链接

因为Http 是基于TCP协议的,所以一个HTTP请求的构建必须建立在一个TCP链接之上,而okhttp之所以这么受欢迎就是因为这个库在TCP链接的使用上做了比较大的优化, 在okhttp中,使用 Connection 来表示一个链接,一个链接上有多个数据流,在http 1版本中可以一个连接上只能有一个数据流,在http2中可以实现多路复用,而实现了一个链接上有多个数据流同时传输.(其实这里的数据流可以理解为一个http请求)。

在okttp中,为了实现一个一个数据流去复用一个connection ,数据流和链接被分离开来。

在实际的请求当中,StreamAllocation 扮演了一个重要的角色,在okhttp的文档当中,这个类将三个重要的实体关联了起来

  • 连接,代表了和远程服务器之间的物理连接
  • Stream,表示在连接上之上的http请求和响应对
  • Calls: 表示一个逻辑序列的Stream调用

这个对象的创建时在 RetryAndFollowUpInterceptor 这个拦截器的 intercept 方法当中,实际上每次请求调用的时候都会首先执行 RetryAndFollowUpInterceptor 的 intercept 方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
int followUpCount = 0;
Response priorResponse = null;
....
}

在构建这个对象的时候需要一个ConnectionPool ,一个Address 对象,请求调用和,事件监听对象,以及一个object.

接下来我们来看一个请求的连接是如何构成的,在http中定义了一系列额拦截器,每个拦截器都在构建发送http请求的过程中发挥了作用,其中 网络连接的建立请求是通过 ConnectInterceptor 这个拦截器完成的,其中的 intercept 方法实现如下

1
2
3
4
5
6
7
8
9
10
11
12
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

在方法的第三行拿到的streamAllocation对象就是我们在 RetryAndFollowUpInterceptor 中创建的 StreamAllocation 对象,然后获取了一个HttpCodec 对象。 在okhttp中,实现了对http 1/2 两个版本的支持。而两种实现都是通过对HttpCodec这个接口实现实现。在文档中,对这个类只有一句话,

Encodes HTTP requests and decodes HTTP responses.

先看这个HttpCodec是怎么获取的,进入到StreamAlloaction中的newStream 方法当中,核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
...
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}

在方法当中核心是构建了一个Connection 对象(代表了到远程服务器的一个链接),然后在这个对象上构建了HttpCodec 方法,继续看 关键的findHealthyConnection方法,

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
/**
* Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
* until a healthy connection is found.
*/
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
// If this is a brand new connection, we can skip the extensive health checks.
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}

其实这个方法的注释也说得比较清楚了,从代码中也可以看到在一个死循环中寻找一个可用的Connection ,核心代码是 findConnection 这个方法的执行。

从这个方法的注释当中可以看出来这个方法到底是怎么工作的

  1. 如果当前已经有一个链接了,那么直接复用这个链接。
  2. 如果没有的话从连接池里面拿一个链接。
  3. 如果连接池里面没有的话,那么新建一个链接。

第一个步骤略过, 接下来,看下这三个步骤是如何构执行的。

从连接池里面找到链接。

1
2
3
4
5
6
// Attempt to get a connection from the pool.
RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
if (pooledConnection != null) {
this.connection = pooledConnection;
return pooledConnection;
}

Internal.instance 这个对象是在构建 OkHttpClient 这个Class对象的时候创建,放在OkHttpClient的静态代码块中,目前来这个类中没有什么有价值的代码,所有方法的实现都是直接调用了传入参数的方法,相当于只做了已成封装,看get方法的调用

1
2
3
public RealConnection get(ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
return pool.get(address, streamAllocation);
}

直接返回了pool调用的结果,进入ConnectionPool 的 get方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Returns a recycled connection to {@code address}, or null if no such connection exists. The
* route is null if the address has not yet been routed.
*/
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}

方法中把当前已有的connections 遍历了一遍,然后筛选出一个可用的链接,进入到isEligible 这个方法当中

这个方法比较长,这里就不贴代码了,说一下里面的关键步骤

  1. 首先判断当前连接上还能不能接受新的Stream(超过最大的限制,或者 noNewStreams被设定为true)
  2. 如果当前连接的route 的地址和请求的Address不匹配直接返回false,在okhttp 中,Address的比较考虑了很多的因素,这里就不展开了,同时在比的过程并没有对比host这个关键的部分。
  3. 如果这个时候请求的host相等,那么认为当前的连接是可以发送这个请求的
  4. 到这里为止,请求的地址在先有的连接中并没有找到,但是这个时候任然可以将请求合并,在源码的注释当中也给出了这个理论的说明。
  5. 首先这个链接上实现的必须是http2 协议
  6. 先有链接的ip和请求的ip必须是同一个ip,因为这个限制,所以必须有了DNS的信息之后我们才能知道ip信息,同时这也要求这个连接是没有代理的,因为在经过代理之后是不知道源服务器的地址的。
  7. 如果是https请求,证书锁必须匹配主机,这个实际上是指需要支持通配符证书。

如果当前的连接是可以用的,进入到streamAllocation 的 acquire方法当中,而这个方法将streamAllocation的connection 对象指向了刚才获取的连接。

如果当前的连接池当中没有找到一个合适的连接,返回null.

这个时候方法的调用回到 findConnection 当中,

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
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new streams.
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
if (result == null) {
// Attempt to get a connection from the pool.
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
return result;
}

这个时候如果当前的Connection 不是null ,说明ConnectionPool的get方法找到了可用的连接,那么这个时候可以直接返回,否则继续往下走。

在创建一个新的连接的时候,有几个事情是需要决定的,首先连接到一个域名的时候,DNS可能返回了多个指向这个地址的IP ,同时这个地址还有使用了代理,所以这个时候需要选择一个连接的地址,接下来的逻辑就是在选择连接对象。

在选择连接对象中,Route这个对象核心,所有的逻辑都是为围绕这儿类来进行的,这里就不详细说里面的代码了。

在获取到了具体的Route之后,在从连接池里面寻找有没有可用的连接。继续调用ConnectionPool的get方法。

如果找到了就直接返回,这个可以复用的连接。

如果这个时候还没有找到可以复用的连接,那么这个时候直接先创建一个RealConnection 对象。

这个时候只是表示连接的对象被创建了,需要开始进行connect操作。

1
2
3
// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);

这个里面就是具体的连接TCP连接操作了,先看第一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}

首先在这个方法的前面需要获取对应Address的请求ConnectionSpec,ConnectionSpec指定了在socket之上的HTTP流的配置,比如在使用https的时候,使用的TLS版本和cipher suites(密码套件),这个ConnectionSpec列表是在构建okhttpClient的时候使用的,默认的情况下它包含了两个配置项,ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTET,这两个对象分别表示了TLS的1.0 - 1.3的四个版本和未加密的明文。

ConnectionSpecSelector这个类主要负责connection spec 的降级机制,

接下来的这部分代码就是在作请求配置的验证,如果这个不是一个https的请求,但是这个请求当中又不包含ConnectionSpec.CLEARTEXT,这个可以发送明文请求的配置,那么这个时候抛出一个Runtime异常。下一步调用的isCleartextTrafficPermitted一直都是返回true,那么这里实际上需要根据的自己的需要来配置。

在完成了http请求配置的验证之后,下面的代码开始建立一个socket连接

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
while (true) {
try {
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
....
eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
.....
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}

首先是requiresTunnel的理解,当使用https通过http代理服务时,http代理会认为这是一个安全https链接,将会放行https封包,所以我们可以使用https来欺骗http代理服务器,而我们自行构建的https封包中则可以封装我们自定义协议的真实数据(因为https是二进制的)。这一切让http代理看起来就像是我们在进行安全http连接一样。

简单的说一下connectTunnel中的步骤

1) 首先创建一个连接到代理服务器的Request 对象。

2)创建并且建立到代理服务器的socket 请求(调用connectSocket方法)

3)createTunnel 向代理服务器发送一个Connect请求,然后对响应进行判断,如果成功的和目标服务器建立了连接,那么这个时候connect回返回200,还有一种可能是代理服务器反悔了401,即未授权。

然后,如果这个时候是不需要http 隧道的话,就可以调用connectSocket方法直接创建一个的socket连接了。

再接下来建立协议进入 establishProtocol 方法,这个里面主要包含了https 和 http2 协议的相关设定。

当这个这个请求是一个非https的请求的时候,那么一旦socket连接建立完成,那就可以直接返回了,如果是一个https的请求,那么这个时候就要开始TLS的握手过程了。这个握手的过程在RealConnection 的connectTls 方法当中.

  1. 方法内存首先开始配置socket的密码套件,TLS版本和TLS扩展,关于TLS扩展可以查看博客

  2. 调用sslsocket.startHandShake 开始握手(博客),具体握手流程如下

    a) 客户端发送被称为ClientHello的信息给服务器端,其中包括了TLS的版本,支持的加密算法和一个随机生成的字符串。

    b) 服务器端接收到客户端发送的ClientHello之后,也向客户端发送一个被称为ServerHello的信息,其中包括了选择的加密算法,一个随机字符串,服务器端的证书,还有选择的TLS版本,如果客户端支持的版本和服务器支持的版本不一致则关闭加密通信。

    c)客户端收到服务器的消息之后开始对服务器端的消息进行验证,包括证书中的域名和实际的域名是否匹配,证书是否过期和证书是不是由可靠机构办法,否则向用户给出警告,由用户决定是否继续进行。完成校验之后,服务器端回复如下信息:由服务器公钥加密的随机串(保证安全); 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送;客户端握手结束通知,表示客户端的握手已经结束了,这一项内存还是前面所有消息的hashcode ,给服务器端验证。

    4)服务器端接收到客户端消息,向客户端发送消息编码改变的通知,随后的消息都将采用约定的秘钥和加密方法进行,同时还有服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

  3. 从sslSocketSession中创建一个Handshake对象,这个对象里面包括了这次握手的基本信息,TLS版本,加密套件,本地发送的证书,和从服务器端接收到的证书。

  4. 然后调用HostnameVerifier 的verify 验证主机地址是否在证书的接收范围之内,如果不在的话会抛出一个SSLPeerUnverifiedException,https连接验证失败。在okhttp中,HostnameVerifier中返回的是 OkHostnameVerifier的一个实例。

  5. 接下来是HPKP的验证,这种验证也是一种防止中间人攻击的验证(博客),如果验证是失败同样会抛出一个SSLPeerUnverifiedException异常。

  6. 如果以上步骤都没有出现问题,那么这一次的handshake就完成了。

那么一直走到这里,一个socket链接的建立就算是完成了,其中包含了很多实现的细节和各种需要考虑的情况。

这个时候一步步返回的话最终回到了StreamAllocation的newStream 方法当中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
......
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}

在找到了resultConnection 之后,通过RealConnection的 newCodec 方法来构建了一个HttpCodec,在这个方法里面判断了一个下当前的协议的版本,http1 还是http2,然后返回了不同的实现。

再接着从调用栈里面向上返回到了ConnectInterceptorConnectInterceptor 的intercept方法。

1
2
3
4
5
6
7
8
9
10
11
12
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

获取了connection 之后,最终使用Chain来调用下一个拦截器。

接下需要执行的是execute这个方法,在RealCall的实现中,这个方法的代码十分简短,但是其中的调用链却是非常的复杂,先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}

在上面已经说过了,一个调用只能执行一次,所以在这个方法前面,首先就是判断是否已经执行过。

captureCallStackTrace(); 可以暂时先略过

接下来就是事件监听者的调用了,在第一部分中我们说明了,可以监听这个请求的各个阶段,在这里这个请求就正式开始调用了,所以调用了 callStart 这个方法。

在第一部分中说过,请求的调度是通过dispatcher这个部分来完成,但是在Dispatcher 的文档中说了这个对象是异步请求的调度策略,所以对于同步请求来说,只是这个对象放到了一个 Deque (双向队列)集合当中,真正的执行还不在这一步。

1
2
3
4
/** Used by {@code Call#execute} to signal it is in-flight. */
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}

接下来就是 getResponseWithInterceptorChain 这个方法,看到这个方法名字就知道正如第一部分所说义,所有的请求都是在这个方法中完成的,先看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}

在这个方法里面通过配置了一系列的拦截器,通过这些拦截器最终完成了一个请求,再看这些拦截器之前,先看下Chain是怎么调用这系列的拦截器的。

Chain 这个接口的真正实现类是 RealInterceptorChain, 其中核心的实现方法如下。

1
2
3
4
5
6
7
8
9
10
11
12
....
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
...
return response;

其中方法的核心就在于方法的处理过程中又创建了一个拦截器来处理请求,这个时候讲index加了1 ,所以这个时候请求是直接传递了下个拦截器处理。

这里找个拦截器的代码看下,在RetryAndFollowUpInterceptor 的intercept方法中,

1
2
3
4
5
6
7
8
9
10
try {
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
...
} catch (IOException e) {
...
} finally {
...
}

可以看到其中对请求的处理还是调用了Chain的 proceed方法,通过这样一个链式的调用,最终完成了对请求链的调用。

回到 RealCall 中的getResponseWithInterceptorChain方法,那么实际上最先处理请求的反而是 CallServerInterceptor 这个拦截器了。

1
2
3
4
5
6
7
8
9
10
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));

接下来我们就来分析这一个个的拦截器,来看一个请求到底是怎么完成的。

按照刚才我们说的最后添加的拦截器反而是最后执行的,那么这个时候应该是 CallServerInterceptor这个拦截器。

这个时候方法的执行我们分几部分执行

请求头的写入

1
2
3
4
5
6
7
8
9
10
11
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);

这里使用前文提到的HttpCodec 完成http请求的编码,非常简单就是对http请求的编码和对响应的解码,其中包含的方法可以去看其中的方法,这里就不贴出来了,跟上面代码相关的是 writeRequestHeaders 这个方法,表示对请求头部的写入。

在代码的分析中只分析Http1Codec.

1
2
3
String requestLine = RequestLine.get(
request, streamAllocation.connection().route().proxy().type());
writeRequest(request.headers(), requestLine);

先获取请求行,然后在写具体的请求头部.

请求体的写入

部分请求方法需要携带请求体,那么在写完请求头部之后,就开始队请求体的处理。

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
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
streamAllocation.noNewStreams();
}
}

请求的读取

在完成了请求发送之后就是请求的读取了,在读取的时候如果发现100 这种临时响应,则需要再次读取一个响应。

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
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
realChain.eventListener()
.responseHeadersEnd(realChain.call(), response);
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;

到这里就已经完成了从数据发送到响应读取的过程。

首先是添加 client.interceptors(),在示例代码中,并没有配置拦截器,所以这个集合当中什么都没有,但是在项目中,可以通过自己自定义的拦截器来实现对数据的一些个性化处理,比如在开发阶段通过自定义的拦截来实现对请求和响应的日志输出。

接下来是 retryAndFollowUpInterceptor 这个拦截器,这个类的定义如下,

1
2
3
4
5
**
* This interceptor recovers from failures and follows redirects as necessary. It may throw an
* {@link IOException} if the call was canceled.
*/
public final class RetryAndFollowUpInterceptor implements Interceptor

这个方法是用在在错误处理和重定向当中发挥作用的,大概的看一下其中interecept方法的实现

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
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
....
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
......
Request followUp = followUpRequest(response, streamAllocation.route());
.....
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
.......
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
}

上面的代码主要分为了两个部分,首先判断请求有没有出现错误,并且判断这个错误是不是可以恢复的,其中细节可以看一下,如果是可以恢复的,那么就继续请求的发送处理,如果是不能处理的那么直接抛出异常。

如果请求没有出现错误,那么接下来会对响应进行处理,判断是否需要重定向等等一系列操作,在 followUpRequest 这个方法当中对各种请求码都进行了处理,返回了一个接下来需要进行的请求。

下面是抽取的其中一段代码,这一段对3xx一系列的请求进行了处理

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
// Does the client allow redirects?
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// Don't follow redirects to unsupported protocols.
if (url == null) return null;
// If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();

由于在RetryAndFollowUpInterceptor 这个类中,对于请求的处理包含在一个whlie循环里面,所以不断的重复重复,直到得到一个正确的响应或者超过了最大的重定向请求次数,或者其他的条件不满足之后才会退出循环。

通过OpenGL优化WEBP动画的渲染

发表于 2018-03-18

前言

在目前的项目中我们使用到了WEBP作为直播动画的方案,使用WEBP的优点就不用说了,缺点我觉得有一个就是Android上目前能够解码webp动画的库只有FB的 Fresco 库,所以目前只能使用这个库作为webp动画播放的库。

既然是直播比较蛋疼的问题就是目前手机屏幕上出现的各种元素会比较多,所以对于UI上的优化就显得比较重要了,在目前我们的项目中,在一个屏幕上用户可以发弹幕,点赞,发送礼物,而且还是在播放器界面,整个界面的元素相当复杂,所以这个时候每一个操作的优化都显得十分重要,不然就会造成UI线程的卡顿,影响用户体验。因此在这里的用户直播礼物的优化就会显得十分重要。但是在Freso库默认提供的 SimpleDraweeView 是继承于ImageView的,这也就说明这个库的渲染是在主线程当中完成的,那么这就提供了优化的空间,我们可以使用OpenGL在非主线程中来完成礼物的渲染。

自定义View

在Freso的使用说明中可以查找到如何使用自定义的View. 既然我们需要使用OpenGL,那么我们需要继承GLSurfaceView 这个类,这个类具体的使用说明可以自行百度,继承之后按照文档里面相关的说明完成一系列方法的重写。

接下来,按照Fresco的教程定义完成View之后,就需要完成OpenGL 当中render的 编写了。代码的核心就是完成onDrawFrame的编写。核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void onDrawFrame(GL10 gl) {
cacheBitmap.eraseColor(Color.TRANSPARENT);
Canvas canvas = new Canvas(cacheBitmap);
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BITS);
texture = Util.loadTexture(cacheBitmap, texture, false);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, position.length,
GLES20.GL_UNSIGNED_SHORT, shortBuffer);
}

其中的主要逻辑梳理一下,在自定义的View当中我们可以通过 mDraweeHolder.getTopLevelDrawable() 拿到当前需要绘制的Drawable对象,但是在OpenGL中,Drawable对象不管用,我们只能将其转为一个Bitmap 对象,所以需要创建一个Canvas,通过drawable .draw 方法来得到其中的图像数据。拿到bitmap之后将其转为opengl中的纹理对象即可。

如果你完全按照Fresco中的代码来定义View, 运行上诉的代码会直接崩溃。

从代码的异常堆栈里面可以看出看我们在WebpGLView这个类中导致了 invalidateDrawable 这个方法的调用,这个方法定义在Drawable.Callback当中

1
2
3
4
5
6
7
8
/**
* Called when the drawable needs to be redrawn. A view at this point
* should invalidate itself (or at least the part of itself where the
* drawable appears).
*
* @param who The drawable that is requesting the update.
*/
void invalidateDrawable(@NonNull Drawable who);

从注释中我们可以看到这个方法是在Drawable出现更新的时候view应该更新自己。

而在Fresco的教程当中也需要我们调用

1
mDraweeHolder.getTopLevelDrawable().setCallback(this);

这行代码,这里之所以能够直接使用this, 就是因为View默认实现了这个接口,我们再来看下View当中是怎么实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Invalidates the specified Drawable.
*
* @param drawable the drawable to invalidate
*/
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
}

从View实现的代码中我们可以看出来,View调用了 invalidate 方法来实现View的更新。

在我们上线的代码中我们在一个OpenGL线程中调用draw(canvas)最终导致invalidateDrawable 的调用,从而在非主线程调用了invalidate,所以出现了上面的崩溃。

所以为了避免这种情况我们只需自己重写这个方法即可,重写十分简单,直接调用requestRender() 即可。

所有的代码都在LNeway上可以找到。

深入理解计算机系统学习笔记

发表于 2017-11-28

#导论#

程序编译的过程

  1. 预处理阶段,将include包含的内容直接插入到文本中间。
  2. 编译阶段:将程序转为汇编代码编写的程序、
  3. 汇编阶段:将汇编代码转为机器指令,输出目标文件、
  4. 链接阶段:负责将所引用库中已经生成好的目标文件合并到汇编阶段所产生的目标文件中,最后生成可执行目标文件。

设备的存储级别
寄存器-》L1,L2,L3缓存-》内存 -》磁盘-》分布式文件系统
从下到上提供缓存作用

虚拟内存的地址空间

程序代码地址 -》程序运行时堆 -》 共享库地址-》栈-》内核地址空间

对于所有代码来说,代码都是从一个固定地址开始。

并发:
线程级别并发,利用现代CPU的超线程技术,一个CPU核心可以同时执行两个线程。

指令级别并发:
现在处理器上,处理器可以同时执行多条指令

单指令,多数据并行:允许一条指令,产生多个可以并行的操作。

抽象:
文件是对I/O的抽象,虚拟存储是对I/O和内存的抽象,进程是对I/O,内存,处理器的抽象。

#信息的表示处理#

在C/C++中,指针实际上指向了一个存储块的虚拟地址的第一个字节,使用指针的时候,指针实际上包含了指向地址的长度。例如在int*指针中,指针所指向地址的长度是4,我们可以将指针的类型的改为char* 这样,我们可以访问这个int中的每一个字节。

字长是单位是字节,32位的电脑是指2^32字节。

##字节序##
大端法:高位有效字节在前面。
小端法:地位有效字节在前面。

计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

##位操作##
基本的位操作:异,或,与,左移,右移
逻辑右移:左边的高位,补0.
算数右移:左边的高位补上k个(右移k位)最高的有效位。(目前基本上都是采用的算术右移)

【Java当中】当移动的位数k很大的时候,通常会通过 k % sizeof(type) 之后 再来进行真正的移动,比如int 类型的左移36,那么实际上真正移动的只有4位 (36 % 32)。

【在部分c/c++中】当对一个数进行左移的时候,如果超过了这个数的字长,那么这个数将会变成零, 例如 32位的int位移大于等32的时候会变成零。

在C++中,对任何一个非零的数执行 ! 运算都是1。

##整数的表示##

  1. C/C++中,数据支持有符号和无符号的,但是在Java中只支持有符号的数
  2. 当有符号数和无符号数之间发生转换的时候,从有符号数转为无符号数,如果是正数那么实际上的数值还是原来的数值,如果是负数,转为无符号的数之后实际上只是对位的解释方式不同了,有符号的数之前是最高有效位是负权值也就是(-2^(w-1)),转为正数之后实际上由负权值便为了正权值,所以两者时间的值实际上是相差了(2 * 2^(w-1) = 2 ^ w),所以在强转之后,可以通过很简单的公式来计算强制转换的结果。
    ​

    static void testSigned() {
        unsigned char u =  0;
        u = -1;
        cout<<hex<<(u - 0)<<endl;
    }
    

    上面的结果会输出 ff 也就是 2^8 + (u),

  3. 在C/C++语言中,当无符号数和有符号数一起运算的时候,会先将有符号数转为无符号数,然后再开始运算,在计算的过程中应该注意此类陷阱。
    ​

    static void testSignedOperation() {
        cout<<(-1<0u)<<endl; // 输出 false;以为 0u是无符号数
    }
    

扩展数字的位表示(向上数据类型的转换)
扩展一个数位的表示就是通常我们在代码里面写的对基本数据类型的向上转换,对于一个无符号的数,只需要在数据位钱补零就行,但是对于一个有符号的数字来说,则需要补充最高的有效位。

截断数字的表示(向下的数据类型转换)
截断数字的表示通常会丢掉k个高位。对于无符号的数字可以直接按照剩下的bit串来解释数字,将其转为对应的数字表示即可,但是对于有符号的数字,我们还需要注意其表示范围,例如对于int 53191,转为short之后,因为是有符号的类型,所以剩下的位串最终会被解释为-12345

运算溢出
当两个数进行运算后,如果出现了溢出的情况,这个时候需要根据两个数的符号类型(有符号还是无符号),对截断后的位串重新进行解释。例如在4位情况下,-8 + (-7) = -15,位串表示为 10001,这个时候需要截取位串,去掉头部的1, 剩下 0001,所以结果为1.

整数的运算
乘法:整数的乘法通常会比加法耗费更多的时间的时间周期,所以通常可以把乘法拆分成位移运算和加法运算的结合。
​
例如x 3, 我们可以将3 拆分成 2^1 + 2^0,
x
3 = x (2^1 + 2^0) = x 2 + x * 1 = x << 1 + x << 0

除法:对于程序中的除法,所有的结果都是向零取整(意味着对于一个结果大于零的数字,这个值是向下取整,但是对于一个小于零的结果,这个值是向上取整)

-5 / 2 = -2;(结果小于零,向上取整)
5 / 2 = 2; (结果小于零,向下取整)

浮点数

  1. 浮点数的计算不满足结合律,分配律,所以在计算的时候需要额外考虑一点。
  2. 浮点数的表示为 V = s E M, (符号位 阶码 尾数)

#程序的机器级表示#

Linux使用平坦寻址的方式,使程序员将存储空间看做一个巨大数组。

机器级代码

  1. 对于机器级编程来说,两种抽象至关重要,第一种是机器级程序的格式和行为,定义为指令集体系结构(ISA), 它定义了处理器状态,指令的格式,以及每条指令对状态的影响。 第二种抽象是机器级程序使用的地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。

  2. 程序存储器包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器块。

  3. 程序链接之后,所有变量在内存中的地址就确定了。

数据格式

  1. 字 16位, 字节 8位,双字 32 位。

  2. 三类操作符: 第一种是立即数,用$ 加上 一个C表示的常数,例如movl $66, %eax, 就是将常数66放入寄存器eax中。第二种操作的对象是寄存器, movl %edx, %eax,例如就是将寄存器edx中的值拷贝到eax,第三种操作的对象是存储器的引用(这里的引用可以改成索引), movl (%edx), %eax, 假设内存数组为M, 这里movl 操作的对象是M[%edx] 这个地址。

  3. 在内存中程序所使用的地址是向下增长的。

  4. 数据传送指令

    数据传输指令S,D 不能同时指向存储寄存器。

  5. %esp寄存器指向的地址总是栈顶,而在栈顶之外的任何数据都认为是无效的。

  6. 算术和逻辑操作

leal 指令的第一个操作数看上去是一个存储器引用,但是该指令并不从指定的位置读取数据,而是将有效地址写入目的操作数。

  1. 特殊的算术操作

IA32 中提供了两种不同的乘法操作imull 和 mull,这两种指令都要求有一个参数存放在%eax寄存器中,而另一个操作数作为源操作数给出,然后乘积存放在%edx (高32位) 和 %eax(低32位),虽然imull有两种用法,但是汇编器可以通过计算操作数的数目来辨别应该使用那种命令。

  1. 条件码寄存器:CPU维护一组条件码寄存器,他们描述了最近算术和逻辑运算的属性,可以检测这些寄存器来执行条件分支指令。
  • CF: 进位标志,表示最近的操作使最高位产生了进位
  • ZF: 零标志,表示最近操作得出的结果为零。
  • SF: 符号标志,表示最近得到的操作结果为负数
  • OF: 溢出标志,表示最近的操作导致一个补码溢出。

OpenGL 片段着色器理解

发表于 2017-11-22

最新在做使用opengl 来实现图片的液化效果,从而为静态图片添加动态的效果。

可以通过一句话来总结一下实现的原理:通过片段着色器来来计算每一个像素点应该显示的颜色在纹理上的位置,从而实现修改渲染像素颜色的功能。

实现可以看下面的函数:

​ vec2 curveStretchFun(vec2 curCoord, vec2 originPosition, float radius)

{

float currentDistance = distance(curCoord, originPosition);

if (currentDistance < radius) {

​ vec2 currentDirection = (curCoord - originPosition) / length(curCoord - originPosition);

​ float angle = dot(currentDirection, vec2(1, 0));

​ float value45 = sin(-90.0);

​ float value135 = sin(90.0);

​ if (angle > value45 && angle < value135 && currentDirection.y > 0.0) {

​ curCoord = originPosition;

​ return curCoord ;

​ } else {

​ return curCoord - vec2(0, 0);

​ }

} else {

​ return curCoord - vec2(0, 0);

}

这里传进来的curCoord 实际上是当前像素点的坐标,我们通过计算originPosition(液化作用的起始点)和当前坐标的距离判断当前像素点是否在作用范围之内,如果不在,那么就不会修改当前像素的坐标,(通过修改坐标来影响在纹理上取像素最终修改当前坐标显示的颜色)。在这个简单的方法中,我们通过吧一个范围的所有颜色全部显示为originPosition 在纹理上对应的颜色。

Android实时滤镜实现原理

发表于 2017-10-23

##原理##

在Android中,目前可以使用Opengl 来实时的预览相机界面,通过这相机的预览界面我们就可以实现滤镜的基本功能。

使用OpenGL 来实现相机的预览界面网上的代码很多,这里就不说了,主要记录一下使用OpenGL实现滤镜的原理。

在相机的预览界面中,我们通常都是相机的所采集到的数据直接渲染到了屏幕上,而我们渲染的屏幕实际上是系统默认提供的一个叫帧缓冲的地方,英文为Frame Buffer Object (FBO) , 既然系统提供了一个默认的帧缓冲对象,那么我们也可以创建一个我们自己的帧缓冲对象,将数据绘制到我们创建的帧缓冲对象上。关于FBO的相关概念,可以参见这篇博客。

通过我们自己创建的帧缓冲对象,可以附加一个或者多个纹理在这个帧缓冲区当中,最终渲染到这个Frame buffer中的数据实际上都是渲染到了纹理图像当中,既然有了这个纹理图像,那么就可以通过Fragmetn shader来实现各种滤镜的效果。

##实现及思考##

下面贴一下关键的代码

绘制相机预览界面的代码,在这段代码中,把相机的数据绘制到了自己创建的FBO对象当中。

if (fbo == null) {             
    fbo = FBO.createFBO(width, height);
}

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo.framebufferID);

GLES20.glViewport(0, 0, width, height);
GLES20.glClearColor(0, 0, 0, 0);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

GLES20.glUseProgram(mProgram);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);

// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
beautyPosition = GLES20.glGetUniformLocation(mProgram, "beauty");

GLES20.glUniform1i(beautyPosition,  isBeauty ? 1 : 0);

// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);

// Prepare the <insert shape here> coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);

mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
GLES20.glEnableVertexAttribArray(mTextureCoordHandle);


GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureVerticesBuffer);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTextureCoordHandle);

// Switch to the default frame buffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);`

创建FBO

public class FBO {
       public int textureID;
    public int renderbuffersID;
    public int framebufferID;

    public static FBO createFBO(int width, int height) {
        FBO  fbo = new FBO();
        int[] texture = new int[1];
        int[] renderbuffersID = new int[1];
        int[] framebufferID = new int[1];

        GLES20.glGenTextures(1, texture, 0);
        GLES20.glGenFramebuffers(1, framebufferID, 0);
        GLES20.glGenRenderbuffers(1, renderbuffersID, 0);

        // renderbuffer
        GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, renderbuffersID[0]);
        GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);

        // framebuffer
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebufferID[0]);
        GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER,
                framebufferID[0]);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,
                GLES20.GL_RGBA, width, height, 0,
                GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
                null);

        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,
                GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D,
                texture[0], 0);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

        fbo.textureID = texture[0];
        fbo.framebufferID = framebufferID[0];
        fbo.renderbuffersID = framebufferID[0];

        return fbo;
    }
}

滤镜处理

在这一步就是将前面附加的纹理作为纹理参数传递到Fragment shader当中,然后可以实现滤镜的效果了。

Fragment shader代码

precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;

void main() {
    vec4 col = texture2D(inputImageTexture, textureCoordinate);
    float h = dot(col.rgb, vec3(0.3, 0.59, 0.21));
    gl_FragColor = vec4(h, h, h, col.a);
}

滤镜代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void draw(int texture) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the <insert shape here> coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);
mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureVerticesBuffer);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
}

思考

实际上在自己实现的过程中发现应该是可以直接避免到自己创建FBO这个对象来实现简单的滤镜的效果,但是这样的话各种滤镜选择的逻辑的可能都会存在一个类当中,而且在这个绘制对应的Fragment Shader 也会存在这种需要变量和属性,但是对应的属性和变量又可能只在一个对象中用到,这就造成了部分代码文件及其臃肿,以至于根本无法扩展,而通过FBO 的方式,这样相当于形成了一个流水线的方式,我们只要拿到上一个步骤最终绘制的纹理,就可以继续拼接下一步操作,这样在整个流程中插入想要的步骤就十分容易了。

Replugin 学习笔记

发表于 2017-09-04

在Replugin 这个框架中,360实现了只需要hook一个点,就可实现插件化加载,而这个点就是classloader。为什么hook这个点就能解决Activity 动态加载的问题,这个就要从我们的Activity是怎么来的说起了,Activity的启动代码相当复杂,在这里就不分析了,网上有不少大神的blog都分析了这个问题,其中跟hook点最相关的就是下面的这一段。
这一段从ActivityThread的 performLaunchActivity 方法中摘出,

Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }

进入mInstrumentation 的 newActivity 方法:

public Activity newActivity(ClassLoader cl, String className,
        Intent intent)
        throws InstantiationException, IllegalAccessException,
        ClassNotFoundException {
    return (Activity)cl.loadClass(className).newInstance();
}

看到这个方法是不是就有一种恍然大悟的感觉了,最终的Activity 实例是通过其 Activity 的class对象调用newInstance()得到的. 所以只需要控制了 Activity的class 对象, 就控制了Activity的产生,而这个class对象,正是我们可以通过hooK来解决的。

在这里Relugin 框架解决了Activity 怎么来的问题,同时因为Activity需要在AndroidManifest中注册,所以在使用Replugin 框架的时候,框架的插件会在生成的Apk文件中添加很多的Activity组件来作为一个个占坑,比如当我们启动的一个Activity A 的时候,Relugin 会将我们的目标Activity替换为坑位中的一个Activity AP ,但是在实际返回这个Activity目标class 对象的时候,返回的却是Activity A 的class 对象。

走完这个流程,Replugin 的核心原理应该就算是掌握了,虽然说得这么简单,但是其中每个步骤都相当的复杂,继续学习吧。

具体实现可以看下demo

Android OpenGL lookAt 函数

发表于 2017-09-02

Android OpenGL 3D中的LookAt的理解

在opengl中的坐标空间有几种, 物体坐标,世界坐标系,相机坐标系,裁剪空间坐标系。

其他的几种坐标系相对好理解,这个相机坐标系反复试验才明白其中含义。

先来看一下安卓中GLES20中的lookatM函数。

1
Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);

在文档中给出的注释如下

Defines a viewing transformation in terms of an eye point, a center of view, and an up vector.

看着这个注释还是没理解。

函数中的的参数前两个可以忽略,后面的九个参数分为三组

第一组是眼睛在空间的坐标

这个就是相机在一个空间直角坐标系中的绝对位置

第二组是物体在空间中的坐标

这个坐标就是我们看的物体在空间坐标系中的位置。

第三组坐标实际上是相机顶部的的指向

这里第三组参数决定了📷顶部方向,大家使用这个参数的时候可以想象一下我们自己的眼睛看东西的时候,实际上这个参数的值是(0, 1, 0),因为把我们自己的头想象称为一个相机,那么这个相机顶部实际上永远指向的天空(假设天空是正轴方向),现在假设我们看屏幕的时候,想把屏幕倒着看,除了不旋转屏幕,我们还有一个选项就是让自己的头顶朝地,这样我们看屏幕的时候就可以实现倒着看了,那么这个时候向量的值应该是(0, -1, 0)。

具体的话大家可以看下面的例子,下面的实例中我们的物体始终都在(0, 0, 0)这个位置不变。

第一组数据是我们眼睛的位置,目前参数值在(-2,-2,2),也就是从立方体的一个顶点的正上方看过去的,现在(upX, upY, upZ)这组参数设定的值是(0, 1, 0), 紫红色是在图形下面的位置,按照上面的解释,如果把upY的值改为-1,表示相机顶部是向下了,那么这个时候最终看到的图形,紫红色是在图像的上部。

注意

  1. 这个参数的设定不能和视线平行,在这个例子当中我们的向量是(-2,-2,-2),如果设定这个up值是x * (-2, -2, -2), 那么将看不到任何图像。
  2. 这个值得含义是表示顶点指向的方向,所以最终含义是在方向这个意思上面,所以(0, -1, 0)和 (0, -x, 0) (x 的取值大于0) 的含义是一样的,最终看到的图像也就没有区别了。

Andorid OpenGL 多层纹理

发表于 2017-08-27

在opengl 可以使用多个纹理单元,从而实现了使用多成纹理渲染,在使用的时候也需要绑定多个纹理单元,然后在GLSL中使用多个纹理绘制即可。

话不多少直接上代码

public class TextureRender implements GLSurfaceView.Renderer {

private Context context;

private int program;
private final float vertext [] = {
        1, 1, 0,   // top right
        -1, 1, 0,  // top left
        -1, -1, 0, // bottom left
        1, -1, 0,  // bottom right
};

private final short position [] = {0, 1, 2, 2, 3, 0};


private FloatBuffer verextBuffer = null;
private ShortBuffer shortBuffer = null;
private FloatBuffer texBuffer;

private int v_position;
private int tex_position;
private int texture;
private int textureUniform;
private int matrix;
private int watermark;
private int watermarkUniform;

private float texure [] = {
        0f, 0f,
        1f, 0f,
        1f, 1f,
        0f, 1f };

public TextureRender(Context context){
    this.context =  context;
    verextBuffer = ByteBuffer.allocateDirect(vertext.length * 4)
            .order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertext);
    verextBuffer.position(0);

    shortBuffer = ByteBuffer.allocateDirect(position.length * 2)
            .order(ByteOrder.nativeOrder()).asShortBuffer().put(position);
    shortBuffer.position(0);

    texBuffer = ByteBuffer.allocateDirect(texure.length * 4)
            .order(ByteOrder.nativeOrder()).asFloatBuffer().put(texure);
    texBuffer.position(0);
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    program = GLES20.glCreateProgram();
    int vertex = Util.createShade(GLES20.GL_VERTEX_SHADER,
            Util.readTextResourceFromRaw(context, R.raw.texture_vertex));
    int fragment = Util.createShade(GLES20.GL_FRAGMENT_SHADER,
            Util.readTextResourceFromRaw(context, R.raw.texture_fragment));

    GLES20.glAttachShader(program, vertex);
    GLES20.glAttachShader(program, fragment);
    GLES20.glLinkProgram(program);
    GLES20.glUseProgram(program);

    v_position = GLES20.glGetAttribLocation(program, "aPos");
    GLES20.glEnableVertexAttribArray(v_position);
    GLES20.glVertexAttribPointer(v_position, 3, GLES20.GL_FLOAT, false, 12, verextBuffer);

    tex_position = GLES20.glGetAttribLocation(program, "v_texCoord");
    GLES20.glEnableVertexAttribArray(tex_position);
    GLES20.glVertexAttribPointer(tex_position, 2, GLES20.GL_FLOAT, false, 0, texBuffer);

    textureUniform = GLES20.glGetUniformLocation(program, "u_samplerTexture");
    GLES20.glUniform1i(textureUniform, 0);

    watermarkUniform = GLES20.glGetUniformLocation(program, "watermark");
    GLES20.glUniform1i(watermarkUniform, 1);


    try {
        texture = Util.createTexture(BitmapFactory.decodeStream(context.getAssets().open("texture.png")));
        watermark = Util.createTexture(BitmapFactory.decodeStream(context.getAssets().open("watermark.png")));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    //设置相机位置
}

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
    GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, watermark);

    // 用 glDrawElements 来绘制,mVertexIndexBuffer 指定了顶点绘制顺序
    GLES20.glDrawElements(GLES20.GL_TRIANGLES, position.length,
            GLES20.GL_UNSIGNED_SHORT, shortBuffer);
}}

下面是createTexture方法

public static int createTexture(Bitmap bitmap) {
    int[] texture = new int[1];
    if (bitmap != null && !bitmap.isRecycled()) {
        // 生成纹理
        GLES20.glGenTextures(1, texture, 0);
        // 生成纹理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
        return texture[0];
    }

    return 0;
}

顶点着色器

attribute vec3 aPos;
attribute vec2 v_texCoord;
varying vec2 outTexCoord;
void main() {
    gl_Position =  vec4(aPos, 1.0);
    outTexCoord = v_texCoord;
}

片段着色器

precision mediump float;
varying vec2 outTexCoord;
uniform sampler2D u_samplerTexture;
uniform sampler2D watermark;
void main(){
 gl_FragColor = mix(texture2D(u_samplerTexture, outTexCoord) * vec4(0, 1, 0, 1),           texture2D(watermark, vec2(1.0 - outTexCoord.x, outTexCoord.y)), 0.2);
}

性能优化

发表于 2017-08-20

最近在做一个相机的的APP,其中涉及到了很多以前没有接触过的知识,APP做到现在功能也都基本完成了,现在有心思把精力放在一些以前比较奇怪的代码上。

内存优化

在目前的实现中,相机的预览页采用了open GL 的方式来展示摄像头采集的数据,但是在根据人脸识别后的添加的sticker上还是采用了Bitmap和 canvas 实现的方式,看到了bitmap就知道内存抖动这块一定是跟它有关了

  1. 问题一:sticker 资源的加载
    在优化前实现中,采用的是通过glide同步的获取bitmap对象,同时通过一个队列来缓存加载的图片,但是这样面对的一个问题是在一个动态的sticker中,如果出现的sticker过多,那么很可能造成这个队列不够用,那么实际上造成的效果是bitmap对象根本没有办法复用。例如实际上这个队列的最大容量是 10, 但是我们有40张图片需要加载的时候,这个队列根本起不到缓存的作用,并且在sticker较大的时候还是比较大的OOM 风险。

1.1 减少bitmap.
首先切换了思路,我们并没有采用队列的方式来缓存所有的图片,而是针对每一个类型的sticker资源都只缓存2个bitmap,这样就内存里面关于sticker的bitmap数量最大为10个(有些类型的sticker只有一个图片资源),这样大大的减少了sticker的内存占用,在这种情况下,也基本上不会出现OOM 的风险,虽然这个时候可能会出现sticker加载慢导致的sticker掉帧情况,但是实际上这个对视觉上的影响并不大,同时加载本地资源的时候,sticker加载的速度还是很快的,并且因为在子线程执行所以这个优化方法是可以采纳的。

1.2 使用option.inBitmap 和 option.inMutable

option.inBitmap 是ANDROID 11 之后推出的属性,通过这个属性在加载bitmap时候可以复用inBItmap 所指向对象的内存而不用重新开辟内存的占用,这个属性在不同的sdk基本上有不同的限制要求,使用的时候需要注意,因为我们的同类型的sticker序列的图片大小都是相同的,所以在使用的时候不用担心这个限制问题。

option.inMutable 通过这个属性我们可以让每次返回的bitmap都是之前的同一个对象

这里引用一下一篇博客的内容

inBitmap
Android在API11添加的属性,用于重用已有的Bitmap,这样可以减少内存的分配与回收,提高性能。但是使用该属性存在很多限制: 在API19及以上,存在两个限制条件:
被复用的Bitmap必须是Mutable。违反此限制,不会抛出异常,且会返回新申请内存的Bitmap。
被复用的Bitmap的内存大小(通过Bitmap.getAllocationByteCount方法获得,API19及以上才有)必须大于等于被加载的Bitmap的内存大小。违反此限制,将会导致复用失败,抛出异常IllegalArgumentException(Problem decoding into existing bitmap)
在API11 ~ API19之间,还存在额外的限制:
被复用的Bitmap的宽高必须等于被加载的Bitmap的原始宽高。(注意这里是指原始宽高,即没进行缩放之前的宽高)
被加载Bitmap的Options.inSampleSize必须明确指定为1。
被加载Bitmap的Options.inPreferredConfig字段设置无效,因为会被被复用的Bitmap的inPreferredConfig值所覆盖(不然,所占内存可能就不一样了)

1.3 使用bitmap pool

因为在最终的绘制到canvas的时候使用的是一个bitmap,在优化前每次都重新创建了一个bitmap对象,实际上这是没有必要的,在使用完了bitmap之后可以通过bitmap pool 缓存起来,然后再次使用,在再次使用之前通过btimap.erase() 方法来擦除之前的数据信息即可,这样就避免了每次都重复创建新的bitmap对象。

1.4 正确的使用Glide.

这一点在Glide相关的文档中应该有正确的说明,避免将Bitmap与不正确的生命周期对象绑定,从而导致在相应的绑定对象被回收之后而bitmap无法回收。

1.5 合理优化图片大小

在使用Glide加载大图的时候,可以重写加载目标的高宽避免加载过大的图片,在项目中使用到了高斯模糊处理的图片作为背景图,如果不作处理的话,直接加载一个屏幕高宽的大图会消耗极大的内存,通过缩小高斯模糊背景处理之后图片的高宽可以极大的优化内存的占用

锁优化

在多线程的环境下锁的使用的确是避不开的,但是在一定条件下,是可以避免锁的使用的。例如在这次开发的APP中,摄像头的数据通过回调的方式传递过来(主线程),同时通过人脸识别的子线程对这个数据进行人脸识别的数据的处理。因为摄像头数据存放在一个类的成员变量中,主线程会进行写操作,而子线程会进行读写两个操作,在优化前两个线程操作这个数据都加上了锁。但是实际上分析一下会发现加锁的情况是可以避免的。锁的作用就是让两个操作在多线程的情况下不会交叉进行,既然是多线程的情况,我们把这个情况变成单线程的即可,在单线程的情况下就不会有这个问题了。

所以优化操作中,直接把子线程读写操作放到了主线程当中,这样数据的操作就避免加锁的情况。

通过对比前后的操作,一个方法的平均执行时间从32ms降低到了 0.4ms,优化有极大的提升。

##滑动优化

  1. 优化每一个列表项的布局结构,尽量减小布局中View的数量,在最近的一次优化中,通过减少TextView的数量来减少了布局的耗时。
  2. 避免使用View的alpha属性,需要使用透明色的时候,通过颜色的透明来实现而不是直接使用alpha.
12…5
Tom Liu

Tom Liu

41 日志
48 标签
© 2018 Tom Liu
由 Hexo 强力驱动
主题 - NexT.Muse