Learning


  • 首页

  • 归档

  • 标签

  • 关于

锁的可重入

发表于 2017-06-25

当一个线程正在请求一个被其他线程持有的锁的时候,会被挂起。但是因为锁的可重入性,一个线程再次请求被自己持有的锁,这个请求会成功。锁的可重入性意味着锁的占有是基于线程的而不是基于调用次数的。

锁的可重入性通过一个锁的占用计数和当前占用线程来实现,当占用计数是零的时候,这个锁会被认为是一个未被占用的锁,当一个线程请求一个未占用的锁的时候,JVM会记录占用锁的当前线程,然后设置占用计数为1. 如果当前持有线程再次请求这个锁,那么计数器将会加1. 如果当前线程退出同步块,那么锁占用计数将会减1,如果最后的占用计数为零,那么锁将会被释放

锁的可重入性极大的简化了面向对象编程的代码,如果没有锁的可重入性,那么一些即使看起来很简单的代码也会出错。

public class Base {
    public synchronized void method() {

    }
}   
public class Sub extends Base {
    public synchronized void method() {
        //do something 
        super.method();
    }
}

在上面的代码中,如果没有锁的可重入性,当一个线程执行Sub的 method 方法时,首先会获取Sub当前实例的锁,当时在调用super的时候,同样需要当前实例的锁,但是这个时候第一次取得的锁并没有释放,所以这样便形成了死锁。

TCP学习笔记2 滑动窗口

发表于 2017-06-25

TCP连接的每一端都可以收发数据,这些收发数据的管理是通过滑动窗口的结构来来维护的

发送窗口

TCP协议以字节为单位来维护窗口,其中可以为分为三个部分

  1. 已经发送并且已经收到ACK 的部分
  2. 已经发送但是未收到ACK 和 即将发送的部分
  3. 窗口移到该位置前都不可以发送的部分

随着时间的推移,发送端不断的发送和接收到ACK,并且接收到接收端的窗口通告,发送端的窗口会不算的往右移动,

下面的三个属于描述了窗口的移动情况

  1. 关闭 窗口的左边界右移,当接收到已经发送数据的ACK的时候,窗口会减小
  2. 打开 即窗口的右边界右移,使得可以发送的数量增大,这通常是因为接收端的处理了数据,缓存区变大,发送了一个更大的窗口通告。
  3. 收缩 即窗口的右边界左移,

TCP的每个报文都包含ack号和窗口通告信息,TCP发送端可以根据这些信息调节窗口的大小,窗口的左边界不能向左移动,因为左边界记录的是接收端的确认信息,具有累计性不能返回。

接收窗口

接收端也维护了一个接收窗口,窗口记录了已经接收并且确认的数据,以及能接收到的最大序列号,接收端希望避免存储重复接收的数据和已经确认的数据,并且避免存储不应该接收的数据(超过右边界的数据)

如果接收到了窗口左边界以左的数据,接收到的数据将会被丢弃,因为这些数据已经被确认接受过,超过了右边界的数据被认为无法处理也会被丢弃。只有当接收数据的序列号到等于左边界的时候,数据才会被接收处理,窗口才会向前滑动,对于使用了SACK的报文来说,这个窗口内的其他的报文也能被接收,但是只有接收到等于左边的数据时,窗口才会移动。

窗口通告

通过窗口通告,TCP 的接收方可以有效的控制发送端的发送速率,当当前的窗口为零的时候,可以有效的阻止发送端发送新的数据。当接收端已经成功处理了接收到的数据,有重新可用的缓存空间的时候,就可以发送一个窗口通告给发送方,来恢复发送。这样的窗口通告通常都不包括数据,成为纯ACK,窗口通告通常采用不可靠的方式传输。

在接收端发送一个窗口通告的同时,发送方还可以通过窗口探测的方式查询当前可用窗口,发送方发送的窗口探测包括一个有效的字节,采用TCP的可靠传输(丢失重传),这样就避免了由窗口通告丢失导致的零窗口死锁。

FFMPEG记录

发表于 2017-06-24

使用的两个关键命令

ffmpeg 
    -framerate 10 
    -start_number 1 
    -i app_imagecache/3461686%04d.png 
    -i logo.png 
    -i pa.png 
    -vframes 44 
    -filter_complex "[1:v]scale=50:50[water];[0:v][water]overlay=0:0[x];[x][2:v] paletteuse,reverse" 
    -y logo.gif

下面来简单的解析一下这段命令

  • framerate 指定生成gif的 帧率,目前ffmpeg 最快支持为 20ms/每帧,所以这里的帧率最大0为 50
  • start_number 指定图片输入序列的启示位置
  • -i app_imagecache/3461686%04d.png 指定生成gif的 图片序列
  • -i logo.png 指定水印
  • vframes 指定帧数
  • -filter_complex “[1:v]scale=50:50[water];[0:v] [water]overlay=0:0[x];[x][2:v]paletteuse,reverse”

这一段指定对输入的处理 [1:v]实际上指定了命令的中的输入参数,这里的 1 表示参数的序号,序号是基于0开始的,这里实际上指向了第二个输入参数也就是 logo.png. scale说明对这个水印的大小,这里指定了高宽为 50、接下来跟着了一个[water]这个就表示对上一个步骤处理结果的重命名,在后续的操作中就可以用这个重命名来指代。接下来使用[water] 表示上一步指定为 50:50 的这个水印 偏移量 为0:0; 接下来是[x][2:v] 这两个单独的意思都能理解,放在一起的理解应该是对将[2:v]所致的结果应用到[x]上面去,[2:v] 执行了第三个输入参数即pa.png, 后面跟着了paletteuse,加上这个是为了提升了gif图像的质量,为gif制定了一个全局的调色盘。

再接下来的reverse 制定了整个gif按照倒序的方式生成。

刚刚说到了gif的调色盘,生成的方式如下

ffmpeg
-i source
-vf palettegen
-y palettegen.png

-i 即我们制定的生成调色盘的源文件,在项目中,我们的gif来源于视频,所以在生成调色盘的时候,这里制定为 视频的路径。

开发的时候遇到一个问题, 从制定的时间开始取视频帧的时候,非常的慢,因为需要从头开始解析,所以后来的做法就是先截取视频,然后截取后的视频中开始取帧。

TCP学习笔记

发表于 2017-06-12

TCP 和UDP 都是运行在IP层之上的协议,但是和UDP协议很大的一个不同点是,TCP面向连接的可靠协议。面向连接是指在TCP开始传输数据之前,传输的端点之间需要先建立一条连接,然后再开始传输数据,可靠是指TCP协议中对数据的可达性作出了保证,如果在传输的过程中出现了数据丢失的情况,那么会对数据进行重传,正是因为这两个不同点,所以TCP协议比UDP协议会复杂很多,在TCP/IP协议详解一书中,花了六个章节来讲解其中的知识。其中的基本知识点包括:连接的建立,超时和重传,数据流和窗口管理,拥塞和控制.

TCP 数据格式

TCP的数据格式如下图所示

此处输入图片的描述

  1. 每个TCP的头部都包含了目的地的端口号和源端口号,TCP层的端口号和IP成的ip地址,组成了一个四元组,这个四元组唯一标识一个连接。
  2. 32位的序列号代表了该数据中包中第一个字节的编号。
  3. 32对确认号是指该数据包的发送方希望接收的下一个序列号,这个值是对当前已经接收的序列号加一所得。
  4. 4位的头部长度标识tcp头部的长度,这个值最大位15,每位标识32位的长度,所以TCP头部长度限制为 15 * 32(bit) = 60字节,在不带选项的情况下,头部长度为20字节。
  5. 当前TCP协议当中还有8位字段,分别标识 1.拥塞窗口减 2.ECN回显 3.URG 4.ACK 5.PSH 6.RST 7.SYN 8.FIN 这些置位分别在连接的不同阶段起作用

TCP连接的建立

TCP连接的建立需要经典的三次握手的过程。过程如下

