*** DONE 1. 熟悉 OkHttp,OKio的实现原理
1. 生成Sink对象
2. 生成RealBufferedSink对象
3. 写操作是调用writeInt()方法
4. 将数据写入到Segment中,Segment是一个双向链表。调用write方法写入数据
之后,然后调用Segment的pop释放存储的消息。
5. 读取时调用readInt()方法,然后放入到Segment中,Segment是片段的意思。
Okio将数据也就是Buffer分割成一块块的片段。Segment拥有前置节点和后置
节点。Segment的片段中是用数组存储的。
6. Segment的优化:一个有共享内存的Segment是不能写入的。
7. SegmentPool是按照单向链表存储的,上限是64K,相当于8个Segment
8. Forwarding流:因为传入的Sink不能继承和复写,这样可以通过装饰
Forwarding流的方式来监听和拦截一些操作
9. DeflaterSink 和 InflaterSource流:这两对流主要是对应zip压缩流,类似
ZipInputStream 和 ZipOutputStream.
10. GzipSource 和 GzipSink 流: Gzip 和 Zip 的主要区别在于平台的通用性
和压缩率,一遍情况下,Gzip 的压缩率更高些。
11. Pipe 流:Okio 中的 pipe 类似生产这消费者的模式,在管道流PipeSource读
取的时候,发现 Buffer.size, 也就是缓冲池数据长度为0的时候,管道
PipeSource 流陷入等待。一直等到 PipeSink 流往 Buffer 中再输入的时
候,阻塞消失,并且管道的流一定是成对出现的。
*** DONE 2. 熟悉EventBus实现原理
1. EventBus 中维护一个 mMethodHunter 对象,该对象用来查找 this 即
Subscirber 的所有标有注解的所有方法。
2. SubscirberMethodHunter 和 EventBus 中的 mSubcriberMap 是同一个对象。
键值对是 Key:EventType value:CopyOnWriteArrayList(Subscription) 用
于保存一个 EventType 类型, 查找该类型对应的所有的 Subscription, 然
后全部执行。
3. EventBus.getDefault().post事件:
在执行 post 方法之后,会将 EventType 对象放入到
ThreadLocal<Queue<Eventtype>> 中, 然后用事件分发器,从消息队列中取出
EventType 对象,然后 调用 getMatchedEventTypes方法 得到
List<Eventtype> 集合,遍历集合中的 Eventtype 对象之后,调用
handleEvent()方法, 在 handleevent 方法里面,遍历所有的 Subscription
对象,根据不同的 ThreadMode 得到不同的 EventHandler 处理不同的事件。
*** DONE 3. 熟悉RxJava实现原理
1. RxJava 是响应式编程,用一个字概括就是流(Stream)。 Stream
就是一个按时间排序的 Events 序列,它可以放射三种不同的
Events,(某种类型的) Value, Error 或者一个 “Completed”
Signal. 通过分别为 Value, Error, "Completed" 定义事件处理函
数,我们将会异步地捕获这些 Events。基于观察这模式, 事件流
从上往下,从订阅源传递到观察者。
2. RxJava 的优点, 它可以避免回调嵌套, 更优雅地切换线程实现异
步处理数据。配合一些操作符,可以让处理事件的代码更加简洁,
逻辑更加清晰。
3. 在 RxJava 里面,有两个必不可少的角色:Subscriber(观察者) 和
Observable (订阅源)
4. Subscriber 在RxJava 里面是一个抽象类, 实现了 Observer 接口。
5. Observable (订阅源)在 RxJava 里面是一个大而杂的类,拥有很
多工厂方法和各式各样的操作符。
6. 如何理解 observeOn 可以多次调用,实现线程的多次切换,最终目
标 Subscriber 的执行线程与最后一次 observeOn() 有关。但
subscribeOn 多次调用只有第一个 subscribeOn() 起作用。
因为 observeOn()作用的是 Subscriber, Subscirber 是最后调用
的,执行的顺序是从上往下执行的,所以不管前面执行多少个
observeOn() 方法,最后都会被最后一个 observeOn()方法给替代。
前面的 observeOn() 方法也会执行,但是最终被切换到的线程是最后
一次调用的 observeOn() 方法。
subscribeOn() 作用的对象是 Observable, 执行的顺序是从下往上,
所以不管后面执行多少个 subscribeOn() 方法,最终执行的方法还
是在第一个 subscribe() 里面。
7. 线程切换调度器 AndroidSchedulers, 具体实现类
LooperScheduler。Looperscheduler 内部持有一个 Handler, 用于
线程的切换。 在 Worker 的 schedule(Action0 action, ...) 方
法中,将 action 通过 Handler 切换到所绑定的线程中执行。
*** DONE 4. 熟悉Retrofit实现原理
1. 定义接口类型
public interface GitHub {
@GET("/repos/{owner}/{repo}/contributors")
Call<List<Contributor>> contributors(
@Path("owner") String owner,
@Path("repo") String repo
);
}
2. Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
Github github = retrofit.create(Github.class)
3. Call<List<Contributor>> call = github.contributors("square", "retrofit");
4. Retrofit 使用注解 + java 接口来定义后台服务 API 接口
注解主要分为 方法注解 和 参数注解
方法注解:
1. @GET
2. @POST
3. @PUT
4. @DELETE
5. @PATCH
6. @HEAD
7. @OPTIONS
8. @HTTP
9. @FORMUrlEncoded
10. @Multipart
11. @Headers
12. @Streaming
参数注解:
1. @Url
2. @Path
3. @Body
4. @Field
5. @FieldMap
6. @Part
7. @PartMap
8. @Query
9. @QueryMap
1. Retrofit 使用的关键一步就是 Retrofit.create 函数创建接口动态代理的
示例,为接口的每个 method 创建一个对应的 ServiceMethod ,并且使用
ServiceMethod 创建 OkHttpCall, 并使用 ServiceMethod 实例的
callAdapter 来调用 okhttpCall 返回结果。
2. 调用流程主要有三步
1. 加载对应的 method 的 ServiceMethod 实例
2. 使用 ServiceMethod 实例和方法调用参数创建 OkhttpCall
3. 调用 serviceMethod.callAdapter.adapt(okHttpCall) 来产生 method
所定义的 (Call<T> 或者其他定义 CallAdapter 支持的返回)
*** 5. MediaScanner的理解
1. Mediascanner 和媒体文件扫描有关,
*** DONE 6. Retrofit + RxJava2.x 实现app的网络层
1. 依赖导入
//okhttp
compile 'com.squareup.okhttp3:okhttp:3.7.0'
compile 'com.squareup.okio:okio:1.12.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.7.0'
//gson
compile 'com.google.code.gson:gson:2.8.0'
//retrofit2
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.2.0'
compile 'com.squareup.retrofit2:converter-scalars:2.2.0'
compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
//rxJava
compile'io.reactivex.rxjava2:rxjava:2.0.1'
compile'io.reactivex.rxjava2:rxandroid:2.0.1'
2. net 包下两个拦截器以及自定义 Observer
1. RequestInterceptor /**
* 类名称:请求前拦截器,这个拦截器会在okhttp请求之前拦截并做处理
*/
public class RequestInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
//请求定制:添加请求头
Request.Builder requestBuilder = original
.newBuilder()
.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
//设置cookie
// String cookie= App.getCookie();
// if (StringUtil.checkStr(cookie)) { //cookie判空检查
// requestBuilder.addHeader("Cookie", cookie);
// }
//如果是post的情况下,请求体定制:统一添加参数,此处演示的是get请求,因此不做处理
if (original.body() instanceof FormBody) {
FormBody.Builder newFormBody = new FormBody.Builder();
FormBody oidFormBody = (FormBody) original.body();
for (int i = 0; i < oidFormBody.size(); i++) {
newFormBody.addEncoded(oidFormBody.encodedName(i), oidFormBody.encodedValue(i));
}
//当post请求的情况下在此处追加统一参数
// String client = Constants.CONFIG_CLIENT;
//
// newFormBody.add("client", client);
requestBuilder.method(original.method(), newFormBody.build());
}
return chain.proceed(requestBuilder.build());
}
}
2. ResponseInterceptor
/**
* 结果拦截器,这个类的执行时间是返回结果返回的时候,返回一个json的String,对里面一些特殊字符做处理
* 主要用来处理一些后台上会出现的bug,比如下面声明的这三种情况下统一替换为:null
*/
public class ResponseInterceptor implements Interceptor {
private String emptyString = ":\"\"";
private String emptyObject = ":{}";
private String emptyArray = ":[]";
private String newChars = ":null";
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
ResponseBody responseBody = response.body();
if (responseBody != null) {
String json = responseBody.string();
MediaType contentType = responseBody.contentType();
if (!json.contains(emptyString)) {
ResponseBody body = ResponseBody.create(contentType, json);
return response.newBuilder().body(body).build();
} else {
String replace = json.replace(emptyString, newChars);
String replace1 = replace.replace(emptyObject, newChars);
String replace2 = replace1.replace(emptyArray, newChars);
ResponseBody body = ResponseBody.create(contentType, replace2);
return response.newBuilder().body(body).build();
}
}
return response;
}
}
3. bean 包下的 HttpResult 类
4. SchedulersTransformer 调度器 此处和 RxJava 1.x 有很大的不同
5. HttpResultFunc 这个类要针对接口编写
6. ApiService
public class ApiService {
private ApiInterface mApiInterface;
private ApiService() {
//HTTP log
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
//RequestInterceptor
RequestInterceptor requestInterceptor = new RequestInterceptor();
//ResponseInterceptor
ResponseInterceptor responseInterceptor = new ResponseInterceptor();
//OkHttpClient
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.addInterceptor(requestInterceptor)
.addInterceptor(responseInterceptor);
// 通过你当前的控制debug的全局常量控制是否打log
if (Constants.DEBUG_MODE) {
builder.addInterceptor(httpLoggingInterceptor);
}
OkHttpClient mOkHttpClient = builder.build();
//Retrofit
Retrofit mRetrofit = new Retrofit.Builder()
.client(mOkHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl("http://xxx.com/")//替换为你自己的BaseUrl
.build();
mApiInterface = mRetrofit.create(ApiInterface.class);
}
//单例
private static class SingletonHolder {
private static final ApiService INSTANCE = new ApiService();
}
//单例
public static ApiService getApiService() {
return SingletonHolder.INSTANCE;
}
/**
* 获取健康信息
*/
public void get_health(Observer<DataBean> observer, Map<String, Object> map) {
mApiInterface.healthInfo(map)
.compose(SchedulersTransformer.io_main())
.map(new HttpResultFunc<>())
.subscribe(observer);
}
}
3. HttpObserver
*** DONE 7. 熟悉 ButterKnife 实现原理
1. 整体的原理-(编译时期-注解处理器)
在 java 代码的编译时期, javac 会调用 java 注解处理器进行处理。
因此我们可以定义自己的注解处理器来干一些事情。一个特定注解的
处理器以 Java 源代码(或者已编译的字节码)作为输入,然后生成
一些文件(通常是 java 文件)作为输出。因此我们可以在用户已有
的代码上添加一些方法,来帮助我们做一些有用的事情。这些生成的
java 文件跟其他手动编写的 java 源代码一样,将会被 javac 编译。
2. 定义处理器, 继承AbstractProcessor。在 java 中定义自己的处理
器都是继承自 AbstractProcessor, 前三个方法都是固定的写法,主
要要是 process 方法。
1. public SourceVersion getSupportedSourceVersion();
//用来指定你使用的 java 版本。
2. public synchronized void init(ProcessingEnvironment
processingEnv);
//会被处理器调用,可以在这里获取 Filer, Elements,
Messager 等辅助类。
3. public Set<String> getSupportedAnnotationTypes();
//这个方法返回 String 类型的 Set 集合,集合里面包含了你需
要处理的注解。
4. public boolean process(Set<? extends TypeElement>
annoations, RoundEnvironment env);
//核心方法, 这个一般的流程就是先扫描查找注解,再生成
java 文件
3. 注解你的处理器
要像 jvm 调用你写的处理器, 你必须先注册。 google 为我们提供
了一个库,简单的一个注解就可以。
compile 'com.google.auto.serivce:auto-service:1.0-rc2'
4. 基本概念
Elements: 一个用来处理 Element 的工具类
Types: 一个用来处理 TypeMirror 的工具类
Filer: 你可以使用这个类来创建 java 文件
5. process 方法里的 Map 对象
Map.Entry<TypeElement, BindingClass>
保存的是所有的注解信息
1. private Map<TypeElement, BindingClass>
findAndParseTargets(RoundEnvironment env)
//每个注解的查找与解析
1. parseResourceArray()
//Process each @BindArray element
2. paraseBindView();
//Process each@BindView element
1. getOrCreateTargetClass(); 得到新的 BindingClass
3. findAndParseListener();
//Process each annoation that corresponds to a
listener
4. findParentType()
//try to find a parent binder for each
5. Class // TypeElement
Attribute (属性变量) // VariableElement
Method // ExecuteableElement
6. 判断父节点,如果是 private 类,抛出异常
类不能是 private 修饰,可以是默认的或者 public
成员变量不能是 private 修饰,可以是默认的或者 public
7. isBindingInWrongPackage(),就是不能在 android,java
这种源码的sdk中使用,如果你包是以 android 或者 java 开
头就会抛出异常。
8.
2.
1. java文件的生成。
compile 'com.squareup:javaopet:1.7.0'
遍历 targeClassMap 集合,调用每一个类的 brewJava() 方法, 最
后返回 JavaFile 对象,再通过 writeTo 方法生成 java 文件
主要步骤:
1. 生成类名
2. 生成构造函数
createBindingConstructor() 方法
1. MethodSepc:生成方法的辅助类
2.
3.
4.
3. 生成 unbind 方法
*** DONE 8. ClassLoader 的理解
1. Classloader 的 defineClass, loadClass, findClass 方法分别有
什么区别
defineClass(String name, byte[] b, ...): 用来把字节数组b 中的
内容转换成 class 类,返回的是 java.lang.Class 类型。
loadClass(String name): 加载名称为 name 的类, 返回的 结果是
java.lang.Class 类实例;
findClass(String name): 用来查找名称为 name 的已经被该
ClassLoader 加载过的类, 返回的结果是 java.lang.Class 类实例。
2. ClassLoader 的作用是根据一个指定的类名称扎到或者生成其对应的
字节代码,然后把字节码转换成一个 Java 类(即 java.lang.Class
实例),除此之外还负责加载 Java 应用所需的资源, Native Lib
库等。
3. Java 的类加载器:系统类加载器和应用开发自定义类加载器。
系统类加载器:
1. 引导类加载器(bootstrap class loader) : 用来加载 Java 核
心库, 是虚拟机中用原生代码实现的,没有继承自 ClassLoader.
2. 扩展类加载器(extensions class loader): 用来加载 Java 的扩
展库, 虚拟机的实现会提供一个默认的扩展库目录,该类加载器
在此目录里面查找并加载 Java 类。
3. 系统类加载器:用来加载应用类路径(CLASSPATH)下的 class,
一般来说 JAVA 应用的类都是由他来完成加载的,可以通过
ClassLoader.getSystemClassLoader() 来获取。
4. 除了引导类加载器之外, 所有的其他类加载器都有一个父类加载
器,系统类加载器的父类加载器是扩展类加载器, 而扩展类加载
器的父类加载器引导类加载器。开发自定义的类加载器的父类是
加载此类加载器的 Java 类的类加载器。
5. JAVA 虚拟机是如何判断两个 Class 类是相同?
JAVA 虚拟机不仅要看类的全名是否相同(含包名路径),还要看
加载此类的类加载器是否一样,只有两者都相同的情况下才认为
两个类是相同的。即便是同样的字节码,被不同的类加载器加载
之后得到的类也是不同的。做插件加载的时候要注意。
4.
*** DONE 9. JNI 使用总结
**** JNI 使用总结(一)
1.基本类型,字符串, 数组
1. 使用了 GetStringUTFChars 方法之后,一定要调用
ReleaseStringUTFChars
2. JNI 支持字符串在 Unicode 和 UTF-8 两种编码之间转换。 Unicode
字符代表了16-bit的字符集合。 UTF-8 字符串使用一种向上兼容
7-bit ASCII 字符串的编码协议。UTF-8 字符串很像 NULL 结尾的 C
字符串。所有的7-bit ASCII 字符的值都在1~127之间,这些值在
UTF-8编码中保存原样。一个字节如果最高位被设置了,意味着这是
一个多字节字符(16-bitUnicode值)
3. 不要忘记检查 GetStringUTFChars,因为 JVM 需要为新诞生的
UTF-8 字符串分配内存,这个操作有可能因为内存太少而失败。失败
是 GetStringUTFChars 会返回 NULL, 并抛出 OutOfMemoryError 异
常。这些 JNI 抛出的异常与 Java 中的异常不同的,一个由 JNI 抛
出来的异常不会改变程序的执行流,因此,我们需要一个显示的
Return 语句来跳过 C 函数中的剩余语句。
1. 构造新的字符串
1. NewStirngUTF 在本地方法中创建一个新的 java.lang.String 字
符串对象。这个新创建的字符串对象拥有一个与给定的 UTF-8 编
码的 C 类型字符串内容相同的 Unicode 编码字符串。
如果 VM 不能为构造 java.lang.String 分配足够的内存,
NewsStringUTF 会抛出一个 OutOfMemoryError 异常,并返回一
个 NULL.
2. GetStringChars 和 ReleaseSTringChars 获取以 Unicode 格式
编码的字符串。当操作系统支持 Unicode 编码的字符串时,这些
方法很有用。
3. UTF-8 字符串以 ‘\0’ 结尾, 而 Unicode 字符串不是,如果
jstring 指向一个 Unicode 编码的字符串,为了得到这个字符串
的长度,可以调用 GetStringLength. 如果一个 jstring 指向一
个 UTF-8 编码的字符串, 为了得到这个字符串的字节长度,可
以调用标准 C 函数 strlen。或者直接对 jstring 调用 JNI 函
数 GetStringUTFLength, 而不用管 jstring 指向的字符串的编
码格式。
4. GetStringChars 和 GetStringUTFChars 函数中第三个参数需要
更进一步的解释。const jchar*
GetStringChars(JNIEnv *env, jstring str, jboolean
*isCopy);
当从 JNI 函数 GetStringChars 中返回得到字符串 B 时,如果
B 是原始字符串 java.lang.String 的拷贝,则 isCopy 被赋值
为 JNI_TRUE。如果 B 和原始字符串指向的是 JVM 中同一份数据,
则 isCopy 被赋值为 JNI_FALSE。当 isCopy 值为 JNI_FALSE 时,
本地代码决不能修改字符串的内容,否则 JVM 中的原始字符串
也会被修改,这会打破 JAVA 语言中字符串不可变的规则。
通常,因为你不关心 JVM 是否会返回原始字符串的拷贝,你只需
要为 isCopy 传递 NULL 作为参数。
JVM 是否会通过拷贝原始 Unicode 字符串来生成 UTF-8 字符串
是不可以预测的,程序员最好假设他会进行拷贝,而这个操作是
花费时间和内存的。一个典型的 JVM 会在 heap 上为对象分配内
存。一旦一个 JAVA 字符串对象的指针被传递给本地代码, GC就
不会再碰这个字符串。换言之,这种情况下,JVM 必须 pin 这个
对象,可是,大量的 pin 一个对象是会产生内存碎片的。因为,
虚拟机会随意性地来选择是复制哈市直接传递指针。
当你不再使用一个从 GetStringChars 得到的字符串时,不管
JVM 内部是采用复制还是直接传递指针的方式,都不要忘记调用
ReleaseStringChars。根据方法 GetStringChars 是复制还是直
接返回指针, ReleaseStringChars 会释放复制对象时所占的内
存,或者 unpin 这个对象。
5. 为了提高 JVM 返回字符串直接指针的可能性, JDK1.2 中引入了
一对新函数, Get/ReleaseStringCritical。表面上, 它们和
Get/ReleaseStringChars 函数差不多,但实际上这两个函数在使
用有很大的限制。
使用者两个函数时,你必须两个函数中间的代码是运行在
“cirtical region(临界区)”的,即这两个函数中间的本地代码
不能调用任何可以让线程阻塞或者等待 JVM 中的其它线程的本地
函数或 JNI 函数。
有了这些限制, JVM 就可以在本地方法持有一个从
GetStringCritical 得到字符串的直接指针是禁止 GC。当 GC 被
禁止时,任何线程如果触发 GC 的话,都会被阻塞。而
Get/ReleaseStringCritical 这两个函数中间的任何本地代码都
不不可以执行会导致阻塞的调用或者为新对象在 JVM 中分配内存。
否则,JVM 有可能死锁,想象一下这样的场景中;
1. 只要当前线程触发的 GC 完成阻塞并释放 GC 时,由其它线程
触发的 GC 才可能由阻塞中释放出来继续运行。
2. 在这个过程中,当前线程会一直被阻塞。因为任何阻塞性调用
都需要获取一个正在被其它线程持有的锁,而其它线程正等待 GC。
3. Get/ReleaseStringCritical 的交迭调用时安全的,这种情况
下,他们的使用必须有严格的顺序限制。而且,我们一定要记
住检查是否因为内存溢出而导致它的返回值是 NULL。因为
JVM 在执行 GetStringCritical 这个函数时,仍有发生数据
复制的可能性,尤其是当 JVM 内存存储的数组不连续时,为
了返回一个指向连续内存空间的指针, JVM 必须复制所有的
数据。
4. 总之,为了避免死锁, 在 Get/ReleaseStringCritical 之间
不要调用任何 JNI 函数。 Get/ReleaseStringCritical 和
Get/ReleasePrimitiveArrayCritical 这两个函数时可以的。
5. JDK1.2 还一对新增函数: GetStringRegion 和
GetStringUTFRegion。这对函数把字符串复制到一个预先分配
的缓冲区内。
GetStringUTFRegion 这个函数会做越界检查,如果必要的话,
会抛出异常 StringIndexOutOfBoundsException。这个方法与
GetStringUTFChars 比较相似,不同的是,
GetStringUTFRegion 不做任何内存分配,不会抛出内存溢出
异常。
6. JNI 字符串操作函数总结。
对于小字符串来说,Get/SetStringRegion 和
Get/SetString-UTFRegion 这两对函数时最佳选择,因为缓冲
区可以被编译器提前分配,而且永远不会产生内存溢出的异常。
当你需要处理一个字符串的一部分时,使用这对函数也是不错
的,因为它们提供了一个开始索引和子字符串的长度值。另外,
复制少量字符串的消耗是非常小的。
在使用 GetStringCritical 时,必须非常小心。你必须确保
在持有一个由 GetStringCritical 获取到的指针时,本地代
码不会在 JVM 内部分配新对象,或者做任何其它可能导致系
统锁死的阻塞性调用。
下面的例子演示了使用 GetStringCritical 时需要注意的一
些地方。
/** This is not safe */
const char* c_str = (*env)->GetStringcritical(env,
j_str, 0);
if (c_str == NULL) {
/** error handling */
}
fprintf(fd, “%s\n”, c_str);
(*env)->ReleaseStringCritical(env, j_str, c_str);
上面的代码的问题在于,GC 被当前线程禁止的情况下,向一
个文件写入数据不一定安全。例如,另外一个线程 T 正在等
待从文件 fd 中读取数据。假设操作系统的规则是 fprintf
会等待线程 T 完成所有对文件 fd 的数据读取操作,这种情
况下就可能产生死锁: 线程 T 从文件 fd 中读取数据时需要
缓冲区的,如果当前没有足够的内存,线程 T 就会请求 GC
来回收一部分, GC 一旦运行,就只能等到当前线程运行
ReleaseStringCritical 时才可以。而
ReleaseStringCritical 只有在 fprintf 调用返回时才会被
调用。 而 fprintf 这个调用,会一直等待线程 T 完成文件
的读取操作。
2. 访问数组
JNI 在处理基本类型数组和对象数组上面是不同的。对象数组里面是
一些指向对象实例或者其他数组的引用。
本地代码中访问 JVM 中的数组和访问 JVM 中字符串有些相似。看一
个简单的例子
JNI 支持一系列的 Get/Release<Type>ArrayElement 函数,这些函
数允许本地代码获取一个指向基本类型数组的元素的指针。由于 GC
可能不支持 pin 操作,JVM 可能会先对原始数据进行复制,然后返
回指向这个缓冲区的指针。
操作基本类型数组的 JNI 函数的总结:
如果你想在一个预先分配的 C 缓冲区和内存之间交换数据,应该使
用 Get/Set</Type>ArrayRegion 系列函数。这些函数会进行越界检
查,在需要的时候会有可能抛出 ArrayIndexOutOfBoundsException
异常。
对于少量的,固定大小的数组,Get/Set<Type>ArrayRegion 是最好
的选择,因为 C缓冲区可以在 Stack(栈)上被很快的分配,而且复
制少量数组元素的代价是很小的。这对函数的另外一个优点就是,允
许你通过传入一个索引和长度来实现对字符串的操作。
如果你没用一个预先分配到的 C 缓冲区,并且原始数组的长度未定,
而本地代码又不想在获取数组元素的指针时阻塞的话,使用
Get/ReleasePrimitiveArrayCritical 函数对。 就像
Get/ReleaseStringCritical 函数对一样,这对函数很小心的使用,
以避免死锁。
Get/Release<type>ArrayElements 系列函数永远是安全的。JVM 会
选择性地返回一个指针,这个指针可能指向原始数据也可能指向原始
数据复制。
3. 访问对象数组
JNI 提供了一个函数对来访问对象数组。 GetObjectArrayElement
返回数组中指定位置的元素,而 SetObjectArrayElement 修改数组
中指定位置的元素。与基本类型的数组不同的是,你不可能得到所有
的对象元素或者一次复制多个对象元素。字符串和数组都是引用类型,
你要是使用 Get/SetObjectArrayElement 来访问字符串数组或者数
组的数组。
静态本地方法 initInt2DArray 创建了一个给定大小的二位数组。执
行分配合初始化数组的任务的本地方法可以是下面的这样子的。
JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls,
int size) {
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env,"[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown*/
}
result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
if (result == NULL) {
return NULL;/* out of memory error thrown */
}
for (int i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough!*/
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
}
2. 在本地方法中通过调用New或者Find开头的函数生成的 JNI 对象,需要
调用 DeleteLocalRef 方法释放内存
通过调用 Get 开头的函数生成的 JNI 对象,需要你调用 Release开头
的函数释放内存。
1. 调用父类的实例方法
CallNonVirtual<Type>Method
1. 使用 GetMethodId 从一个指向父类的引用当中获取方法 ID.
2. 传入对象,父类,方法 ID 和参数,并调用
CallNonVirtualVoidMethod, CallNonvirtualBooleanMethod 等一系
列函数中的一个。
3. 调用构造函数
JNI 中,构造函数可以和实例方法一样被调用,调用方式也相似。传
入”<init>“作为方法名, ”V“作为返回类型。你可以通过向 JNI 函
数 NewObject传入方法来调用构造函数。
1. 缓存字段 ID 和方法 ID
1. 使用时缓存
字段 ID 和 方法 ID 可以在字段的值被访问或者方法被回调的时
候缓存起来。在方法中使用 static 静态变量 将方法 ID 和 字
段 ID 保存起来。
2. 类的静态初始化过程中缓存字段和方法 ID
我们在使用时缓存字段和方法的ID的话,每次本地方法被调用时
都要检查ID是否已经被缓存。许多情况下,在字段ID和方法ID被
使用前就初始化是很方便的。VM在调用一个类的方法和字段之前,
都会执行类的静态初始化过程,所以在静态初始化该类的过程中
计算并缓存字段ID和方法ID是个不错的选择。
3. 两种缓存ID的方式之间的对比
1. 比起静态初始时缓存来说,使用时缓存有一些缺点
1. 使用时缓存的话,每次使用时都要检查一下
2. 方法ID和字段ID在类被unload时就会失效,如果你在使用
时缓存ID,你必须确保只要本地代码依赖于这个ID的值,
那么这个类不被会unload(下一章演示了如何通过使用JNI
函数创建一个类引用来防止类被unload)。另一方面,如
果缓存发生在静态初始化时,当类被unload和reload时,
ID会被重新计算。
2.
3.
2. 管理局部引用
JDK 提供了一系列的函来管理局部引用的生命周期。这些函数包括:
EnsureLocalCapacity, NewLocalRef, PushLocalFrame,
PopLocalFrame.
JNI 规范中指出,VM 会确保每个本地方法创建至少16个局部引用。
经验表明,这个数量已经满足大多数不需要和 JVM 的内部对象有太
多交互的本地方法。如果真的需要创建更多的引用,本地方法可以通
过调用 EnSureLocalCapacity 来支持更多的局部引用。
#define N_REFS /*the maximum number of local references*/
if (env->PushLocalFrame(N_REFS)){
/* out of memory */
}
env->PopLOcalFrame(NULL);
释放全局引用
当你的本地代码不再需要一个全局引用时,你应该调用
DeleteGlobalRef来释放它。如果你没有调用这个函数,即使这个对
象已经没用了,JVM也不会回收这个全局引用所指向的对象。
当你的本地代码不再需要一个弱引用时,应该调用DeleteWeakGlobalRef来释放
它,如果你没有调用这个函数,JVM仍会回收弱引用所指向的对象,但弱引用本
身在引用表中所占的内存永远也不会被回收。
编写工具函数时,请遵守下面的规则:
1、 一个返回值为基本类型的工具函数被调用时,它决不能造成局部、
全局、弱引用不被回收的累加。
2、 当一个返回值为引用类型的工具函数被调用时,它除了返回的引
用以外,它决不能造成其它局部、全局、弱引用的累加。
在管理局部引用的生命周期中,Push/PopLocalFrame是非常方便的。
你可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用
PopLocalFrame,这样的话,在函数对中间任何位置创建的局部引用
都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。
1. 异常处理
本地代码通常有两种方式来处理一个异常:
1. 一旦发生异常,立即返回,让调用者处理这个异常。
2. 通过 ExceptionClear 清除异常,然后执行自己的异常处理代码。
3. 当一个异常发生之后,必须先检查,处理,清除异常之后再做其
它 JNI 函数调用,否则的话,结果未知。当前线程中有异常的时
候,你可以调用的 JNI 函数非常少,通过来说,当有一个未处理
的异常时,你只可以调用两种 JNI 函数:异常处理函数和清除
VM 资源的函数。
4.
2.
3.
4. JNI 线程同步
if (env->Monitor(obj) != JNI_OK) {
/* error handling */
}
/*synchronized block*/
if (env->Monitor(obj) != JNI_OK) {
/* error handing */
}
运行上面这段代码时,线程必须先进入obj的监视器,再执行同步块中的
代码。MonitorEnter需要传入jobject作为参数。同时,如果另一个线程
已经进入了这个与jobject监视器的话,当前线程会阻塞。如果当前线程
在不拥有监视器的情况下调用MonitorExit的话,会产生一个错误,并抛
出一个IllegalMonitorStateException异常。上面的代码中包含了
MonitorEnter和MonitorExit这对函数的调用,在这对函数的使用时,我
们一定要注意错误检查,因为这对函数有可能执行失败(比如,建立监
视器的资源分配不成功等原因)。这对函数可以工作在jclass、jstring、
jarray等类型上面,这些类型的共同特征是,都是jobject引用的特殊类
型有一个MonitorEnter方法,一定也要有一个与之对应的MonitorExit方
法。尤其 是在有错误或者异常需要处理的地方,要尤其小心。
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
一旦当前线程被附加到JVM上,AttachCurrentThread函数就会返回一个
属于当前线程的JNIEnv指针
有许多方式可以获取JavaVM指针。可以在VM创建的时候记录下来,也可
以通过JNI_GetCreatedJavaVMs查询被创建的虚拟机,还可以通过调用
JNI函数GetJavaVM或者定义JNI_OnLoad句柄接口。与JNIEnv不同的是,
JavaVM只要被缓存在全局引用中,是可以被跨线程使用的。JDK1.2以后
提供了一个新调用接口(invocation interface)函数GetEnv,这样,
你就可以检查当前线程是否被附加到JVM上,然后返回属于当前线程的
JNIEnv指针。如果当前线程已经被附加到VM上的话,GetEnv和
AttachCurrentThread在功 能上是等价的。
5. 混淆ID和引用本地代码中使用引用来访问JAVA对象,使用ID来访问方法
和字段。 引用指向的是可以由本地代码来管理的JVM中的资源。比如
DeleteLocalRef这个本地函数,允许本地代码删除一个局部引用。而字
段和方法的ID由JVM来管理,只有它所属的类被unload时,才会失效。本
地代码不能显式在删掉一个字段或者方法的ID。 本地代码可以创建多个
引用并让它们指向相同的对象。比如,一个全局引用和一个局部引用可
能指向相同的对象。而字段ID和方法ID是唯一的。比如类A定义了一个方
法f,而类B从类A中继承了方法f,那么下面的调用结果是相同的:
jmethodID MID_A_f = (*env)->GetMethodID(env, A, "f", "()V");
jmethodID MID_B_f = (*env)->GetMethodID(env, B, "f", "()V");
Unicode字符串结尾
从GetStringChars和GetStringCritical两个方法获得的Unicode字符串不
是以NULL结尾的,需要调用GetStringLength来获取字符串的长度。一些
操作系统, 如Windows NT中,Unicode字符串必须以两个’\0’结尾,这
样的话,就不能直接 把GetStringChars得到的字符串传递给Windows NT
系统的API,而必须复制一份 并在字符串的结尾加入两个“\0”
访问权限失效
在本地代码中,访问方法和变量时不受JAVA语言规定的限制。比如,可以
修改private和final修饰的字段。并且,JNI中可以访问和修改heap中任
意位置的内 存。这些都会造成意想不到的结果。比如,本地代码中不应
该修改java.lang.String和java.lang.Integer这样的不可变对象的内容。
否则,会破坏JAVA规范。
一个GetStringChars必然要对应着一个ReleaseStringChars
传递数据
像int、char等这样的基本数据类型,在本地代码和JVM之间进行复制传
递,而对象是引用传递的。每一个引用都包含一个指向JVM中相应的对象
的指针,但本地代码不能直接使用这个指针,必须通过引用来间接使用。
**** JNI 使用总结 (二)
本章是JNI设计思想的一个概述,在讲的过程中,如果有必要的话,还会对底层实现技术的原理做说明。本章也可以看作是JNIEnv指针、局部和全局引用、字段和方法ID等这些JNI主要技术的规范。有些地方我们可能还会提到一些技术是怎么样去实现的,但我们不会专注于具体的实现方式,主要还是讨论一些实现策略。
11.1 设计目标
JNI最重要的设计目标就是在不同操作系统上的JVM之间提供二进制兼容,做到一个本地库不需要重新编译就可以运行不同的系统的JVM上面。
为了达到这一点儿,JNI设计时不能关心JVM的内部实现,因为JVM的内部实现机制在不断地变,而我们必须保持JNI接口的稳定。
JNI的第二个设计目标就是高效。我们可能会看到,有时为了满足第一个目标,可能需要牺牲一点儿效率,因此,我们需要在平台无关和效率之间做一些选择。
最后,JNI必须是一个完整的体系。它必须提供足够多的JVM功能让本地程序完成一些有用的任务。
JNI不能只针对一款特定的JVM,而是要提供一系列标准的接口让程序员可以把他们的本地代码库加载到不同的JVM中去。有时,调用特定JVM下实现的接口可以提供效率,但更多的情况下,我们需要用更通用的接口来解决问题。
11.2 加载本地库
在JAVA程序可以调用一个本地方法之间,JVM必须先加载一个包含这个本地方法的本地库。
11.2.1 类加载器
本地库通过类加载器定位。类加载器在JVM中有很多用途,如,加载类文件、定义类和接口、提供命令空间机制、定位本地库等。在这里,我们会假设你对类加载器的基本原理已经了解,我们会直接讲述加载器加载和链接类的技术细节。每一个类或者接口都会与最初读取它的class文件并创建类或接口对象的那个类加载器关联起来。只有在名字和定义它们的类加载器都相同的情况下,两个类或者接口的类型才会一致。例如,图11.1中,类加载器L1和L2都定义了一个名字为C的类。这两个类并不相同,因为它们包含了两个不同的f方法,因为它们的f方法返回类型不同。
图11.1 两个名字相同的类被不同类加载器加载的情况
上图中的点划线表达了类加载器之间的关系。一个类加载器必须请求其它类加载器为它加载类或者接口。例如,L1和L2都委托系统类加载器来加载系统类java.lang.String。委托机制,允许不同的类加载器分离系统类。因为L1和L2都委托了系统类加载器来加载系统类,所以被系统类加载器加载的系统类可以在L1和L2之间共享。这种思想很必要,因为如果程序或者系统代码对java.lang.String有不同的理解的话,就会出现类型安全问题。
11.2.2 类加载器和本地库
如图11.2,假设两个C类都有一个方法f。VM使用“C_f”来定位两个C.f方法的本地代码实现。为了确保类C被链接到了正确的本地函数,每一个类加载器都会保存一个与自己相关联的本地库列表。
图11.2 类加载器和本地库的关联
正是由于每一个类加载器都保存着一个本地库列表,所以,只要是被这个类加载器加载的类,都可以使用这个本地库中的本地方法。因此,程序员可以使用一个单一的库来存储所有的本地方法。
当类加载器被回收时,本地库也会被JVM自动被unload。
11.2.3 定位本地库
本地库通过System.loadLibrary方法来加载。下面的例子中,类Cls静态初始化时加载了一个本地库,f方法就是定义在这个库中的。
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary("mypkg");
}
}
JVM会根据当前系统环境的不同,把库的名字转换成相应的本地库名字。例如,Solaris下,mypkg会被转化成libmypkg.so,而Win32环境下,被转化成mypkg.dll。
JVM在启动的时候,会生成一个本地库的目录列表,这个列表的具体内容依赖于当前的系统环境,比如Win32下,这个列表中会包含Windows系统目录、当前工作目录、PATH环境变量里面的目录。
System.loadLibrary在加载相应本地库失败时,会抛出UnsatisfiedLinkError错误。如果相应的库已经加载过,这个方法不做任何事情。如果底层操作系统不支持动态链接,那么所有的本地方法必须被prelink到VM上,这样的话,VM中调用System.loadLibrary时实际上没有加载任何库。
JVM内部为每一个类加载器都维护了一个已经加载的本地库的列表。它通过三步来决定一个新加载的本地库应该和哪个类加载器关联。
1、 确定System.loadLibrary的调用者。
2、 确定定义调用者的类。
3、 确定类的加载器。
下面的例子中,JVM会把本地库foo和定义C的类加载器关联起来。
class C {
static {
System.loadLibrary("foo");
}
}
11.2.4 类型安全保障措施
VM中规定,一个JNI本地库只能被一个类加载器加载。当一个JNI本地库已经被第一个类加载器加载后,第二个类加载器再加载时,会报UnsatisfiedLinkError。这样规定的目的是为了确保基于类加载器的命令空间分隔机制在本地库中同样有效。如果不这样的话,通过本地方法进行操作JVM时,很容易造成属于不同类加载器的类和接口的混乱。下面代码中,本地方法Foo.f中缓存了一个全局引用,指向类Foo:
JNIEXPORT void JNICALL
Java_Foo_f(JNIEnv *env, jobject self)
{
static jclass cachedFooClass; /* cached class Foo */
if (cachedFooClass == NULL) {
jclass fooClass = (*env)->FindClass(env, "Foo");
if (fooClass == NULL) {
return; /* error */
}
cachedFooClass = (*env)->NewGlobalRef(env, fooClass);
if (cachedFooClass == NULL) {
return; /* error */
}
}
assert((*env)->IsInstanceOf(env, self, cachedFooClass));
... /* use cachedFooClass */
}
上面的例子中,因为Foo.f是一个实例方法,而self指向一个Foo的实例对象,所以,我们认为最后那个assertion会执行成功。但是,如果L1和L2分别加载了两个不同的Foo类,而这两个Foo类都被链接到Foo.f的实现上的话,assertion可能会执行失败。因为,哪个Foo类的f方法首先被调用,全局引用cachedFooClass指向的就是哪个Foo类。
11.2.5 unload本地库
一旦JVM回收类加载器,与这个类加载器关联的本地库就会被unload。因为类指向它自己的加载器,所以,这意味着,VM也会被这个类unload。
11.3 链接本地方法
VM会在第一次使用一个本地方法的时候链接它。假设调用了方法g,而在g的方法体中出现了对方法f的调用,那么本地方法f就会被链接。VM不应该过早地链接本地方法,因为这时候实现这些本地方法的本地库可能还没有被load,从而导致链接错误。
链接一个本地方法需要下面这几个步骤:
1、 确定定义了本地方法的类的加载器。
2、 在加载器所关联的本地库列表中搜索实现了本地方法的本地函数。
3、 建立内部的数据结构,使对本地方法的调用可能直接定向到本地函数。
VM通过下面这几步,同本地方法的名字生成与之对应的本地函数的名字:
1、 前缀“Java_”。
2、 类的全名。
3、 下划线分隔符“_”。
4、 方法名字。
5、 有方法重载的情况时,还会有两个下划线(“__”),后面跟着参数描述符。
VM在类加载器关联的本地库中搜索符合指定名字的本地函数。对每一个库进行搜索时,VM会先搜索短名字(short name),即没有参数描述符的名字。然后搜索长名字(long name),即有参数描述符的名字。当两个本地方法重载时,程序员需要使用长名字来搜索。但如果一个本地方法和一个非本地方法重载时,就不会使用长名字。
JNI使用一种简单的名字编码协议来确保所有的Unicode字符都被转化成可用的C函数名字。用下划线(“_”)分隔类的全名中的各部分,取代原来的点(“.”)。
如果多个本地库中都存在与一个编码后的本地方法名字相匹配的本地函数,哪个本地库首先被加载,则它里面的本地函数就与这个本地方法链接。如果没有哪个函数与给定的本地方法相匹配,则UnsatisfiedLinkError被抛出。
程序员还可以调用JNI函数RegisterNatives来注册与一个类关联的本地方法。这个JNI函数对静态链接函数非常有用。
11.4 调用转换(calling convention)
调用转换决定了一个本地函数如何接收参数和返回结果。目前没有一个标准,主要取决于编译器和本地语言的不同。JNI要求同一个系统环境下,调用转换机制必须相同。例如,JNI在UNIX下使用C调用转换,而在Win32下使用stdcall调用转换。
如果程序员需要调用的函数遵循不同的调用转换机制,那么最好写一个转换层来解决这个问题。
11.5 JNIEnv接口指针
JNIEnv是一个指向线程局部数据的接口指针,这个指针里面包含了一个指向函数表的指针。在这个表中,每一个函数位于一个预定义的位置上面。JNIEnv很像一个C++虚函数表或者Microsoft COM接口。图11.3演示了这种关系。
图11.3 线程的局部JNIEnv接口指针
如果一个函数实现了一个本地方法,那么这个函数的第一个参数就是一个JNIEnv接口指针。从同一个线程中调用的本地方法,传入的JNIEnv指针是相同的。本地方法可能被不同的线程调用,这时,传入的JNIEnv指针是不同的。但JNIEnv间接指向的函数表在多个线程间是共享的。
JNI指针指向一个线程内的局部数据结构是因为一些平台上面没有对线程局部存储访问的有效支持。
因为JNIEnv指针是线程局部的,本地代码决不能跨线程使用JNIEnv。
11.5.2 接口指针的好处
比起写死一个函数入口来说,使用接口指针可以有以下几个优点:
1、 JNI函数表是作为参数传递给每一个本地方法的,这样的话,本地库就不必与特定的JVM关联起来。这使得JNI可以在不同的JVM间通用。
2、 JVM可以提供几个不同的函数表,用于不同的场合。比如,JVM可以提供两个版本的JNI函数表,一个做较多的错误检查,用于调试时;另外一个做较少的错误检查,更高效,用于发布时。
11.6 传递数据
像int、char等这样的基本数据类型,在本地代码和JVM之间进行复制传递,而对象是引用传递的。每一个引用都包含一个指向JVM中相应的对象的指针,但本地代码不能直接使用这个指针,必须通过引用来间接使用。
比起传递直接指针来说,传递引用可以让VM更灵活地管理对象。比如,你在本地代码中抓着一个引用的时候,VM那小子可能这个时候正偷偷摸摸地把这个引用间接指向的那个对象从一块儿内存区域给挪到另一块儿。不过,有一点儿你放心,VM是不敢动对象里面的内容的,因为引用的有效性它要负责。瞅一下图11.4,你就会得道了。
图11.4 本地代码抓着引用时,VM的偷鸡摸狗
11.6.1 全局引用和局部引用这对好哥儿们
本地代码中,可以通过JNI创建两种引用,全局引用和局部引用。局部引用的有效期是本地方法的调用期间,调用完成后,局部引用会被JVM自动铲除。而全局引用呢,只要你不手动把它干掉,它会一直站在那里。
JVM中的对象作为参数传递给本地方法时,用的是局部引用。大部分的JNI函数返回局部引用。JNI允许程序员从局部引用创建一个全局引用。接受对象作为参数的JNI函数既支持全局引用也支持局部引用。本地方法执行完毕后,向JVM返回结果时,它可能向JVM返回局部引用,也可能返回全局引用。
局部引用只在创建它的线程内部有效。本地代码不能跨线程传递和使用局部引用。
JNI中的NULL引用指向JVM中的null对象。对一个全局引用或者局部引用来说,只要它的值不是NULL,它就不会指向一个null对象。
11.6.2 局部引用的内部实现
一个对象从JVM传递给本地方法时,就把控制权移交了过去,JVM会为每一个对象的传递创建一条记录,一条记录就是一个本地代码中的引用和JVM中的对象的一个映射。记录中的对象不会被GC回收。所有传递到本地代码中的对象和从JNI函数返回的对象都被自动地添加到映射表中。当本地方法返回时,VM会删除这些映射,允许GC回收记录中的数据。图11.5演示了局部引用记录是怎么样被创建和删除的。一个JVM窗口对应一个本地方法,窗口里面包含了一个指向局部引用映射表的指针。方法D.f调用本地方法C.g。C.g通过C函数Java_C_g来实现。在进入到Java_C_g之前,虚拟机会创建一个局部引用映射表,当Java_C_g返回时,VM会删掉这个局部引用映射表。
图11.5 创建和删除局部引用映射表
有许多方式可以实现一个映射表,比如栈、表、链表、哈希表。实现时可能会使用引用计数来避免重得。
11.6.3 弱引用
弱引用所指向的对象允许JVM回收,当对象被回收以后,弱引用也会被清除。
11.7 对象访问
JNI提供丰富的函数让本地代码通过引用来操作对象,而不用操心JVM内部如何实现。使用JNI函数来通过引用间接操作对象比使用指针直接操作C中的对象要慢。但是,我们认为这很值得。
11.7.1 访问基本类型数组
访问数组时,如果用JNI函数重复调用访问其中的每一个元素,那么消耗是相当大的。
一个解决方案是引入一种“pin”机制,这样JVM就不会再移动数组内容。本地方法接受一个指向这些元素的直接指针。但这有两个影响:
1、 JVM的GC必须支持“pin”。“pin”机制在JVM中并不是一定要实现的,因为它会使GC的算法更复杂,并有可能导致内存碎片。
2、 JVM必须在内存中连续地存放数组。虽然这是大部分基本类型数组的默认实现方式,但是boolean数组是比较特殊的一个。Boolean数组有两种方式,packed和unpacked。用packed实现方式时,每个元素用一个bit来存放一个元素,而unpacked使用一个字节来存放一个元素。因此,依赖于boolean数组特定存放方式的本地代码将是不可移植的。
JNI采用了一个折衷方案来解决上面这两个问题。
首先,JNI提供了一系列函数(例如,GetIntArrayRegion、SetIntArrayRegion)把基本类型数组复制到本地的内存缓存。如果本地代码需要访问数组当中的少量元素,或者必须要复制一份的话,请使用这些函数。
其次,程序可以使用另外一组函数(例如,GetIntArrayElement)来获取数组被pin后的直接指针。如果VM不支持pin,这组函数会返回数组的复本。这组函数是否会复制数组,取决于下面两点:
1、 如果GC支持pin,并且数组的布局和本地相同类型的数组布局一样,就不会发生复制。
2、 否则的话,数组被复制到一个不可变的内存块儿中(例如,C的heap上面)并做一些格式转换。并把复制品的指针返回。
当数组使用完后,本地代码会调用另外一组函数(例如,ReleaseInt-ArrayElement)来通知JVM。这时,JVM会unpin数组或者把对复制后的数组的改变反映到原数组上然后释放复制后的数组。
这种方式提供了很大的灵活性。GC算法可以自由决定是复制数组,或者pin数组,还是复制小数组,pin大数组。
JNI函数必须确保不同线程的本地方法可以同步访问相同的数组。例如,JNI可能会为每一个被pin的数组保持一个计数器,如果数组被两个线程pin的话,其中一个unpin不会影响另一个线程。
11.7.2 字段和方法
JNI允许本地代码通过名字和类型描述符来访问JAVA中的字段或调用JAVA中的方法。例如,为了读取类cls中的一个int实例字段,本地方法首先要获取字段ID:
jfieldID fid = env->GetFieldID(env, cls, "i", "I");
然后可以多次使用这个ID,不需要再次查找:
jint value = env->GetIntField(env, obj, fid);
除非JVM把定义这个字段和方法的类或者接口unload,字段ID和方法ID会一直有效。
字段和方法可以来自定个类或接口,也可以来自它们的父类或间接父类。JVM规范规定:如果两个类或者接口定义了相同的字段和方法,那么它们返回的字段ID和方法ID也一定会相同。例如,如果类B定义了字段fld,类C从B继承了字段fld,那么程序从这两个类上获取到的名字为“fld”的字段的字段ID是相同的。
JNI不会规定字段ID和方法ID在JVM内部如何实现。
通过JNI,程序只能访问那些已经知道名字和类型的字段和方法。而使用Java Core Reflection机制提供的API,程序员不用知道具体的信息就可以访问字段或者调用方法。有时在本地代码中调用反射机制也很有用。所以,JDK提供了一组API来在JNI字段ID和java.lang.reflect.Field类的实例之间转换,另外一组在JNI方法ID和java.lang.reflect.Method类实例之间转换。
11.8 错误和异常
JNI编程时的错误通常是JNI函数的误用导致的。比如,向GetFieldID方法传递一个对象引用而不是类引用等。
11.8.1 不检查编程错误
JNI函数不对编程错误进行检查。向JNI函数传递非法参数会导致未知的行为。原因如下:
1、 强制JNI函数检查所有可能的错误会减慢所有本地方法的执行效率。
2、 大部分情况下,运行时没有足够的类型信息来做错误检查。
大部分的C库函数也同样对编程错误不做预防。例如printf这个函数,当接收到非法的参数时,它会引发一起运行时错误,而不会抛出错误码。强制C库函数检查所有可能的错误会导致错误被重复检查,一次是在用户代码中,一次是在库函数中。
虽然JNI规范没有要求VM检查编程错误,但鼓励VM对普通错误提供检查功能。例如,VM在使用JNI函数表的调用版本时可能会做更多的错误检查。
11.8.2 JVM异常
一旦JNI发生错误,必须依赖于JVM来处理异常。通过调用Throw或者ThrowNew来向JVM抛出一个异常。一个未被处理的异常会记录在当前线程中。和JAVA中的异常不同,本地代码中的异常不会立即中断当前的程序执行。
本地代码中没有标准的异常处理机制,因此,JNI程序最好在每一步可能会产生异常的操作后面都检查和处理异常。JNI程序员处理异常通常有两种方式:
1、 本地方法可以选择立即返回。让代码中抛出的异常向调用者抛出。
2、 本地代码可以通过调用ExceptionClear清理异常并运行自己的异常处理代码。
异常发生后,一定要先进行处理或者清除后再进行后续的JNI函数调用。大部分情况下,调用一个未被处理的异常都可能会一个未定义的结果。下面列表中的JNI函数可以在发生异常后安全地调用:
· ExceptionOccurred
· ExceptionDescribe
· ExceptionClear
· ExceptionCheck
·
· ReleaseStringChars
· ReleaseStringUTFchars
· ReleaseStringCritical
· Release<Type>ArrayElements
· ReleasePrimitiveArrayCritical
· DeleteLocalRef
· DeleteGlobalRef
· DeleteWeakGlobalRef
· MonitorExit
最前面的四个函数都是用来做异常处理的。剩下的都是用来释放资源的,通常,异常发生后都需要释放资源。
*** 10. java 线程总结
**** DONE 简单说说 Java 中 sleep() 与 wait() 方法的区别
sleep() 方法使当前线程进入停滞状态(阻塞当前线程),让出 CUP 的使用,
目的是不让当前线程独自霸占该进程所获的 CPU 资源。该方法是 Thread 类的
静态方法,当在一个 synchronized 块中调用 sleep() 方法时,线程虽然休眠
了,但是其占用的锁并没有被释放;当 sleep() 休眠时间期满后,该线程不一
定会立即执行,因为其它线程可能正在运行而且没有被调度为放弃执行,除非此
线程具有更高的优先级。
wait() 方法是 Object 类的,当一个线程执行到 wait() 方法时就进入到一个
和该对象相关的等待池中,同时释放对象的锁(对于 wait(long timeout) 方法
来说是暂时释放锁,因为超时时间到后还需要返还对象锁),其他线程可以访问。
wait() 使用 notify() 或 notifyAll() 或者指定睡眠时间来唤醒当前等待池中
的线程。wait() 必须放在 synchronized 块中使用,否则会在运行时抛出
IllegalMonitorStateException 异常。
由此可以看出它们之间的区别如下:
sleep() 不释放同步锁,wait() 释放同步锁。
sleep(milliseconds) 可以用时间指定来使他自动醒过来,如果时间没到则只能
调用 interreput() 方法来强行打断(不建议,会抛出 InterruptedException),
而 wait() 可以用 notify() 直接唤起。
sleep() 是 Thread 的静态方法,而 wait() 是 Object 的方法。
wait()、notify()、notifyAll() 方法只能在同步控制方法或者同步控制块里面
使用,而 sleep() 方法可以在任何地方使用。
*** 11. 非静态内部类的总结
**** DONE 为什么非静态内部类中不能有 static 成员变量却可以有 static final 属性的编译期常量?
这是一个陷阱题,看起来似乎很简单,实际是一箭双雕,即考察了非静态内部类
相关知识,还考察了 final 的各种常量分类细则(很多人回答时会疏忽这个点)。
由于 Java 中非静态内部类默认持有了外部类引用,也就是说可以将它看成是其
外部类的一个成员,所以其必须跟外部类实例相关联才能初始化。而静态成员是
属于类层次的,是不需要类实例就可以初始化访问的。此时如果假设让一个非静
态内部类拥有了静态变量,则其应该可以不依托于任何外部类实例就能访问,而
非静态内部类却没法做到不实例化其外部类而使用,所以这种设计从语法层面就
是互斥的。
而对于 static final 成员来说就不一样了,至于为什么不一样我们需要先切换
补充一些知识点再说。我们先看下面程序段:
public class Outer {
class Inner {
static int t1 = 100; //编译错误
static final int t2 = 100; //编译成功
static final int t3 = new Integer(100); //编译错误
}
public static void main(String[] args) {
System.out.println(Outer.Inner.t1);
System.out.println(Outer.Inner.t2);
System.out.println(Outer.Inner.t3);
}
}
上面程序片段分别编译运行的结果在注释部分已经给出了,在我们挨个解释上面
语句现象前先说说 Java 对常量的一些定义和处理机制。
对于 Java 中的常量其实可以分为编译期常量和非编译期常量。编译期常量指的
是在程序编译阶段(不需要加载类的字节码)就可以确定具体值的常量,其中会
涉及到编译期常量折叠(编译器可以语法分析计算出值的常量表达式进行计算取
值)。非编译期常量(运行期常量)指的是在程序运行阶段(需要加载类的字节
码)才可以确定具体值的常量(编译期无法折叠,编译器能做的只是对所有可能
修改它的地方进行检查和报错)。
当我们通过类名访问被 static final 修饰的常量时,如果该常量是编译期常量
则该类不会被 JVM 加载,如果该常量是非编译期常量则该类会被 JVM 加载。当
通过类名访问被 static 修饰的变量时都会触该类被 JVM 加载。
有了上面这个概念我们再来分析上面代码段的原因。由于 Java 类属性的初始化
顺序为(静态变量、静态初始化块)>(变量、初始化块)> 构造器,所以 JVM
要求所有的静态属性必须在类对象创建之前完成初始化。故对于 t1 属性调用处
来说,必须要等到外部类 Outer 实例化之后(即有创建一个外部类对象)JVM
才能加载其内部类 Inner 的字节码,而我们调用处并没有对外部类进行实例化,
所以内部类 Inner 也不会被加载,而 t1 是 static 属性,要初始化 t1 就必
须先加载内部类的字节码;而 t3 是非编译期常量,要初始化它们也必须加载内
部类的字节码才能确定它们的值;只有 t2 是编译时常量,所以不会触发内部类
加载机制;故而有了上面代码段的结果。
所以说非静态内部类中不能有 static 成员变量却可以有 static final 属性的
编译期常量而不能有 static final 属性的运行期常量。
由此又引出了一个新的面试题和代码优化小技巧。
为什么我们在类中定义 static 修饰的 String 常量时经常会在它前面加上
final 修饰符?因为这样可以使 JVM 不必加载该类的类体就能直接使用其值从
而节省了内存空间。
*** 12. MVP 自己的理解
**** 1. V 层 P 层
1. 每一界面(Activity 或者 Fragment)都需要实现定义的 View 接口,也就
是需要的 UI 操作。创建 Contract 接口,里面定义 View 接口和
Presenter 接口。
2. View 接口需要继承 BaseView 接口,Presenter 接口需要继承
BasePresenter 接口
3. BaseView 接口定义 setPresenter 方法
void setPresenter(T presenter);
4. BasePresenter 接口定义 start 方法
void start();
5. 创建 Presenter 接口的实现类,在 Activity 里面定义一个类型为
Contract.Presenter 的属性,持有 Presenter 接口的引用。
**** 2. M 层
1. 创建一个 data 目录
2. 在 data 目录下面创建一个 source 目录。
3. 在 data 目录下面创建一个单例的 Respository
4. 在 data 目录下面创建 DataSource 接口定义
5. 在 DataSource 接口里面定义对本地和远程数据的操作方法。
6. 在 source 目录下面分别新建 local 和 remote 目录,表示本地和远程的数
据操作。
7. 在 local 和 remote 目录下面分别新建 DataSource 接口实现类。
8. 创建 IO 线程池,网络线程池,主线程
*** DONE 13. MVVM 的实现原理
**** 1. Databinding
1. Databing 是一种框架,MVVM 是一种架构,一种模式。DataBinding 是一个实现
数据和 UI 绑定的框架, 是实现 MVVM 模式的工具,而 MVVM 中的
VM(ViewModel) 和 View 可以通过 Databing 来实现
**** 2. MVC
1. 视图层(View) 对应于 XML 布局文件
2. 控制层(Controller) Android 的控制层是由 Activity 来承担的,
Activity 本来主要是初始化页面,展示数据的操作,但是因为 XML 视图功
能太弱,所以 Activity 既要负责视图的显示又要加入控制逻辑。
3. 模型层(Model) 我们针对业务模型,建立的数据结构和相关的类,它主要
负责网络请求,数据库处理,I/O 的操作。
**** 3. MVP
1. 视图层(View) 负责绘制 UI 元素,与用户交互,对应于 XML,Activity,
Fragment,Adapter
2. 模型层(Model)
负责存储,检索,操纵数据,一般包含网络请求,数据库操作,I/O 流。
3. 控制层(Presenter)
Presenter 是整个 MVP 体系的控制中心,作为 View 与 Model 交互的中心
纽带,处理 View 与 Model 间的交互和业务逻辑。
**** 4. MVVM
1. Mode : 负责数据实现和逻辑处理,类似 MVP。
2. View : 对应 Activity 和 XML, 负责 View 的绘制以及与用户交互,类似
MVP。
3. ViewModel : 创建关联,将 Model 和 View 绑定起来,如此之后,我们
model 的更改,通过 ViewModel 反馈给 view,从而自动刷新界面。
4. 总结: View 层的 Activity 通过 DataBinding 生成 Binding 生成
Binding 实例,把整个实例传递给 ViewModel, ViewModel 层通过把自身与
Binding 实例绑定,从而实现 View 中 layout 与 ViewModel 的双向绑定。
MVVM 的缺点是数据绑定使得 Bug 很难被调试。
*** TODO 14. Dagger2 的理解
**** 注解
Dagger2 通过注解来生成代码,定义不同的角色,主要的注解如下:
1. @Module: Module 类里面的方法专门提供依赖,所以我们定义一个类,用
@Module 注解,这样 Dagger 在构造类的实例的时候,就知道从哪里去找到
需要的依赖。
2. @Provides: 在 Module 中,我们定义的方法是用这个注解,以此来告诉
Dagger 我们想构造对象并提供这些依赖。
3. @Inject: 通常在需要依赖的地方使用这个注解。换句话说,你用它告诉
Dagger 这个类或者字段需要依赖注入。这样,Dagger 就会构造一个这个类
的实例并满足他们的依赖。
4. @Component:Component 从根本上来说就是一个注入器,也可以说是
@Inject 和 @Module 的桥梁,它的主要作用就是连接这两个部分。将
Module 中产生的依赖对象自动注入到需要依赖实例的 Container 中。
5. @Scope:Dagger2 可以通过自定义注解限定注解作用域,来管理每个对象实
例的生命周期。
6. @Qualifier: 当类的类型不足以鉴别一个依赖的时候,我们就可以使用这个
注解标示。例如:在 Android 中,我们会需要不同类型的 context,所以我
们就可以定义 qualifier 注解 @perApp 和 @perActivity, 这样当注入一个
context 的时候,我们就可以告诉 Dagger 我们想要哪种类型的 context。
**** 结构
Module --> Component --> (inject) --> Container
**** 配置
Java Gradle
// Add plugin https://plugins.gradle.org/plugin/net.ltgt.apt
plugins {
id "net.ltgt.apt" version "0.5"
}
// Add Dagger dependencies
dependencies {
compile 'com.google.dagger:dagger:2.x'
apt 'com.google.dagger:dagger-compiler:2.x'
Android Gradle
// Add Dagger dependencies
dependencies {
compile 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
*** 15. JAVA 集合总结
**** List 和 Set 的区别
1. Set 接口实例存储的是无序的,不重复的数据。 List 接口实例存储的是有
序的,可以重复的元素。
2. Set 检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变,
实现类有 HashSet, TreeSet.
3. List 和数组类似,可以动态增长,根据实际存储的数据的长度自动增长
List 的长度。查找元素的效率高,插入删除效率低,因为会引起其它元素位
置的改变,实现类有ArrayList,LinkedList,Vector,
CoplyOnWriteArrayList.
**** ArrayList 与 Vector 的区别
1. 两者都是基于索引,内部结构是数组
2. 元素存取有序并都允许为 null
3. 都支持 fail-fast机制(fast-fail 机制是 java 集合中的一种错误机制。
当多个线程对同一个集合的内容进行操作时,就可能忽产生 fail-fast 事
件。)
4. Vector 是同步的,不会过载,而 ArrayList 不是,但 ArrayList 效率比
Vector 高,如果在迭代中对集合做修改可以使用 CopyOnWriteArrayList
5. 初始容量都为 10,但 ArrayList 默认增长为原来的50%,而 Vector 默认增
长为原来的一倍,并且可以设置。
6. ArrayList 更通用,可以使用 Collections 工具类获取同步列表和只读列表
7. 使用场景分析
1. Vector 是线程同步的,所以它也是线程安全的,而 ArrayList 是线程异
步的,是不安全的。如果不考虑线程的安全因素,一般用 ArrayList 效
率比较高。
2. 如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据
量比较大的数据,用 Vector 有一定的优势。
**** ArrayList 和 LinkedList 的区别
1. 两者都是 List 接口的实现类
2. ArrayList 是基于动态数组的数据结构,而 LinkedList 是基于链表的数据
结构
3. 对于随机访问 get 和 set(查询操作),ArrayList 要优于 LinkedList,
因为 LinkedList 要移动指针
4. 使用场景分析
1. 当需要对数据进行对比访问的情况下选用 ArrayList,当需要对数据进行
多次增加删除修改时采用 LinkedList
**** CopoyOnWriteArrayList
1. CopoyOnWriteArrayList 是 ArrayList 的线程安全的变体,其中的所有可变
操作(add, set等)都是对底层数组进行一次新的复制来实现的,相比
ArrayList 的要慢一些,适用于读多写少的场景。
2. 在并发操作容器对象时不会抛出 ConcurrentModificationException,并且返
回的元素与迭代器创建时的元素是一致的。
3. 容器对象的复制需要一定的开销,如果对象占用内存过大,可能造成频繁的
YoungGC 和 Full GC
4. CopyOnWriteArrayList 不能保证数据实时一致性,只能保证最终一致性。
**** HashMap 和 HashTable 的区别
1. 都是基于 hash 表实现的,每个元素都是 key-value 对,内部都是通过单休
链表解决冲突,容量都会自动增长 HashMap 默认容量为16,每次扩容变为原
来的2倍,HashTable 初始容量为11,每次扩容变为原来的2倍加1
2. HashMap 继承自 AbstractMap 类,HashTable 继承自 Dictionary 类。
3. HashTable 是同步的,适合多线程环境,而 HashMap 不是,单效率相对较高
4. HashMap 允许 key 和 value 为 null,而 HashTable 不允许
5. Hash 值的使用不同,HashTable 直接使用对象的 Hashcode 值,而 HashMap
重新计算 hash 值
6. 在 Java1.4 中引入了 HashMap 的子类 LinkedHashMap, 若需要遍历顺序,
可以从 HashMap 转向 LinkedHashMap,而 HashTable 的顺序是不可预知的。
7. HashMap 提供对 Key 的 Set 进行遍历,因此它支持 fail-fast 机制,而
HashTable 提供对 key 的 Enumeration 进行遍历,不支持 fail-fast
8. HashTable 被认为是个遗留的类,如果再迭代的时候修改 Map,可以使用
ConcurrentHashMap (Java5 出现)
9. HashTable 产生于 JDK1.1 ,而 HashMap 产生于 JDK1.2
**** HashSet 和 TreeSet
1. HashSet 不能保证元素的排列顺序,TreeSet 是 SorteSet 接口的唯一实现
类,可以确保集合元素处于排序状态
2. HashSet 底层用的是哈希表, TreeSet 采用的数据结构是红黑树
3. HashSet 中元素可以为 null,但只能有一个,TreeSet 不允许放入 null
4. 使用场景分析
1. HashSet 是基于 Hash 算法实现的,其性能通常都优于 TreeSet。我们通
常都应该使用 HashSet, 在我们使用排序的功能时,我们才使用 TreeSet
**** HashMap 和 ConCurrentHashMap 的区别
1. HashMap 不是线程安全的,而 ConCurrentHashMap 是线程安全的。
2. ConCurrentHashMap 采用锁分段技术,将整个 Hash 桶进行了分段 Segment,
也就是将整个大的数组分成了几个小的片段 Segment, 而且么个小的片段
Segment 上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到
哪一个片段 Segment,然后再在这个片段上面进行插入,而且这里还需要获取
Segment 锁。
3. ConcurrentHashMap 让锁的粒度更精细一些,并发性能更好。
**** Enumeration 和 Iterator 的区别
1. Enumeration 是 JDK1.0 出现的,只能读取集合的数据
2. Iterator 的方法名是标准化的。
3. Iterator 支持 fail-fast 机制,而 Enumeration 不支持
4. Enumeration 的内存暂用较少,效率比 Iterator 高,但 Iterator 更安全
**** Iterator 和 ListIterator 的区别
1. ListIterator 继承自 Iterator 接口,然后添加了一些额外的功能
2. 两者都有 hasNext()和 next() 方法,可以实现顺序向后遍历,
ListIterator 还有 hasPrevious() 和 previous() 方法,可以实现逆序遍
历
3. 都有 remove() 方法可以实现删除对象,ListIterator 还有添加方法 add()
和修改方法 set(),可以实现添加和修改对象,Iterator 的协议不能确保迭
代的次序,所以没有提供 add() 方法。
4. Iterator 可遍历 Set 和 List 集合, 而 ListIterator 只能遍历 List 集
合
5. istIterator 有nextIndex() 和 previousIndex() 方法,可以定位当前的索
引位置,Iterator 没有此功能。
6.