此处输入图片的描述

  1. 客户端首先发送一个SYN报文(TCP头部中的SYN被置位),并且指明自己想要连接的端口号和给定一个初始化的序列号SYN(c)。(第一次握手)
  2. 服务端收到这个客户端的SYN 报文之后给出一个响应,在这个响应当中,服务器给出了自己的初始化序列号SYN(s),此时为了对确认客户端的请求,服务端会将客户端发送的序列号加一之后放在TCP头部的确认号字段当中ACK(c+1),因此每发送一个syn,序列号都会加1.(第二次握手)
  3. 为了确认服务器端SYN(S),客户端会将服务器端的S + 1之后放在ACK(S+1)中,发送给服务端。(第三次握手)

三次握手的过程不仅让通信的双方了解到一个连接正在建立,同时也在利用数据包的选项来交换初始序列号。

TCP连接的断开

与TCP 连接的建立相比,TCP 连接的断开多了一步,一共需要四个步骤。

  1. 连接的主动关闭者(Client),发送一个FIN报文来表示自己想关闭连接,同时这个报文里面还包括一个ACK来确认对方最近一次发来的数据。
  2. 服务端接收到客户端断开的请求之后,发送一个ACK对客户端的FIN 报文做出响应,这个时候连接处于半关闭状态。
  3. 服务端在发送完数据之后,同样发送一个FIN给客户端,来关闭连接。
  4. 客户端接收到了FIN之后,对服务端的FIN做出一个响应,至此连接关闭。

初始化序列号

TCP建立的连接的时候需要选择一个初始化序列号,这个初始化序列号随着时间的变化而变化,所以每个连接的初始化序列号都不一样。

TCP连接是由双方的端口和IP地址这个四元组组成,选择有效的初始化序列号可以有效避免连接超时重连后,无效的数据重新到达目的主机导致的数据混乱问题,这个时候选择有效的初始化序列号可以最大限度的避免这一问题。在双方通信的过程中,一个TCP报文只有同时具备连接四元组和当前活动窗口的序列号才会被通信双方接受。

TCP选项

TCP的头部可以包含多个选项,常用的选项如下

  1. 最大段大小选项 指的是允许从对方接收到的最大报文段,这个报文段大小只记录器数据的厂区而不包括TCP和IP包头的长度。
  2. 选择确认选项 这个可以帮助TCP实现快速重传,并且帮助发送方了解到接收方的空洞,
  3. 窗口缩放选项(待补充)
  4. 时间戳选项和防回绕序列 这个选项要求发送方在每一个报文中添加2-4个字节的时间戳数字,接收方会在响应中反映这些数字,这也允许发送方来测量一个RTT

JNI相关的知识点

发表于 2017-05-12

extern “C”

  1. extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

与extern对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

  1. 典型的,一个C++程序包含其它语言编写的部分代码。类似的,C++编写的代码片段可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。

为了使它们遵守统一规则,可以使用extern指定一个编译和连接规约。例如,声明C和C++标准库函数strcyp(),并指定它应该根据C的编译和连接规约来链接:

JNI技术

动态加载的几个知识点:

  1. System.loadLibrary会优先查找apk中的so目录,再查找系统目录,系统目录包括:/vendor/lib(64),/system/lib(64)
  2. 不能使用不同的ClassLoader加载同一个动态库
  3. System.loadLibrary加载过程中会调用目标库的JNI_OnLoad方法,我们可以在动态库中加一个JNI_OnLoad方法用于动态注册如果加了JNI_OnLoad方法,其的返回值为JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6其一。我们一般使用JNI_VERSION_1_4即可
  4. Android动态库的加载与Linux一致使用dlopen系列函数,通过动态库的句柄和函数名称来调用动态库的函数

Retrofit 源码学习

发表于 2017-04-06

Retrofit的对象是通过建造者模式构建出来的

public Retrofit build() {
  if (baseUrl == null) { //检查baseurl
    throw new IllegalStateException("Base URL required.");
  }

  okhttp3.Call.Factory callFactory = this.callFactory;
  if (callFactory == null) {
    // 如果未设置Okhttp 对象,那么使用默认的
    callFactory = new OkHttpClient(); 
  }
  Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {
    callbackExecutor = platform.defaultCallbackExecutor();
  }
  // Make a defensive copy of the adapters and add the default Call adapter.
  List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
  adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

  // Make a defensive copy of the converters.
  List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);

  return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
      callbackExecutor, validateEagerly);
}

在项目中我们定义了 TestService 这个接口

public interface TestService {
    @GET("xxxx/{path}/data")
    Call<String> getData(@Path("path") String path);
}

当我们需要使用TestService 的时候只需要调用 retrofit.create 方法。

service = retrofit.create(TestService.class);

就会得到一个TestService 的实例。

进入create 方法

public <T> T create(final Class<T> service) {
// 检查是否是一个有效的接口, 同时限定这个类只能是接口,并且没有继承其他的接口。
Utils.validateServiceInterface(service);  
if (validateEagerly) {
  eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
    new InvocationHandler() {
      private final Platform platform = Platform.get();

      @Override public Object invoke(Object proxy, Method method, Object... args)
          throws Throwable {
        // If the method is a method from Object then defer to normal invocation.
        if (method.getDeclaringClass() == Object.class) {
          return method.invoke(this, args);
        }
        if (platform.isDefaultMethod(method)) { // 源码中始终返回false.
          return platform.invokeDefaultMethod(method, service, proxy, args);
        }

        ServiceMethod serviceMethod = loadServiceMethod(method);
        OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
        return serviceMethod.callAdapter.adapt(okHttpCall);
      }
    });
}

eagerlyValidateMethods(service);

这个方法会把service中所有的方法放在 Retrofit 实例的 Map serviceMethodCache 对象当中,供以后的调用。validateEagerly 这个参数的作用就是判断是否在首次创建Retrofit 的时候就把所有的方法都放到这个map当中,如果不是就是使用到的时候再放进去

这个Map 中的key 是 Method 对象,而key 是SerivceMethod 这个对象中包含了很多东西,例如baseURL, relativeUrl, headrs 等信息,可以看看源码,ServiceMethod 对象同样是通过建造者模式创建的,创建的过程如下

  1. 创建一个 CallAdapter 对象
  2. 检查返回值类型,如果没有给定泛型中的具体类型,那么会抛出异常,例如直接返回Call 会抛出一个异常。
  3. 设置Response body 的转换器。
  4. 开始解析添加在方法上的注解,其中的注解包括了Http Method, Path, Header等信息,如果其中还包含Query Sring ,还会检查query string 是否不正确的使用@Path 注解。
  5. 然后再次检查是否没有添加表示Http Method的注解
  6. 检查是否有请求体,如果没有请求体,但是请求需要提交表单 那么会抛异常。
  7. 检查参数的类型,参数不能为表示泛型的通配符,必须是具体的类型
  8. 检查方法参数的注解,如果没有添加方法参数的注解会抛异常。
  9. 检查注解和方法参数注解的匹配,和使用规范注解(例如一个参数只能有一个注解)

方法的最后通过动态代理的方式返回了一个实现了 service 代表接口的类。

这里说一下动态代理匿名内部类中 Platform, 这个对象实际上表示了不同平台,目前里面默认的有Java, Android,IOS, 根据不同的平台会有一些不同地方,例如默认的回调池, CallAdapter 如何创建(后面分析)等等。

ServiceMethod serviceMethod = loadServiceMethod(method);

这一行代码就是前面提到的从 serviceMethodCache 这个map 对象中去取对应的方法。
之后创建了一个OkHttpCall 对象,OkHttpCall 实现了 Call 这个interface,进入到Call 这个interface中,上面有一段对这个类的注释:

Call最终代表了Retrofit 发送请求到服务器然后从服务器接受一个响应,每一个call产生自己的请求和和响应,可以通过clone方法来多次实现多次请求,通过这种方式可以实现轮询或者请求失败的重试。Call 通过enqueue方法来实现异步的调用,也可以使用 execute() 来实现同步的调用,当call正在读响应或者发请求的时候可能会有IOException.

读完这段之后,我们再来看 OkHttpCall 就会明白 OkHttpCall 只是 使用OkHttp来实现发送请求,所以Retrofit 并不是一个真正发送网络请求的库,真正实现网络请求的是okhttp,Retroif 只是在在上面实现了一层封装,提供了接口。

我们接着看 return serviceMethod.callAdapter.adapt(okHttpCall);

这里调用CallAdpater的adapt 方法返回了一个对象。

CallAdapter 是一个接口。

public interface CallAdapter<T> {
      Type responseType();
      <R> T adapt(Call<R> call);
}

其中定义两个方法,从方法的文档中可以知道 responseType 返回响应泛型的具体类型,在TestService中就是String, 而 adapt 方法就是真正产生返回值 Call的, 在文档里面说:这个方法实际上产生了 参数 call 的 一个代理。我们可以通过自定义自己的CallAdapter并且在retrofit对象构建的过程中传入,Retrofit 提供了一个默认的CallAdapter.

这个 callAdapter 对象创建的关键是在retrofit build过程中的 adapterFactories, 这是一个 CallAdapter 对象的List.

我们再回到Retrofit 的 build 方法当中

Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {
    callbackExecutor = platform.defaultCallbackExecutor();
  }

  List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
  adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

callbackExecutor 这里返回对象不为空,会返回一个 MainThreadExecutor

static class MainThreadExecutor implements Executor {
    private final Handler handler = new Handler(Looper.getMainLooper());
    @Override 
    public void execute(Runnable r) {
        handler.post(r);
    }
 }

这个熟悉android Handler 的一看就知道了

接下来看 platform 对象的 defaultCallAdapterFactory 方法, 传入了 callbackExecutor

这里 platform 对象实际上 是 Android

static class Android extends Platform {
    ....    
    @Override 
    CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }
    ....
}

所以真正的 CallAdaterFactory (工厂模式创建CallAdater)在这里被构造出来。接着看 ExecutorCallAdapterFactory

贴关键代码

class ExecutorCallAdapterFactory extends CallAdapter.Factory {
    ...
  @Override
  public CallAdapter<Call<?>> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    ...
    final Type responseType = Utils.getCallResponseType(returnType);
    return new CallAdapter<Call<?>>() {
    ...
      @Override public <R> Call<R> adapt(Call<R> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);
      }
    };
  }

这里又是一个匿名内部类,关键的adapt 方法返回了 ExecutorCallbackCall 对象。

接着看 ExecutorCallbackCall, 继续部分关键代码

static final class ExecutorCallbackCall<T> implements Call<T> {
    final Executor callbackExecutor;
    final Call<T> delegate;

    ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
      this.callbackExecutor = callbackExecutor;
      this.delegate = delegate;
    }

@Override 
public void enqueue(final Callback<T> callback) {
  if (callback == null) throw new NullPointerException("callback == null");
  delegate.enqueue(new Callback<T>() {
    @Override public void onResponse(Call<T> call, final Response<T> response) {
      callbackExecutor.execute(new Runnable() {
        @Override public void run() {
          if (delegate.isCanceled()) {
            callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
          } else {
            callback.onResponse(ExecutorCallbackCall.this, response);
          }
        }
      });
    }

    @Override public void onFailure(Call<T> call, final Throwable t) {
      callbackExecutor.execute(new Runnable() {
        @Override public void run() {
          callback.onFailure(ExecutorCallbackCall.this, t);
        }
      });
    }
  });
}
....

首先 ExecutorCallbackCall 这个类也是一个Call对象,只用看enqueue 方法,方法的实现全部都是用调用了delegate,所以这里就知道了之前CallAdapter 接口文档里面写的 adapt 方法返回的只是 adapt 方法参数 call, 的一个包装类,这里的 delegate 就是call 对象。在这个回调里面也通过 callbackExecutor (MainThreadExecutor)实现了将callback 放到主线程里面来调用。

分析完了callAdapter.adapt 的方法,知道他返回的是一个 OkhttpCall的包装类。

再接着看OkhttpCall, 看异步发送请求的实现,

@Override
public void enqueue(final Callback<T> callback) {
    checkNotNull(callback, "callback == null");

    okhttp3.Call call;
    Throwable failure;

    synchronized (this) {
        if (executed) // 一个请求不能重复执行
            throw new IllegalStateException("Already executed.");
        executed = true;

        call = rawCall;
        failure = creationFailure;
        if (call == null && failure == null) {
            try {
                call = rawCall = createRawCall();  // ①
            } catch (Throwable t) {
                failure = creationFailure = t;
            }
        }
    }

    if (failure != null) {
        callback.onFailure(this, failure);
        return;
    }

    if (canceled) {
        call.cancel();
    }

    call.enqueue(new okhttp3.Callback() {
        .....
    });
}

① 首次执行的时候,rawCall 和 creationFailure 都为空,这个是需要创建 rawCall , 也就是okhttp3.Call 的对象,进入到 createRawCall() 方法

private okhttp3.Call createRawCall() throws IOException {
    Request request = serviceMethod.toRequest(args);
    okhttp3.Call call = serviceMethod.callFactory.newCall(request);
    if (call == null) {
      throw new NullPointerException("Call.Factory returned null.");
    }
    return call;

}

先通过serviceMethod 创建了一个 Request 对象,

Request toRequest(Object... args) throws IOException {
RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers, contentType, hasBody, isFormEncoded, isMultipart);
@SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;
int argumentCount = args != null ? args.length : 0;
if (argumentCount != handlers.length) {
  throw new IllegalArgumentException("Argument count (" + argumentCount
      + ") doesn't match expected count (" + handlers.length + ")");
}
for (int p = 0; p < argumentCount; p++) {
  handlers[p].apply(requestBuilder, args[p]);
}
return requestBuilder.build();
}

实际上在这里,根据我们自定义service中的方法的参数来创建了一个Okhttp 中的请求对象Requestd,其中需要注意的是, 这个构造其中的过程中完成了对配置的各项参数的处理,其中ParameterHandler 是一个抽象类,继承这个类的类需要实现

abstract void apply(RequestBuilder builder, T value) throws IOException;

看一下Path 参数是如何被处理的,代码如下

static final class Path<T> extends ParameterHandler<T> {
    ....
    @Override 
    void apply(RequestBuilder builder, T value) throws IOException {
      if (value == null) {
        throw new IllegalArgumentException(
            "Path parameter \"" + name + "\" value must not be null.");
      }
      builder.addPathParam(name, valueConverter.convert(value), encoded);
    }
}

最终是在 builder.addPathParam(name, valueConverter.convert(value), encoded);完成了对name(定义在url中的{name})替换。

最后调用requestBuilder.build() 完成了okhttp request 对象的创建。

在看到 createRawCall 方法当中,接下来需要用 okhttp 的 request 对象创建一个Call 对象,
接下来进入到 service.callFactory 的 newCall方法当中,这个方法最终的实现在OkHttpClient 这个类中,newCall直接创建了 RealCall 对象。

final class RealCall implements Call {...}

看到 RealCall 这个对象的声明,可以看出来实际上 OkHttpCall 是一个包装类,最终实现请求的是 RealCall。

那么实际上OkHttpCall实现的Call的各种方法实际上都是调用RealCall的实现来完成的。

再回到OkhttpCall 的enqueue 方法当中,如果上述的创建请求对象的过程中出错了,那么直接调用回调的onFailure 方法。

接下来 会调用call的 enqueue 方法,从上面的分析可以知道 call 实际上是 RealCall的一个对象,我们直接到RealCall中去看 enqueue 方法的实现。

RealCall 内部重载了enqueue

void enqueue(Callback responseCallback, boolean forWebSocket) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));}

关键是调用 client.dispatcher().enqueue() 这个方法,

因为这个里面就涉及到 okHtpp 源码的分析,就不再深入的分析,只是简单的看下里面的代码:

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }

}

AsyncCall 是一个Runnable 的对象,最终放到通过executorService方法得到的线程池中来执行。
不过放进去之前先判断一下当前正在执行的请求是否超过了限制,还有判断当前运行的请求队列是否有过多(大于maxRequestsPerHost)的request 请求同一个host ,如果不满足就放到等待队列中。

当请求完成的代码,是在AsyncCall当中。

Override 
protected void execute() {
  boolean signalledCallback = false;
  try {
    Response response = getResponseWithInterceptorChain(forWebSocket); // ①
    if (canceled) {
      signalledCallback = true;
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      signalledCallback = true;
      responseCallback.onResponse(RealCall.this, response);
    }
  } catch (IOException e) {
    if (signalledCallback) {
      // Do not signal the callback twice!
      Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    client.dispatcher().finished(this);
  }
}

① 在这里我们有机会通过自己Interceptor 来处理请求和响应, 在Retrofit 中默认定义了一个 ApplicationInterceptorChain,请求从这里开始处理。

 @Override 
 public Response proceed(Request request) throws IOException {
  // If there's another interceptor in the chain, call that.
  if (index < client.interceptors().size()) {
    Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
    Interceptor interceptor = client.interceptors().get(index);
    Response interceptedResponse interceptor.intercept(chain);

    if (interceptedResponse == null) {
      throw new NullPointerException("application interceptor " + interceptor
          + " returned null");
    }
    return interceptedResponse;
  }
  // No more interceptors. Do HTTP.
  return getResponse(request, forWebSocket);
}

这里实际上有一个类似递归的调用,通过不断的 改变 ApplicationInterceptorChain 的 index, 来使每个interceptor的intercept 方法调用,并且通过request 这个引用,来使request 所指向的对象得到每个拦截器的处理。这里拦截器可以想想java Web 中Servlet 中拦截器的处理。

接下来getResponse() 方法 就是真正发送Http请求的地方,这个里面涉及到较多okHttp 的使用,这里就不分析了,等后面看了okhttp 的源码再来说。

在getResponse() 中会得到一个okhttp response 的对象,根据请求的结果来回调 responseCallback 的 onFailure 和 onResponse 方法。

responseCallback 这个对象就是在OkHttpCall 的 enequeue 方法中 调用 RealCall enequeue 方法传入的,那么接下来在回到 OkHttpCall 的 enqueue 方法。

public void enqueue(final Callback<T> callback) {
    ....

    call.enqueue(new okhttp3.Callback() {
  @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
      throws IOException {
    Response<T> response;
    try {
      response = parseResponse(rawResponse);
    } catch (Throwable e) {
      callFailure(e);
      return;
    }
    callSuccess(response);
  }

  @Override public void onFailure(okhttp3.Call call, IOException e) {
    try {
      callback.onFailure(OkHttpCall.this, e);
    } catch (Throwable t) {
      t.printStackTrace();
    }
  }

  private void callFailure(Throwable e) {
    try {
      callback.onFailure(OkHttpCall.this, e);
    } catch (Throwable t) {
      t.printStackTrace();
    }
  }

  private void callSuccess(Response<T> response) {
    try {
      callback.onResponse(OkHttpCall.this, response);
    } catch (Throwable t) {
      t.printStackTrace();
    }
  }
});
}

从这里调用我们可以简单的看出这里实际上又是Callback的一个包装,拿到okhttp3.Response 对象之后调用 parseResponse 方法,将OkHtt Response 转换为一个 Retrofit 的 Response.
然后调用 callSuccess 最终来回调我们使用时传入的callback.

到这里我们使用Retrofit 实现一个异步请求的过程就分析完成了。

Java 异常处理

发表于 2017-03-29

最近在看Effective Java 看到了更异常相关的章节,虽然里面道理都看的懂,但是一旦要自己操作起来的时候就懵逼了,后来发现了一篇博文感觉里面写的还不错,现在翻译出来如下,附上原文的链接 Desiging with exceptions

异常的好处

异常有不少优点,首先它可以让你将异常处理的代码和正常的代码分离,在99.9%的情况下,你可以用try 代码块来包裹你想执行的代码,然后将异常处理的代码放在catch代码块中,这样的写法将避免使你的代码看起来杂乱无章。

如果你觉得一个方法无法处理一个特定的错误,那么你可以通过抛出一个异常,让其他人来处理这个问题。如果你抛出了一个编译时期的异常,那么Java编译器将迫使调用者来处理指定的异常,要么通过try catch ,要么调用者自己也声明抛出异常,Java编译器对编译期间异常的强制处理,使得Java程序更加的健壮。

什么时候抛异常

什么时候应该抛出一个异常呢? 可以用下面的一句话来总结

如果你的方法遇到了无法处理的异常条件,那么抛出一个异常

(看到这句话一定会想MDZZ….)

不幸的是,尽管这一句总结很好记忆,但是它并没有让你更清楚的明白这个问题,它实际上引出了另外一个问题:什么是异常条件?

事实上判断一个特定的事件是不是一个异常条件是一个主观上的问题,这个判断并不是总是清晰。

一个更加有用的总结应该是下面这句:

在可以正常处理逻辑的情况下,不要使用异常(原文:Avoid using exceptions to indicate conditions that can reasonably be expected as part of the typical functioning of the method.)

因此不正常的条件可以理解为一个方法中无法正常处理的部分。为了更好的理解这句话,请看下面的例子,

示例

作为示例,请看java.io中的两个类,FileInputStream 和 DataInputStream ,下面这段代码用FileOutputStream 来输出一个文本文件。

import java.io.*;
class Example9a {
    public static void main(String[] args)
        throws IOException {
        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }
        FileInputStream in;
        try {
            in = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }
        int ch;
        while ((ch = in.read()) != -1) {
            System.out.print((char) ch);
        }
        System.out.println();
        in.close();
    }
}

在这个示例中,read()方法没有通过使用异常来表示读取文件已经到达了文件的末尾,而是通过返回了一个特定的值 -1, 在这个方法中,到达文件的末尾是一种正常的使用场景。正常读取字节的方式就是一直读直到读到文件的末尾。

再来看看DataInputStream 这个类,采用不同的方式来表明到达了文件的末尾。

import java.io.*;
class Example9b {
    public static void main(String[] args)
        throws IOException {
        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }
        FileInputStream fin;
        try {
            fin = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }
        DataInputStream din = new DataInputStream(fin);
        try {
            int i;
            for (;;) {
                i = din.readInt();
                System.out.println(i);
            }
        }
        catch (EOFException e) {
        }
        fin.close();
    }
}

每次调用readInt的时候,它从流中读取四个字节,当读取到文件的末尾的时候,抛出了一个EOFException, 对于这个方法来说,抛出异常是一个合理的选择,原因如下

  • 首先,readInt()不能返回一个特定的值,因为任何一个数字都可能是一个合理的返回值,因此无法用一个特殊的数字来表明读取到了文件的末尾。
  • 第二,如果readInt读到了文件的末尾,但是只有三个字节,因此抛出一个编译期异常让调用者来处理是使用这个类的一部分。因此这个类的设计者抛出了一个编译期异常让迫使调用者来处理这个异常。

还有另外一种方法来表明”数据已经到达了结尾”,下面看StringTokenizer 和Stack 的例子。

import java.io.*;
import java.util.*;
class Example9c {
    public static void main(String[] args)
        throws IOException {
        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }
        FileInputStream in = null;
        try {
            in = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }
        // Read file into a StringBuffer
        StringBuffer buf = new StringBuffer();
        try {
            int ch;
            while ((ch = in.read()) != -1) {
                buf.append((char) ch);
            }
        }
        finally {
            in.close();
        }
        // Separate StringBuffer into tokens and
        // push each token into a Stack
        StringTokenizer tok = new StringTokenizer(buf.toString());
        Stack stack = new Stack();
        while (tok.hasMoreTokens()) {
            stack.push(tok.nextToken());
        }
        // Print out tokens in reverse order.
        while (!stack.empty()) {
            System.out.println((String) stack.pop());
        }
    }
}

在上面这个例子中,从一个文件中读取字节,然后将字节转换为char, 放到一个StringBuffer中,之后让StringTokenizer 使用默认的分隔符来分割字符串,并且将字符串放入到一个栈中, 下一步是用pop操作将栈中数据全部取出,因为栈是一个FILO结构,数据将会倒序输出。StringTokenizer 和Stack 都有一个条件来判断数据是否已经到达末尾。每次调用 nextToken的时候都会返回一个String, 当所有的String 对象都被返回的时候,StringTokenizer 必须表明所有的数据都被取完了,在这个示例中,null 本来可以被用作一个数据已经被取完的返回值,但是这个类的设计者采用了另外一种方式,用 hasMoreTokens() 这个方法来判断当前是否还有数据未被取完,在每次调用nextToken之前,你都需要调用hasMoreTokens() 方法。

在这个设计之中,类的设计者认为到达数据结构的末尾是一种正常的情况(原文:This approach shows that the designer did not consider reaching the end of tokens an abnormal condition, 个人理解:数据结构为空实际上是一种正常的情况,但是从一个为空的数据结构中获取数据则是处于不正常的条件,在后面会马上提到已经实际上是违反了使用这个方法的约定),如果你在在使用nextToken之前没有检查hasMoreTokens(), 你会遇到 NoSuchElementException,尽管这个是在已经到达数据结构末尾这种情况下抛出的,它更多的是表明了你使用这个类的方式不对, 而不是表示没有更多数据了。

类似的,Stack 同样有一个方法 empty(), 它表示栈中是否还有其他的元素,在你使用pop方法之前,你都需要使用empty 来判断栈中是否还有元素,如果你在使用pop()之前没有调用empty(), 如果栈中没有了其他的元素,那么你将遇到 EmptyStackException(), 尽管这个异常只会在栈为空的情况下发生,但是这个运行时异常更多的是用来表明客户端代码中出了问题,而不是提示你这个栈为空。

异常实际上表明违反了使用约定

上面的例子可能会让你觉得,你应该使用异常来传递一个信息而不是用其他的方式。如果从另外一个角度来思考异常,可能会让你更加清楚什么时候应该使用他们

异常实际上表明了违反了使用的约定 (原文:Exceptions indicate a broken contract)

在面向对象编程中,我们经常谈到的一点是Design by Contract approach. 在软件设计中,一个方法实际上代表了调用者和设计者 之间的一种约定,这种约定包括调用者必须满足前置条件,而方法本身必须满足后置条件(个人理解:调用者使用方法的时机应该正确,方法的参数应该传对,而方法本身应该能按照约定正常的处理输入然后给出输出)

前置条件

以String对象的charAt(int index) 为例,这个方法要求index 必须在 0 和 length -1 之间。 例如string的长度为5, 那么你的输入应该是 0 - 4.

后置条件(Postcondition)
String chatAt方法的后置条件是 返回index指定位置的字符,并且string本身保持不变。

如果调用者传入了 -1 或者大于等于 length 的参数,那么实际上是调用者 违反了约定,那么这个时候会抛出一个 StringIndexOutOfBoundsException , 这个异常表明调用者代码有bug。

如果charAt 方法本身接收到了一个合法的输入,但是由于这个方法本身无法返回指定位置的字符(无法满足后置条件),同样应该抛出异常,这个异常表明了这个方法有bug或者无法处理运行时资源。

What to Throw

一旦你决定要抛出一个异常,那么你需要选择应该抛出哪一种异常。

Exceptions VS errors

一般说来你应该要使用异常而不是Error, Error 也是 Throwable的子类。有时表明JVM出了某些问题例如 OOM Error,有使用JAVA API会抛出错误,例如 java.awt.AWTError, 但是在你的代码中,应该只是用异常。

编译期异常 VS 运行期异常

这个问题才是一个大问题,到底是编译期异常还是运行时异常,编译期是Exception 及其子类,但是子类部分中要把RuntimeException 和 其子类排除,Error 及其子类同样也不是编译期异常。

如果你抛出了一个编译期异常但是并没有捕获,那么你需要在你的方法声明中加上throws, 这样调用者在使用你的代码时必须使用try catch 来处理异常或者将他自己的方法添加异常的声明,使用编译期异常将迫使调用者来处理方法可能抛出的异常。

如果你抛出的是一个运行期异常,那么调用者可以使用try catch 来捕获异常或者直接忽略掉异常。编译期将不会迫使调用者去处理这个异常,实际上调用者可能都不知道这个方法会抛出哪些异常。

简单的区别方式如下:

如果你抛出了一个异常,并且你觉得客户端程序员可以有意识的处理这个异常,那么应该使用编译期异常

一般来说,如果异常是想表明类的使用方式出了问题,那么这个异常因该是运行期异常。String charAt 抛出的StringIndexOutOfBoundsException 是运行期异常。String 的设计者不希望迫使调用者每次使用charAt的时候都去处理可能的错误。

但是 java.io.FileInputStream 的 read方法 抛出一个编译期异常,这个异常表明在读取文件的期间出现了某些问题,这个问题并不是由于客户端使用的方式不对而引起,它只是表明这个方法本身无法按照约定从文件中读取字节,类的设计者认为这种这种不正常的条件是十分普遍的。并且住够重要让调用者去处理这种情况。

通过上面的对比,基本上可以得出以下的结论,如果异常条件的发生是方法本身无法满足约定并且重要程度足够到需要调用者去处理,那么这个时候需要抛出一个编译器异常,迫使调用者去处理,否则抛出一个运行期异常。

定义具体的异常

最终你需要决定使用哪个异常类,在这里规则非常的具体,不要直接使用Exception,你应该选择现有的已经存在的可以说明你异常情况的异常类。这样调用者可以有选择的处理各种异常情况。

同时你可以在这些异常中嵌入你想添加的信息,但是不要靠这些信息来区分发生了什么异常。

总结

这篇文章最重要的一点在于,异常是用来处理不正常的条件的,当程序可以用一个正常的返回值来标志时,就不应该使用异常。 尽管异常可以可以帮助你去将异常处理代码和处理正常逻辑的代码区分开,但是不恰当的使用只会让你的代码更加难以阅读。

Java 泛型

发表于 2017-03-28

Java中的泛型是在编译器这个级别实现的,在生成的Java字节码当中是没有泛型中的类型信息的,使用泛型的时候加上的类型参数会在编译的时候去掉,这个过程称作为类型擦除,如在代码中定义的List 和 List, 在编译之后都变成了List, JVM看到的只有List (所以这一点和C++中的模板是不同的,在C++中为每一个模板类生成一个类, 此外在Java中,泛型中还不支持基本类型) 而由泛型附加的类型信息对JVM来说是不可见的,Java编译器会在编译的时候尽可能发现出错的地方,但是仍然无法避免运行时刻出现的强制类型转换异常,类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。

泛型的很多奇怪特性都与类型擦除有关:

  • 泛型没有自己独有的Class类对象,比如不存在List.class ,只有List.class;
  • 静态变量被泛型类所有的实例共享,例如MyClass 中定义静态变量 var , 访问这个静态变量的方式任然是MyClass.var。
  • 泛型的参数类型不能用在Java的异常处理中,以为异常处理是由JVM的 Runtime 来处理的,但是由于类型擦除,所以JVM是不知道这个类型信息的存在的,对于JVM来说都是MyException.也就无法执行对应的catche语句。(目前在Intelij IDE中,定义的一个泛型的异常直接报错(Generic class may not extends java.lang.Throwable))

这些原因的根源都是泛型的类型信息只是在编译期存在的,而在程序执行的时候无法找到的。

但是这里又有另外一个问题了,我们在程序运行的时候明明是可以通过API 拿到另一个类的泛型信息的,
class 对象由这两个方法

  • getGenericSuperclass()
  • getGenericInterface()

那么这两个类拿到的泛型信息又是从哪里获取的呢?
Java文件在编译之后会生成字节码文件,字节码文件的结构如下图所示

此处输入图片的描述

接下来,先定义两个类型,

public class MyClass  {
    List data;
    public void setData(List data) {
        this.data = data;
    }
}
public class MyClassString {
    List<String> data;
    public void setData(List<String> data) {
        this.data = data;
    }
}

在编译完成之后,我们可以通过javap 来获取两者生成的字节码

运行 javap -c MyClassString 结果如下

public class com.baidu.accweather.MyClassString {
  java.util.List<java.lang.String> data;
  public com.baidu.accweather.MyClassString();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void setData(java.util.List<java.lang.String>);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field data:Ljava/util/List;
       5: return
}

运行javap -c MyClass 结果如下

public class com.baidu.accweather.MyClass {
  java.util.List data;
  public com.baidu.accweather.MyClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void setData(java.util.List);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field data:Ljava/util/List;
       5: return
}

这个时候通过对比发现出列data 的声明不同之外其他的地方完全一样,特别需要注意的是在setData当中,从注释来看 data, 它的类型信息里面已经没有了泛型类型的信息。

接下来我们需要用 -verbose 参数打印堆栈大小、各方法的locals及args参数,以及class文件的编译版本

运行javap -verbose MyClassString , 其常量池的信息输出如下(MyClass 的就不贴出来了)

   Classfile /D:/javaworkspace/AccWeather/out/production/AccWeather/com/baidu/accweather/MyClassString.class
  Last modified 2017-3-27; size 610 bytes
  MD5 checksum caaccea4d689459e3464f2088a3efb34
  Compiled from "MyClassString.java"
public class com.baidu.accweather.MyClassString
  SourceFile: "MyClassString.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#22         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#23         //  com/baidu/accweather/MyClassString.data:Ljava/util/List;
   #3 = Class              #24            //  com/baidu/accweather/MyClassString
   #4 = Class              #25            //  java/lang/Object
   #5 = Utf8               data
   #6 = Utf8               Ljava/util/List;
   #7 = Utf8               Signature
   #8 = Utf8               Ljava/util/List<Ljava/lang/String;>;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/baidu/accweather/MyClassString;
  #16 = Utf8               setData
  #17 = Utf8               (Ljava/util/List;)V
  #18 = Utf8               LocalVariableTypeTable
  #19 = Utf8               (Ljava/util/List<Ljava/lang/String;>;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               MyClassString.java
  #22 = NameAndType        #9:#10         //  "<init>":()V
  #23 = NameAndType        #5:#6          //  data:Ljava/util/List;
  #24 = Utf8               com/baidu/accweather/MyClassString
  #25 = Utf8               java/lang/Object
{
  java.util.List<java.lang.String> data;
    flags:
    Signature: #8                           // Ljava/util/List<Ljava/lang/String;>;

  public com.baidu.accweather.MyClassString();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
        0       5      0      this   Lcom/baidu/accweather/MyClassString;

  public void setData(java.util.List<java.lang.String>);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field data:Ljava/util/List;
         5: return
      LineNumberTable:
        line 12: 0
        line 13: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0       6     0    this   Lcom/baidu/accweather/MyClassString;
          0       6     1    data   Ljava/util/List;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
          0       6     1    data   Ljava/util/List<Ljava/lang/String;>;
    Signature: #19                          // (Ljava/util/List<Ljava/lang/String;>;)V
}

从setData 方法输出的信息来看, 里面有两张表,一个本地变量表,一个是变量类型表,从 LocalVariableTypeTable 中可以看出来 变量 data 的类型信息是 java/util/List
而最后 Signature 的属性在这里具体指明了是常量池中的#19,这里具体说明了这个方法的信息。

C++拷贝控制

发表于 2017-03-26

当定义一个类的时候,我们显示或者隐式的制定这个类的对象在拷贝,移动,赋值或者销毁的时候做什么,一个类通过定义五中特殊的成员函数来控制这些操作,包括 拷贝构造函数, 拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数

拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。
析构函数定义了此类型对象销毁的时候做什么。

拷贝构造函数
如果一个构造函数的第一个参数子自身类型的引用,并且任何额外类型的参数都有默认值,则此构造函数是拷贝构造参数。

class Foo {
    Foo(); // 默认构造参数
    Foo(Foo& f); // 拷贝构造参数
}

一般而言,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中,编译器从给定对象中依次将每个非 static的成员拷贝到正在创建的对象中。

每个成员的类型决定了它是如何拷贝的:类类型的成员会使用其拷贝构造函数,内置类型则是直接拷贝,虽然我们不能拷贝一个数组,但是合成拷贝函数会逐元素的拷贝一个数组类型的成员,如果数组元素是类类型,则使用元素的拷贝构造函数来拷贝。

拷贝初始化不仅发生在使用 = 定义变量的时候发生,还有以下情况

  • 将对象作为一个实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象(方法不能返回一个方法内对象的引用)
  • 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。

拷贝构造参数被从来初始化非引用类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果不是,那么调用拷贝构造函数的时候,其参数有需要拷贝初始化,这样就陷入了死循环

拷贝构造函数必须是存在且可以访问的(例如不能是private)

Point foo(Point p) { // 传递参数调用一次
    Point local = p;  // = 调用一次
    Point* heap = new Point(p); // 调用一次
    *heap = p; // 不会调用拷贝构造函数
    Point pa[4] = {local, *heap}; // 调用两次
    return local;
}

拷贝赋值运算符( = 操作符 )
与类控制其对象如何初始化一样, 类也可以控制其对象如何赋值
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符,合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。

析构函数
析构函数执行与构造函数相反的工作,析构函数释放对象使用的资源,并且销毁对象的非static成员数据,析构函数是一个类的成员函数,名字由 ~ 加上类名构成,它没有返回值也没有参数,所以析构函数不能被重载。

在一个析构函数中首先执行函数体然后销毁成员,成员按照初始化的顺序逆序销毁。销毁类类型的成员需要执行成员自己的析构函数,内置类型没有析构函数,因此销毁内置类型成员什么也不需要做,需要注意的是,当隐式的销毁一个内置类型的成员不会delete 它所指向的对象。

与普通指针不同,智能指针是类类型,所有具有析构函数,只能指针成员在析构阶段会被自动销毁。

合成析构函数
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数,类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。

成员是在析构函数之后隐含的析构阶段被销毁的,在整个对象被销毁的过程中,析构函数作为成员销毁步骤之外的另一部分而进行鍀

三/五法则

  • 需要析构函数的类需要拷贝和赋值操作
  • 需要拷贝操作的类也需要赋值操作,反之亦然

阻止拷贝

  • 定义删除的函数,在函数的参数列表后面加上=delete,来指出我们希望将它定义为删除的。=delete通知编译器以及我们代码的读者,我们不希望定义这些成员,因此可以使用 = delete 放在拷贝参数的后面来阻止拷贝。但是需要注意的是析构函数不能使用=delete. 如果析构函数被删除,那么久无法销毁此类型的对象了,对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或者创建该类型的变量和是临时对象。对于定义了析构函数的类型,虽然我们不能定义这种类型的变量或者成员,但是可以动态的分配这种类型的对象。
    本质上,当含有不可能拷贝,赋值或者销毁的成员时,类的合成拷贝控制函数就被定义成删除的、
  • 在新的标准发布之前,类是通过将其拷贝函数和拷贝赋值运算符定义为private 来阻止拷贝。

希望阻止拷贝的类应该使用=delete 来定义他们自己的拷贝构造函数和拷贝赋值运算符、

拷贝控制和资源管理

通常定义类外管理资源的类,都必须定义拷贝控制成员,为了定义这些成员,我们必须定义此类对象的拷贝语义,一般来说有两种选择,可以定义拷贝操作使类的类型看起来像一个值或者像一个指针,类的行为像一个值意味着它应该有自己的状态,副本和对象完全是独立的,改变副本也不会对原有的对象由任何影响,反之亦然。 行为像指针的类则共享状态,当我们拷贝这类的对象时,副本和对象使用相同的底层数据,改变副本也会改变对象,反之亦然。

赋值运算符
当编写赋值运算符的时候有两点需要注意

  • 如果将一个对象赋予它自身,赋值运算符必须能正确的工作
  • 大多数赋值运算符组合了析构函数和拷贝构函数的工作。

当编写一个赋值运算符的饿时候,一个好的模式是现将右侧运算对象拷贝到一个局部临时变量中,当完成拷贝之后销毁左侧运算对象的现有成员就是安全的了,完成之后就可以将剩下的数据拷贝到左侧对象的成员中了。

定义行为像指针的类

对行为类似指针的类,我们需要为期定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是其指向的对象,自定义的类任然需要自己的析构函数来释放接受构造函数所指向的内存,但是析构函数不能单方面的释放内存,只有当最后一个指针被销毁的时候才能释放所指向的内存。令一个类展现类似指针最好的方法就是使用shared_ptr。但是如果我们希望自己直接来管理,那么使用引用计数会是一个很好的选择。

引用计数
引用计数的工作方式如下:

  • 除了初始化对象,每个构造函数都要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态,当我们创建一个对象的时候,只有一个对象共享状态,因此计数器的值为1
  • 拷贝构造函数不分配新的计数器,而是与拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器。
  • 析构函数递减计数器,表示共享状态的对象少了一个,当一个计数器为零的时候,析构函数需要释放对象。
  • 拷贝赋值运算符递增右侧运算对象的计算器,递减左侧运算对象的计数器,如果左侧运算对象的计数器为零,那么拷贝赋值运算符需要销毁对象。

C++ swap 函数

在大多数时候,使用默认的swap函数就足够了, 但是对于一些自定义的类型来说就不是了,因为在默认的实现中,交换两个对象需要一次拷贝和两次赋值,但是对于某些类型来说,拷贝的代价太大,所以需要尽可能的避免拷贝,同时,在自定义的类型的成员变量中,存在成员指针,这个时候我们只需要交换指针即可,而不需要拷贝指针所指向的对象。

移动函数
在C++的新特性中,一个很重要的特性就是移动而非拷贝的能力,因为在很多情况下,我们在完成了拷贝之后马上就将拷贝对象原来所占用的内存销毁掉了,如果这个时候使用移动而非拷贝就可以避免一部分开销,有助于性能的提升。

右值引用

在新的C++标准中为了支持移动操作,添加了这一种新的引用类型,右值引用。所谓的右值引用就是必须绑定到右值上, 可以通过&& 来绑定到一个右值上。左值引用这右值引用有明显的区别:左值有持久的状态,而右值要么是常亮要么是求值过程中临时创建的对象。右值引用只能绑定到临时对象,那么右值引用的对象即将被销毁,而且该对象没有其他的用户=。

变量是一个左值,变量可以看做只有一个运算对象而没有运算符的表达式。因此我们不能将右值引用绑定到一个对象上,即使这个变量是右值引用类型也不行。

虽然不能直接将一个右值引用绑定到一个左值上,但是可以使用std::move() 函数来显示的将一个左值引用转换为一个右值引用。但是需要注意的一点是:调用move就意味着除了对传入的引用的赋值或者销毁外,将不再使用这个引用。在调用move之后,不能对对象的状态作任何假设。

移动构造函数
移动构造函数的第一个参数是该类类型的一个右值引用,除此之外所有其他的参数都应该有默认值除了完成资源的移动,移动构函数还必须保证对移动源对象的销毁操作是无害的。特别需要注意的是,一旦资源完成移动,源对象就不在只想被移动的资源。

移动操作,标准库容器和异常
由于移动构造函数通常只是移动资源而不分配任何资源,所以移动操作通常不会抛出任何异常。当我们需要定义一个不抛出异常的函数时,我们需要在函数声明的后面手动加上noexcept,如果我们不这样做,那么标准库会认为移动自定义的对象可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。

为什么需要noexcept:

  • 虽然移动操作通常不会抛出异常,但是抛出异常是允许的
  • 标准库容器能对异常发生时自身的行为提供保障

比如有以下情况的存在,自定义类

class A {
    void * data1;
    void * data2;
}

当需要完成移动构造函数时,如果在移动了data1 之后出现了异常,无法完成data2的移动,那么这个时候将无法满足A 自身保持不变的情况(data1已经被改变)。 另一方面,如果使用拷贝构造函数,那么这个时候无论拷贝有没有出现异常,原来的A都不会受到影响。

因此为了避免这种潜在的问题,除非标准库知道元素类型在移动构造函数不会抛异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望标准库容器在重新分配内存的情况使用自定义的移动构造函数而不是拷贝构造函数就必须将移动构造函数声明为noexcept。

移动赋值运算符
移动赋值运算符执行与析构函数和移动构造函数相同的工作,与移动构造函数一样,如果移动赋值运算符不抛出任何异常,应该将其标记为noexcept,类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值的情况。

下面是使用移动构造函数和移动赋值运算符的一个示例、

#include <string>
#include <iostream>
#include <vector>
#include <utility>

using namespace std;
class MyClass {

private:
    string* name;

public:
    MyClass(string * name_ = new string("MyClass....")) : name(name_) {

    }   

    MyClass(const MyClass & other) {
        this->name = new string(*other.name);
        cout<<"copy construct is invoked"<<endl;
    }

    MyClass(MyClass && right) noexcept {
        if (this != &right) {
            delete name;
            this->name = right.name;
            right.name = nullptr;
        }
        cout<<"move construct is invoked"<<endl;
    }

    ~MyClass() {
        if (name) {
            delete name;
        }
    }

    string getString() const {
        return *name;
    }

    MyClass& operator=(MyClass && right) noexcept {
        if (this != &right) {
            delete name;
            this->name = right.name;
            right.name = nullptr;
        }
        cout<<"move assigment is invoked"<<endl;
        return *this;
    }
};

int main() {
    string name = "xxxxxx";
    MyClass test(&name);
    MyClass test1;
    test1 = std::move(test);
    cout<<test1.getString()<<endl;
}

移动后源对象必须可析够

从一个源对象移动数据并不会销毁这个对象,但是有时候在完成移动操作之后,源对象会被销毁,因此当我们编写一个移动操作时,必须确保一个源对象进入可析够状态。

所有五个拷贝控制成员应该看做一个整体:一般说来,如果一个类定义了任何一个拷贝操作,它就应该定义所有的五个操作,如前所述,这些类必须定义拷贝构造函数,拷贝赋值运算符和析构函数才能正确工作,这些类通常拥有一个资源,而拷贝成员必须拷贝此资源,一般说来,拷贝一个资源会导致一些额外开销,在这种拷贝非必要的情况下,定义了移动构造函数和赋值运算符的类就可以避免此问题。

需要注意的是,不建议随意使用移动操作,由于一个移动源对象具有不确定状态,对其调用std::move是危险的,当调用std::move的时候,必须确定移动后源对象没有其他的用户,通过在类代码中小心使用move可以大幅提升性能。

右值和左值引用成员函数
在旧的标准中,不能阻止对对右值进行赋值,为了维持向后兼容性,新的标准库任然允许想右值赋值,但是在自定义的类中,可以阻止这种情况的出现。

Linux学习笔记之进程

发表于 2017-02-14

进程是多道程序设计的操作系统中的基本概念,通常把进程定义为程序执行的一个实例, 例如16个user在使用vi,那么操作系统就有16个独立的进程,尽管他们共享同一个可执行代码。

进程,轻量级进程和线程
当一进程创建的时候,它几乎与父进程相同,它接受父进程地址空间的一个(逻辑)拷贝(假设当前进程A中定义了一个变量X,其地址为 0x01,那么拷贝之后,子进程中对应的X,地址空间任然为0x01,虽然两者的地址一样,但是这只限于在虚拟内存的地址空间内,两者在物理地址上肯定是不一样的)。并且从进程创建系统调用的下一条指令开始执行与父进程相同的代码,尽管父子进程可以共享有程序代码的页,但是他们各自有独立的数据拷贝(堆和栈),因此子进程对内存单元的修改对父进程来说是可见的.

进程描述符
进程描述符中包含了与一个进程相关的所有信息,它不仅包括了很多进程属性的字段,而且一些字段还包括了指向其他数据结构的指针。进程描述符都是task_struct结构。

进程的状态
进程描述符中的state字段描述当前进程的状态,进程每种状态是互斥的。

  • 运行状态(TASK_RUNNING)
  • 可中断的等待状态(TASK_INTERRUPTIBLE):进程被挂起,知道某个条件为真。
  • 不可中断的等待状态(TASK_UNINTERRUPTERABLE) 这个时候即使把信号传递到挂起进程也不能改变它的状态,在一些特定的情况下这种状态很有用,例如,当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备时就会用到这种状态,探测完成之前,设备驱动程序不能被中断,否则,硬件设备处于不可知的状态。
  • 暂停状态(TASK_STOPPED) 进程的执行被暂停,当进程接收到SIGSTOP 等信号的时候进入暂停状态。
  • 跟踪状态(TASK_TRACED)进程的执行由debugger程序暂停,当一个进程被另一个进程监控的时候(例如debugger执行ptrace()系统调用监控一个测试程序),任何程序都可以把这个程序置于TASK_TRACED.
  • 僵死状态(EXIT_ZOMBIE) 进程的执行被终止,但是父进程还没有发布wait4() 或者 waitpid() 系统调用来返回死亡进程的信息,发布wait()类系统调用前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能还需要它。
  • 僵死撤销状态(EXIT_DEAD) 最终状态,由于父进程刚发出wait4()或者waitpid()系统调用,因而进程由系统删除。

标识一个进程
一般来说能被独立调度的每个执行上下文都必须拥有它自己的进程描述符,因此即使共享内核大部分数据结构的轻量级线程,也有他们自己的task_struct结构。
进程和进程描述符之间有非常严格的一一对应的关系,这使得用32位进程描述符地址标识进程成为一种方便的方式,进程描述符指针指向这些地址,内核对进程的大部分引用是通过进程描述符指针进行的。

类Unix操作系统允许用户使用一个叫做进程标识符的processID(PID)的数字来标识进程,PID存放在进程描述符的pid字段中,PID被顺序编号,新建的PID通常是前一个进程的PID加1,pid的值有一个上限,在默认情况下是 32767, 但是系统管理通过修改/proc/sys/kernel/pid_max来减小这个值。同时在64位的体系中可以把PID的上限扩大到 4194303。当内核的使用的PID达到这个上限的时候必须开始循环使用已闲置的小PID号。

由于循环使用PID编号,内核必须通过一个pidmap-array 位图来表示当前已经分配的PID号和闲置的PID号,因为一个页框包含了32768 (4 1024 8)个位,所以在32位体系结构中位图单独放在一个页中,在64位体系结构中,当内核分配了超过当前位图大小的pid号时,需要为PID位图增加更多的页。系统会一直保存这些页不会释放掉。

进程的切换
尽管每个进程都可以拥有属于自己的进程空间,但是所有的进程都必须共享CPU寄存器,因此在CPU恢复一个进程的执行之前,内核必须必须确保每个寄存器装入挂起进程时的值。

进程恢复之前必须装入寄存器的一组数据称为硬件的上下文,硬件上下文是可执行上下文的一个子集,因为执行上下文包含了进程执行时需要的所有信息。在Linux中,进程硬件上下文的一部分存在TSS段(任务状态段(Task State Segment, TSS)是x86架构电脑上是一个保存任务信息的数据结构,被操作系统内核用于任务管理),而剩余部分存放在内核态堆栈中。

thread字段
在每次切换进程的时候,被替换进程的硬件上下文必须要保存在别处,不能像Intel原始设计那样把它保存在TSS中,因为linux为每个处理器而不是每个进程使用TSS.

因此每个进程描述符包含一个thread_struct的tread 字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。这个数据结构包含的字段设计大部分CPU寄存器,但是不包含eax, ebx这类通用寄存器,他们的值保存在内核堆栈中。

执行进程切换
进程切换可能只能发生在精心定义的点:schedule()函数。
本质上,每个进程切换由两步组成

  1. 切换页全局目录以安装一个新的地址空间。
  2. 切换内核堆栈和硬件上下文

创建进程
为了避免子进程拷贝时耗费资源的问题,现在Linux中引入了三种机制

  1. 写时复制技术允许父子进程读相同的物理页,只要两者中有一个尝试写一个物理页,内核就把这个页的内容拷贝到新的物理页,并把这个页分配给正在写的进程。
  2. 轻量级进程允许父子进程共享进程在内核的很多数据结构,如页表(也就是整个用户态地址空间),打开的文件表和信号处理
  3. vfork(),系统调用创建的进程能共享其父进程的内存地址空间,为了防止父进程重写子进程需要的数据,子进程创建后将阻塞父进程的执行,一直到子进程退出或执行一个新的程序为止。

clone(), fork()和 vfork();

在linux中,轻量级进程是由clone() 创建的。在传统的Linux中fork使用clone实现,vfork()系统调用在Linux中也是用clone实现的。

do_fork()函数负责处理clone, fork, vfor,系统调用, do_fork() 利用辅助函数copy_process 来创建进程描述符已经进程执行所需的其他所有内核数据。

线程在linux中的实现
liux中实现线程的机制非常的独特,从内核角度来说,它并没有线程这个概念,linux把所有的线程都当做进程来实现,内核并没有定义特别的调度算法和数据结构来表示线程,相反,线程仅仅被视作一个与其他进程共享某些资源的进程,每个线程都有唯一里属于自己的task_struct,所以在内核中他看起来就是一个普通的进程。

内核线程
内核经常需要在后台执行一些操作,这种任务可以通过内核线程(独立运行在内核空间的标准线程)来完成,内核线程和普通的进程间的区别在于内核线程没有的地址空间,它们只在内核空间运行,从来不切换到用户空间去,内核进程和普通进程一样,可以被调度也可以被抢占

内核线程只能由其他内核线程创建,内核线程创建之后需要调用wake_up_process()才能运行,否则它不会主动运行,创建一个进程并让它运行起来,可以调用kthread_run()来达到,这个例程是以宏实现的,只是简单的调用了kthread_create和wake_up_process();

内核线程启动之后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop的参数为kthread_create函数返回的task_struct结构的地址。

int kthread_stop(struct task_struct *k);

进程的终结
一般来说进程的析构是由自身引起的,它发生在exit系统调用时,既可能是显式的调用,也可能是隐式的从程序的主函数返回,C语言的编译器会在函数的返回点后面放置调用exit()的代码,当进程遇到既不能自己处理也不能忽略的信号时,还可能被动的终结。

删除进程描述符
在调用了do_exit()方法之后,尽管线程已经僵死不能运行了,但是系统还是保留了他的进程描述符,这样可以让系统在子进程终结之后仍然能获取它的信息,因此进程终结时所需的额清理工作和进程描述符的删除被分开执行,在进程获取已总结的子进程的信息后,或者内核通知他不再关注该信息后,紫禁城的task_struct才被释放。

wait这一族函数都是通过唯一的系统调用wait4来实现的,它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的pid,此外调用该函数提供的指针会包含子函数退出时的代码,

处理孤儿进程
如果父进程在子进程之前退出,那么系统会给子进程找一个新的父进程,否则这些孤儿进程在退出自后就会永远处于僵死状态而无法释放其所占用的内存,对于这个问题,解决方法是给子进程在当前线程组内找一个进程作为父亲,如果不行就让init做他们的父亲。在do_exit()方法当中会调用exit_notify() 该函数会调用forget_orginal_parent() 而后者会调用find_new_reaper()来执行寻父的过程。

123…5
Tom Liu

Tom Liu

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