Java中的Reference解析

前言

Java执行GC判断对象是否存活有两种方式其中一种是引用计数

引用计数:Java堆中每一个对象都有一个引用计数属性,引用每新增1次计数加1,引用每释放1次计数减1。

在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于(reachable)可达状态,程序才能使用它。

从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用,本文主要讲解各个引用的用途以及引用队列的作用。

1、强引用(StrongReference)

强引用指的是程序代码中普遍存在的,类似如下代码

1
Object object = new Object();

当内存空间不足时,只要强引用还在,Java虚拟机会抛出OutOfMemoryError错误,使程序异常终止,也不会靠回收强引用的对象来解决内存不足的问题。

如果强引用对象不使用时,需要弱化从而使GC能够回收,如下所示:

1
object = null;

2、软引用(SoftReference)

软引用是用来描述一些有用但并不是必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回首范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等,软引用可用来实现内存敏感的高速缓存。

1
2
3
4
5
// 强引用
String strongReference = new String("abc");
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);

软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

1
2
3
4
5
6
7
8
9
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
// Notify GC
System.gc();
System.out.println(softReference.get()); // abc
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference); //null

注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。

当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:

1
2
3
4
5
6
if(JVM内存不足) {
// 将软引用中的对象引用置为null
str = null;
// 通知垃圾回收器进行回收
System.gc();
}

也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的较新的软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因,后面会具体讲解。

3、弱引用(WeakReference)

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的
对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,
都会回收掉只被弱引用关联的对象。

1
2
3
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;

复制代码JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:

1
2
str = null;
System.gc();

注意:如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference来记住此对象。

下面的代码会让一个弱引用再次变为一个强引用:

1
2
3
4
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
// 弱引用转强引用
String strongReference = weakReference.get();

同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4、虚引用(PhantomReference)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。

虚引用主要用来跟踪对象被垃圾回收器回收的活动,虚引用与软引用和弱引用的一个区别在于:

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

1
2
3
4
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

5、引用队列(ReferenceQueue)

在前面软引用、弱引用和虚引用都有讲到引用队列,那引用队列具体是做什么的呢?
官方对于引用队列类的注释是:

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.
译为:引用队列是将垃圾收集器在监测到适当的可达性更改后将已注册的引用对象添加到该队列。

对于软引用和弱引用和虚引用,我们希望当一个对象被gc掉的时候通知用户线程,进行额外的处理时,就需要使用引用队列了。ReferenceQueue即这样的一个对象,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理等。

查看源码发现ReferenceQueue的代码结构并不复杂,从源码上看,实际上ReferenceQueue只是名义上的引用队列,它只保存了Reference链表的头(head)节点,并且提供了队列出队入队删除操作,而Reference实际上本身提供单向链表的功能,也就是说Reference通过成员属性next构建单向链表,而链表的操作是委托给ReferenceQueue完成。详细参考:深入理解JDK中的Reference原理和源码实现
在这里插入图片描述

6、总结

1、Java中四种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用

2、ReferenceQueue引用队列用来记录被回收的引用为用户线程做额外操作作铺垫

3、对各种引用回收时间、用途、生成时间作总结:

参考:

1、《深入理解Java虚拟机》

2、深入理解JDK中的Reference原理和源码实现

3、理解Java的强引用、软引用、弱引用和虚引用

(转)算法之优先队列 PriorityQueue解决Top K 问题

转自:https://www.jianshu.com/p/a4a1984fc4ff

解决方法:
维护一个大小为 K 的小顶堆,依次将数据放入堆中,当堆的大小满了的时候,只需要将堆顶元素与下一个数比较:如果大于堆顶元素,则将当前的堆顶元素抛弃,并将该元素插入堆中。遍历完全部数据,Top K 的元素也自然都在堆里面了。

当然,如果是求前 K 个最小的数,只需要改为大顶堆即可

将数据插入堆 95 大于 20,进行替换 95 下沉,维持小顶堆
对于海量数据,我们不需要一次性将全部数据取出来,可以一次只取一部分,因为我们只需要将数据一个个拿来与堆顶比较。

另外还有一个优势就是对于动态数组,我们可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,我们就直接拿它与堆顶的元素对比。这样,无论任何时候需要查询当前的前 K 大数据,我们都可以里立刻返回给他。

整个操作中,遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK),加起来就是 O(nlogK) 的复杂度,换个角度来看,如果 K 远小于 n 的话, O(nlogK) 其实就接近于 O(n) 了,甚至会更快,因此也是十分高效的。

最后,对于 Java,我们可以直接使用优先队列 PriorityQueue 来实现一个小顶堆,这里给个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static List<Integer> solutionByHeap(int[] input, int k) {
List<Integer> list = new ArrayList<>();
if (k > input.length || k == 0) {
return list;
}
Queue<Integer> queue = new PriorityQueue<>();
for (int num : input) {
if (queue.size() < k) {
queue.add(num);
} else if (queue.peek() < num) {
queue.poll();
queue.add(num);
}
}
while (k-- > 0) {
list.add(queue.poll());
}
return list;
}

Java中Lambda表达式解析

在大部分开发者看来,Lambda表达式只是一种语法糖,简化了书写匿名内部类的写法。实际上Lambda表达式并不仅仅是匿名内部类的语法糖,JVM内部是通过invokedynamic指令来实现Lambda表达式的,与内部类的实现有很大的差异。

本文主要讲解以下知识点:

一、函数式接口

二、Lambda表达式与匿名内部类

三、Lambda实现原理

一、函数式接口

众所周知Javascript具有一个强大的特性:闭包。Java中最接近闭包概念的东西就是lambda表达式了,而Lambda为Java添加了缺失函数式编程的特点。所以什么是函数是接口呢?

函数式接口需满足以下两个条件:

  1. 它是接口
  2. 这个接口有且仅有一个抽象方法

例如我们常用的:Runnable、View.OnClickListener、Comparable等都是函数式接口,因为它们都只有一个方法,而且都是抽象的。虽然只有一个抽象方法,是不是就意味着只能有一个方法呢?实际并不是,虽然有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

嗯?!Java接口中难道还可以定义非抽象方法么?平时我们的接口大概长这样:

public interface IdiomSubmitListener {
    void verifyResult(String result);
    void onSuceess();
}

那接口的非抽象方法是啥?原来在JDK 1.8 对于接口而言具有以下新特性:
接口可以定义非抽象方法,但必须使用default或者staic关键字来修饰
具体细节点可以参考 JAVA 8新特性 允许接口定义非抽象方法 快速入门案例

如果一个接口符合函数式接口的定义,那么我们就可以在该接口上面声明FunctionalInterface注解,用来表示该接口是一个函数式接口,并按照函数式接口的规范在编译的时候对该接口进行检查。

当然如果某个接口只有一个抽象方法,但我们并没有给该接口声明FunctionalInterface注解,那么编译器依旧会将该接口看做是函数式接口。

那Lambda表达式跟函数式接口又有什么关联呢?
在JDK 1.8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型–函数式接口。

因此可以说 在JDK 1.8中,Lambda表达式就是一个函数式接口的实例。
所以如果一个实例是函数式接口的实例,那么该对象就可以用Lambda表达式来表示

二、Lambda表达式与匿名内部类

我们知道代码IDE如果是在JDK1.8的环境下,使用匿名内部类作为一个参数传入到方法中,编译器会提示我们:Anonymous new Runnable() can be replaced with lambda,匿名内部类XXX可以替换为lambda表达式。

如下所示,匿名内部类 Runnable是一个函数式接口的实例,所以我们可以用lambda表达式来将之替换,从而将代码变得更加简洁。

在这里插入图片描述
那么我们是否就认为:Lambda表达式只是为匿名内部类中提供的一种语法糖,他们有什么区别呢?底层原理是完全一样的呢?

他们主要区别如下:

1、关键字this。匿名内部类的this指向匿名类,而Lambda表达式的this指向被Lambda包围的外部类

2、编译方式。Java编译器将Lambda表达式编译成类的私有方法,使用Java7的invokedynamic字节码动态绑定这个方法。而匿名内部类将编译成外部类$数字编号的新类。这也造成第1点关键字this指向不同地方的原因。

三、Lambda实现原理

我们知道如果使用匿名内部类,编译期间会生成一个外部类$数字编号的类,如图所示:

在这里插入图片描述

而如果使用Lambda表达式进行编译后并没有生成新类。
在这里插入图片描述

我们对Lambda表达式生成的class文件使用:javap -p -v Test.class 进行反编译生成如下内容,为便于观察,删除了一些无用内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class wang.julis.jwbase.basecompact.Test

Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
{
public wang.julis.jwbase.basecompact.Test();
descriptor: ()V
flags: (0x0001) 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 12: 0

private void testLambda();
descriptor: ()V
flags: (0x0002) ACC_PRIVATE
Code:
stack=3, locals=1, args_size=1
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: pop
13: return
LineNumberTable:
line 14: 0
line 18: 13

private static void lambda$testLambda$0();
descriptor: ()V
flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String lambda
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 15: 0
line 16: 8
}
SourceFile: "Test.java"
InnerClasses:
public static final #50= #49 of #53; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #21 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#22 ()V
#23 REF_invokeStatic wang/julis/jwbase/basecompact/Test.lambda$testLambda$0:()V
#22 ()V

从反编译的结果我们可以看到:

1、编译期间自动生成私有静态类lambda$testLambda$0而这里面就就是lambda的具体实现逻辑

2、使用invokedynamic去执行lambda表达式 关于invokedynamic命令具体细节可以参考: 08 | JVM是怎么实现invokedynamic的?(上)

3、lambda表达式编译后并没有生成外部类$数字编号的类

总结:

1、函数式接口:有且仅有一个抽象方法,可以用非抽象方法1.8后支持

2、匿名内部类的this指向匿名类,而Lambda表达式的this指向被Lambda包围的外部类

3、lambda表达式编译后不会生成外部类$数字编号的类

4、Java编译器将Lambda表达式编译成类的私有方法,使用Java7的invokedynamic字节码动态绑定这个方法。

参考:
1、《深入探索Android热修复技术原理》2.3.8章节
2、Java8 lambda表达式、函数式接口、方法引用

Android骨架屏效果的实现与原理解析

0、前言

大家在使用淘宝的时候,如下图所示有遇到这样的效果,其会只展示一部分骨架大致图,等数据加载完毕之后再展示真正的页面数据。与菊花图相比起来,这样的实现能更好的提升用户的体验,这种效果称做:Skeleton Screen Loading,中文叫做骨架屏

1、骨架屏的实现方式

在现在主流的骨架屏实现效果中有两种方式:

这些开源库中,自己比较喜欢今天Skeleton这个开源库,总结了有如下一些优缺点:

优点:

  1. 代码方案实现及使用方式简单,通过替换View和Adapter实现效果,使用Builder设计模式来构造。
  2. 代码耦合程度不高。没有复杂的设计模式,使得代码结构清晰明了。
  3. 骨架屏的效果使用相对于较灵活,可以对整个布局实现骨架屏效果,也可以对单一View实现骨架屏效果。

缺点:

  1. 需要对每个骨架屏效果单独写一套xml布局。
  2. 使用的removeView和addView对 原有布局的view进行替换,存在一定的风险性
  3. 必须清晰的知道所bind的View类型,存在一定的类型转化问题。
  4. 依赖了shimmerlayout第三方库

2、Skeleton解读

一、Skeleton的使用方式

展示骨架屏效果:

View rootView = findViewById(R.id.rootView);
skeletonScreen = Skeleton.bind(rootView)
           .load(R.layout.activity_view_skeleton)//骨架屏UI
           .duration(1000)//动画时间,以毫秒为单位
           .shimmer(true)//是否开启动画
           .color(R.color.shimmer_color)//shimmer的颜色
           .angle(30)//shimmer的倾斜角度
           .show();

关闭骨架屏效果并展示原有View:

skeletonScreen.hide()

流程:

**1. 选择需要替换的目标view

  1. 将骨架效果xml与目标view进行绑定
  2. 添加一些效果属性,比如:动画时间、是否开启展示动画、动画颜色等
  3. 在合适的实际关闭骨架屏效果**

二、Skeleton源码实现

Skeleton提供两个绑定方法,分别绑定普通View与RecyclerView,分别返回对应的Builder

1
2
3
4
5
6
7
8
public class Skeleton {
public static RecyclerViewSkeletonScreen.Builder bind(RecyclerView recyclerView) {
return new RecyclerViewSkeletonScreen.Builder(recyclerView);
}
public static ViewSkeletonScreen.Builder bind(View view) {
return new ViewSkeletonScreen.Builder(view);
}
}

我们首先来看看如何实现与普通View绑定,构造方法中传入目标View,并对shimmer动画效果设置默认的颜色,在Builder里面我们可以看到各种相关参数的设定。

1
2
3
4
public Builder(View view) {
this.mView = view;
this.mShimmerColor = ContextCompat.getColor(mView.getContext(), R.color.shimmer_color);
}

接下来再到show的步骤,主要实现还是由ViewSkeletonScreen来实现

1
2
3
4
5
public ViewSkeletonScreen show() {
ViewSkeletonScreen skeletonScreen = new ViewSkeletonScreen(this);
skeletonScreen.show();
return skeletonScreen;
}

其中ViewSkeletonScreen与绑定的RecyclerViewSkeletonScreen都实现了SkeletonScreen接口,SkeletonScreen有两个接口方法分别是

void show();
void hide();

对于ViewSkeletonScreen.show()进入源码,这里出现一个比较重要的类ViewReplacer,等下再进行解析,通过show的源码清楚的知道逻辑:
1、生成骨架效果View
2、利用生成的View替换目标View。

其中生成骨架效果View阶段主要还是通过LayoutInflater去加载传入mSkeletonResID

1
2
3
4
5
6
7
@Override
public void show() {
View skeletonLoadingView = generateSkeletonLoadingView();
if (skeletonLoadingView != null) {
mViewReplacer.replace(skeletonLoadingView);
}
}

接下来主要讲解ViewReplacer类,其构造方法传入目标View

1
2
3
4
5
6
public ViewReplacer(View sourceView) {
mSourceView = sourceView;
mSourceViewLayoutParams = mSourceView.getLayoutParams();
mCurrentView = mSourceView;
mSourceViewId = mSourceView.getId();
}

其比较重要的方法有两个:replace()restore() 这两个方法分别为SkeletonScreen 的show()和hide()的最终实现,首先看replace()方法,有两个方法重载,分别传入targetViewResID或者targetView,最终还是会走到replace(View targetView)中。
其主要逻辑为:

**1. 判断所替换的View和骨架屏效果View是否为同一个View

  1. remove掉在父布局中的目标View
  2. 将骨架屏效果View添加到目标View的父布局中**
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
public void replace(int targetViewResID) {
if (mTargetViewResID == targetViewResID) {
return;
}
if (init()) {
mTargetViewResID = targetViewResID;
replace(LayoutInflater.from(mSourceView.getContext()).inflate(mTargetViewResID, mSourceParentView, false));
}
}

public void replace(View targetView) {
if (mCurrentView == targetView) {
return;
}
if (targetView.getParent() != null) {
((ViewGroup) targetView.getParent()).removeView(targetView);
}
if (init()) {
mTargetView = targetView;
mSourceParentView.removeView(mCurrentView);
mTargetView.setId(mSourceViewId);
mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
mCurrentView = mTargetView;
}
}

在执行添加到目标View的父布局中,有执行一个init方法,主要做两件事:

**1. 获取目标View的父View

  1. 找到目标View在父View 中的位置索引,为之后添加骨架屏View到父View中做铺垫**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean init() {
if (mSourceParentView == null) {
mSourceParentView = (ViewGroup) mSourceView.getParent();
if (mSourceParentView == null) {
Log.e(TAG, "the source view have not attach to any view");
return false;
}
int count = mSourceParentView.getChildCount();
for (int index = 0; index < count; index++) {
if (mSourceView == mSourceParentView.getChildAt(index)) {
mSourceViewIndexInParent = index;
break;
}
}
}
return true;
}

至此对普通View的骨架屏效果实现流程已经完全梳理完成,那对于RecyclerView呢?其实两者实现逻辑差不多,主要有两个差异:

  1. RecyclerViewSkeletonScreen的Builder中,相比ViewSkeletonScreen多了一个adapter()方法,传入目标RecyclerViewAdapter
  2. 在show的时候对目标RecyclerView的adapter进行替换,使用骨架屏效果的adapter。hide的时候恢复为原先的Adapter

3、总结

  1. Skeleton的原理主要是通过替换目标View和RecyclerView的Adapter
  2. 在Skeleton的使用过程中最需要关心的两个问题是:show()和hide()的时机
  3. 对于整个页面的骨架屏效果实现,个人推荐在布局中添加一个全屏的空View盖在原先内容上
  4. 注意一些异常情况下的hide(),要不然整个页面就“假死”状态了。

参考:
https://juejin.im/post/5c789a4ce51d457c042d3b31

Android 插件化之ClassLoader

0、前言:

插件化要解决的三个核心问题:类加载、资源加载、组件生命周期管理。

在Android插件化中其原理实际是 Java ClassLoader的原理,此博文主要对Android插件化中类加载中的DexClassLoader做总结,便于之后对Android插件化的理解学习。

Android的Dalvik虚拟机和Java虚拟机的运行原理相同都是将对应的java类加载在内存中运行。而Java虚拟机是加载class文件,也可以将一段二进制流通过defineClass方法生产Class进行加载。Dalvik虚拟机加载的dex文件。dex文件是Android对与Class文件做的优化,以便于提高手机的性能。可以想象dex为class文件的一个压缩文件。dex在Android中的加载和class在jvm中的相同都是基于双亲委派模型,都是调用ClassLoader的loadClass方法加载类。

1、DexClassLoader和PathClassLoader区别

Android 也有自己的 ClassLoader,分为 DexClassLoaderPathClassLoader,这两者有什么区别和关联呢?

阅读源码可以看到两者的构造方法分别为:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以发现DexClassLoaderPathClassLoader 多一个参数String optimizedDirectory,那这个参数具体做什么的呢?继续查看源码我们可以知道optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile 对象,其具体体现在如下代码区域:

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

因此两者区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。所以在插件化中我们使用DexClassLoader来加载class的,接下来讲解DexClassLoader的用法。

2、DexClassLoader用法

其构造方法为:

DexClassLoader(
    String dexPath, 
       String optimizedDirectory, 
    String librarySearchPath,
     ClassLoader parent)

dexPath:被解压的apk路径,不能为空。
optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。
libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
parent:父亲加载器,一般为context.getClassLoader(),使用当前上下文的类加载器。

接下来讲解具体使用流程:

1、新建一个名为plugin的project,其中新建一个Bean类,只有一个方法getName()返回一个字符串“My App”,然后对plugin这个工程打包为apk,将apk放在主工程的asser目录中。
在这里插入图片描述
2、构造Classloader

File extractFile = getFileStreamPath("app-debug.apk");
String dexPath = extractFile.getPath();
File fileRelease = getDir("dex", 0);
ClassLoader classLoader = new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), null, getClassLoader());

3、利用构造好的Classloader反射调用插件类中的方法

Class mLoadClassBean;
try {
      mLoadClassBean = classLoader.loadClass("com.example.plugin.Bean");
      Object beanObject = mLoadClassBean.newInstance();
      Method getNameMethod = mLoadClassBean.getMethod("getName");
      getNameMethod.setAccessible(true);
      String name = (String) getNameMethod.invoke(beanObject);
      Log.e("julis", name);
  } catch(Exception e) {
      e.printStackTrace();
  }

成功打印出结果:
在这里插入图片描述

参考:

https://www.jianshu.com/p/4b4f1fa6633c

https://www.jianshu.com/p/53aa2de20cf8

https://cloud.tencent.com/developer/article/1071815

忙碌中求生活-记录23岁生日

此时周六晚七点半,刚吃了一碗自己做的番茄鸡蛋面,将浸泡了很久的银耳原料装入了电饭煲内,静待熟时。回到自己的房间,窗外不断传来来往的车轱辘声,好像在告诉我他们很忙吧。看到书桌上的日历,还沉浸在六月份,我可能也很忙吧,都忘记将它带入七月份。

来杭州一年有余,从一名大三学生暑假实习,到大四学生实习,再到应届毕业生签订转正合同,从而成为一名正式的“社会人儿”。时间过得真的很快吧,一周周一下子就没有了。进入了七月份,明显感觉比之前更忙了,或者说是因为自己身份的转变,导致肩上的责任变得不太一样了吧。因为工作节奏的改变,有时候也变得有些麻木吧,日记有时候总会忘记写,写字这件事感觉也变得有些奢侈起来,日语学习的节奏好像变得慢了起来。但是时间的脚步并不因为你的忙碌而停下来。

我最近在读《时间简史》,讲述了从认知革命到农业革命再到人类文化的融合统一再到科学革命,作者用通俗的话语讲完了整个人类历史,真的很值得阅读。其中有一章节引发了我的思考,作者认为:农业革命是史上最大的骗局。因为在长达250万里里人类都靠狩猎或者采集果实为生,而到大约一万年前全然改变,从日升到日落,人类忙着对植物的培育,一心认为这样就能得到更多的水果、谷物和肉类,使得人类生活能够变得更加容易。然而事实上是人变得越来越辛苦。人类每次决定多做一点事情(比如用锄头来耕地,而不是直接将种子撒在地里面),我们认为这样没错,这样会使我们的收成更好一点,有了更好的收成,就不用更多地去担心荒年的问题了,不用挨饿了。工作努力一点,生活也能过得好一点。不过这都是理想的状态。

人们确实工作得更努力也更辛苦,但没想到大家的孩子也更多了,人口慢慢地增加了,所得到的食物也就变得少了,生存压力也就更大了,资源也开始变得稀缺,而引发各种低问题。可是为什么他们不赶快放弃农耕回到原始的采集社会?原因在于,所有的改变都是必须点滴积累,经过许多代,才能改变社会,等到那个时候,已经没有人记得过去的生活方式和现在有什么不一样了,也没得选了。采用了农耕生活,村落的人口从100人到了110人,难道会有10个人自愿挨饿,好让其他人回到过去的美好时光?但这已经无法回头。于是人类付出了更多,但得到的却变得没有以前那样多。

我们都各自为生活变得轻松而努力,但是事实上我们过得并不轻松。人的欲望永远也满足不了,今天拥有了这个,明天还想拥有更好的。现在,我们随手可以发送一条信息,传到地球另一方,而他立马能够回你。我们确实省下了很多时间和麻烦,但生活真的更轻松了么?我们以为省了时间,然而我们其实是把生活的步调调成了过去的10倍,于是我们整天忙忙碌碌、焦躁不安。

这是原书作者对农业革命是史上最大的骗局的论证吧,我再赞同不过了。每每与我的爸妈打电话,我都会有很大的感触,他们总是在忙碌,他们每天都在拼命的挣钱,可是日子真的好起来了么?生活真的轻松了么?不久前,在家族微信群里面,我妈拍了一张我爸的照片放在群里,我爸双手背后,露出一脸笑意,背景好像是他们住那里的一个普通的建筑吧。想表现出: 你看我和你妈在玩,我们多开心啊。 当我看到这张照片的时候,我心里五谷杂粮。

一方面是 我看到我爸的头发,愈发的白了,白了大多半了。记得上一次有这感触的时候,还是我大二的时候,那次我从学校回家,他到车站来接我,我坐他后面,我发现他的白头已经有很多很多了,那时我差点哭了出来。爸妈真的老了,可我还没有给他们带来好的生活,我曾告诉他们说:等我实习你们就回家吧,不要工作了,我能养活我自己,以后也能养活你们。可是他们并不,他们还是在工作,烈日下,每每打电话跟我说太阳是有多么多大温度是有多么高,我心里都很难受。可是他们总会说一句话:“这都是为了你以后更轻松一点”。此时我却不知道该说些什么。

另一方面是 我看着我妈拍的那张照片,真的很糊,不是我妈不会拍照,也不是她眼神不好,真的是她的手机像素不好。我的爸妈,辛苦了大半辈子,其实也有很多积蓄了,虽然没有大城市里的那样多,但在老家里也算是稍微有一些钱的,可是他们却舍不得给自己多花一分钱,能将就用则用。每次电话,我都给他们讲让他们对自己好一点,我说 你们到底挣钱是为了做什么?给他们讲了很多道理,可是他们也总是会一句:“这都是为了你以后更轻松一点”。而我也只能强忍着,因为我现在还没有足够的能力,心里暗暗发誓:我会让他们过上轻松地日子。

我其实一直都在思考一个问题:人到底活在这个世界上是为了什么?科学家无法解释,这是一个哲学问题,没有人知道正确的答案。我们只是沧海中的一粟,如果掀不起波浪,那么就好好感受海的浩瀚吧。忙忙碌碌,短视频以及直播的崛起,可能真的是因为都市生活节奏变得太快,于是都在夹缝中去寻找那一丝丝快感,其实可以做的很多吧,摘自网上“试着每天自己为自己做美味的饭菜,试着经常联络一下家人好友,试着拾起丢下很久的小说,试着用心养一颗植物,试着在空气清新的清晨去跑步,试着约一下自己暗恋已久的女孩…”。看看足球比赛,看看电影,多出去走走,再忙也不要忘记生活吧。

快九点了,删删写写,也不道该放一些什么在日志上,那就这样吧。
祝自己二十三岁生日快乐

记录两张此时自己二十三岁的照片,没出门没刮胡子没收拾,一张沧桑一张微笑,生活亦如此吧。

xxx.jpg
xxx.jpg 隐私保护

—-二零一九年六月十一

单例模式的设计

我们都知道单例模式很简单,大概是这样:

1
2
3
4
5
6
7
8
9
10
//单线程单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){
instance = new Singleton();
}
return instance;
}
}

但是呢,在多线程条件下getInstance()并不是一个原子操作。由于代码没有使用任何同步机制,因此该线程可能会出现线程交错的情形:在instance还是null的时候,如果两个线程同时执行到 if(null==instance)那么会创建两个实例,从而违背了初衷。于是通过简单加锁来解决这种问题:

1
2
3
4
5
6
7
8
9
10
11
12
//简单加锁实现单例模式
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
synchronized (Singleton.class){//加入synchronized同步
if(null==instance){
instance = new Singleton();
}
return instance;
}
}
}

这种方式实现单例模式固然安全,但意味着每次调用 getInstance()都会申请锁,为了避免开销,我们想到了另一种办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的错误单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){//先检查是否为null,再执行之上的代码
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

通过这种方法,虽然第一次检查对变量instance的访问没有加锁从而使竞态仍然可能存在,它似乎避免了锁的开销又保障了线程的安全。然后对 instance = new Singleton();进行伪代码独立子操作:

1
2
3
obj=allocate(Singleton.class);//1、分配对象所需的存储空间
invokeConstructor(obj);//2、初始化obj的引用对象
instance=obj;//3、将对象引用写入共享变量

由于重排序的规则,临界区内的操作可以再临界区内重排序,因此JIT编译器可能将上述子操作重排序为:1->3->2,即在初始化对象之前将对象引用写入实例变量instace。由于锁对有序性的保障是有条件的,而操作1读取intance变量的时候并没有加锁,因此重排序是对1操作是有影响的:该线程可能看到一个未初始化(或者为初始化完毕)的实例,即intance不为null。于是该线程直接就直接返回这个instance变量所引用的实例,而实例可能是未初始化完毕的,这就是可能导致程序出错。明白问题的原因之后,解决方法也不难想到了:只需将instance变量加入volatile修饰则可。于是代码变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的正确单例模式实现
public class Singleton {
private static volatile Singleton instance=null;//加入volatile修饰
public static Singleton getInstance() {
if(null==instance){
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

到此为止,才正确实现安全的“单例模式”。

参考:《黄文海-Java多线程编程实战指南(核心篇)》

Android监听截屏事件之媒体读取的探索

最近做了一个需求:监听用户截屏,然后生成相关海报。
参考了Android 截屏事件监听的文章,大致思路是:a

1、利用ContentObserver用来监听指定Uri的所有资源变化,当媒体库中有相关图片新增的时候,则发送相关的通知。

2、得到回调的Uri后,借助ContentResolver在媒体数据库中查询最后一条数据

3、对数据做一些过滤。比如短时间重复截屏的情况以及其他App也插入了媒体文件等情况做处理。

不过有一些适配性的问题:

1、截屏后读取文件数据库后获取到件的绝对路径后,利用“screenshot”等关键字判断是否是截屏图片,并不能适配所有手机截屏的命名规则,以及其他应用同时间产生带有“screenshot”等关键词的文件也会有问题。

2、在某些型号手机中(现遇到Vivo)从数据库中读取的文件并不是获取到的最新的截屏文件,而且其他目录的文件,这里就有些难以理解了,所以今天取探究一下媒体数据库的读取。

其中ContentObserver如下代码所示:

` /**
 * 媒体内容观察者(观察媒体数据库的改变)
 */
private class MediaContentObserver extends ContentObserver {
    private Uri mContentUri;
    public MediaContentObserver(Uri contentUri, Handler handler) {
        super(handler);
        mContentUri = contentUri;
    }
    [@Override](https://my.oschina.net/u/1162528)
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        handleMediaContentChange(mContentUri);
    }
}`

其中获取最后一次更新的媒体文件时的代码(为便于查看 删除了判空处理代码):

 /**
 * 处理媒体数据库的内容改变
 */
private void handleMediaContentChange(Uri contentUri) {
    Cursor cursor = null;
    /** 读取媒体数据库时需要读取的列 */
    private static final String[] MEDIA_PROJECTIONS =  {
        MediaStore.Images.ImageColumns.DATA,
        MediaStore.Images.ImageColumns.DATE_TAKEN };
    try {
        // 数据改变时查询数据库中最后加入的一条数据
        cursor = mContext.getContentResolver().query(
                contentUri,
                 MEDIA_PROJECTIONS,
                null,
                null,
                MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
        );
        // 获取各列的索引
        int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
        int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);

        // 获取行数据
        String data = cursor.getString(dataIndex);
        long dateTaken = cursor.getLong(dateTakenIndex);

        // 处理获取到的第一行数据
        handleMediaRowData(data, dateTaken);
}

这次的目的主要探究的是从数据库获取相关信息的过程

1、Android 的多媒体如何存储?

Android的多媒体文件主要存储在 /data/data/com.android.providers.media/databases 目录下,该目录下有连个db文件:

内部存储数据库文件:internal.db

存储卡数据库:external-XXXX.db

媒体文件的操作主要是围绕着这两个数据库来进行,这两个数据库的结构是一样的。

这两个数据库包含这些表:
album_art 、audio 、search 、album_info 、audio_genres、 searchhelpertitle、albums、 audio_genres_map、 thumbnails、
android_metadata、 audio_meta、 video、artist_info 、audio_playlists 、videothumbnails、artists 、audio_playlists_map、
artists_albums_map 、images

2、表的结构
对于Images表:主要存储images信息。表结构如下:

CREATE TABLE images (
_id INTEGER PRIMARY KEY, 
_data TEXT,
_size INTEGER,
_display_name TEXT,
mime_type TEXT,
title TEXT, 
date_added INTEGER, 
date_modified INTEGER,
description TEXT,
picasa_id TEXT,
isprivate INTEGER,
latitude DOUBLE, 
longitude DOUBLE, 
datetaken INTEGER, 
orientation INTEGER, 
mini_thumb_magic INTEGER, 
bucket_id TEXT, 
bucket_display_name TEXT );

`

各字段所表示意思,如图所示:

图片来自:Android MediaProvider数据库模式说明

所以在截屏监听数据的时候所读取的数据库返回值,分别为:

_data :图片据对路径

datetaken:取子EXIF照片拍摄事件,空的话为文件修改时间

private static final String[] MEDIA_PROJECTIONS =  {
      MediaStore.Images.ImageColumns.DATA,
      MediaStore.Images.ImageColumns.DATE_TAKEN };

在查询过程中构造的数据库代码为:

public final Cursor query (Uri uri, 
    String[] projection,
    String selection, 
    String[] selectionArgs, 
    String sortOrder)

`
其中对应的构造参数官方解释为:

uri The URI, using the content:// scheme, for the content to retrieve.

projection A list of which columns to return. Passing null will return all columns, which is inefficient.

selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI.

selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.

sortOrder How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.


所以参数依次为:
所要查找的目标、所要的返回值、条件限制(类似sql中where)、匹配项、排序规则

所以这里的查询就显而易见了:获取最新图片数据库下data和datatoken列的数据

cursor = mContext.getContentResolver().query(
              contentUri,
              MEDIA_PROJECTIONS,
              null,
              null,
              MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
      );

然而…并不能解释vivo手机为什么查找出来不是最新截图的图片的问题

Android在子线程中创建Handler为什么会抛出异常?

复习一下消息机制,如下代码:

new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            handler = new Handler();
        }
    }.start();

如果执行会抛出异常:

Can't create handler inside thread Thread.currentThread() that has not called Looper.prepare()

这是为什么呢?

我们进入Handler的构造方法

    public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

从上面的代码我们很清新的可以知道mLooper 为null,那么就会抛出这样的异常,那么mLooper 为什么会为空呢?这是因为在线程中的Looper还未被创建,所以在Looper.myLooper()中sThreadLocal.get()就会返回null。

我们知道 Handler的作用是处理消息,将消息传递给MessageQueue,而MessageQueue存在于Looper中,如果没有Looper那么就没有MessageQueue,所以创建Handler时,Looper不能够为空。

所以以上代码可以进行一个修改:

private void test() {
    new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            Looper.prepare();
            handler = new Handler();
            Looper.loop();
        }
    }.start();
}

其中 Looper.prepare();为当前线程创建Looper并绑定在ThreadLocal中
Looper.loop();执行消息循环,这样子 Handler就能够正常工作了。

写在我即将毕业旅行前

忙碌的一天,又要到下班的时候了,一天好似很忙,其实回过头来仔细想想,一天好像并没有做太多的事情,然而确实是这样子度过一天了,就像我们的青春岁月,看样子并没有经历什么,但它就这样匆匆过去了,亦忧伤,亦憧憬。

还有两周就要回校参加毕业典礼了,向公司请了十天假,算上端午以及周末,大概有半个多月属于自己的日子吧,也算是大学最后一点还是以学生身份存在的时间了。早在几个月前我都在筹划这次出去游玩了,因为我想出去走走,想出去看看。纵观整个大学生涯,自己也算是去了不少地方了,屋子里车票、机票已经收纳了一沓了。看着每一张车票的起点与终点以及行程时间,还能想起那时候发生过的事,或许有些事还在记忆里面,或许可能存在我的日记里面,或许我什么也记不得了。

2015年8月15日 绵阳-上海:距离大学开学还有一个月了,父亲陪我度过了整个高三,待我拿到通知书后,我与父亲来到上海。父亲是一个节俭的人,买了绿皮车,将近两千多公里的路程,三十多个小时的行程,脚没有办法舒展开,时间长了别的难受。当看到那些没有座位的那些人要不停地给过路人让路的时候,我觉得自己也挺幸运的,至少我还有一个属于自己的位置,不需要为别人让路。在这煎熬的行程中,我并没有去抱怨,我只是一直在想,我以后一定要努力,一定不会让我和我父亲再受这样的遭遇。

2015年9月14日 上海-温州:马上就要大学开学了。离开了父母,将一个人去面对新的生活了,路过杭州的时候,车窗外一片开阔的平原,让我看到极具江南特色的景象:鳞次栉比的房屋伴着小河流,来来往往的车辆川流不息,很憧憬那样“小桥流水人家”的生活,这大概是对杭州的第一印象吧。

2016年1月22日 温州-成都:大学寒假第一次回家,运气很好,我和同学抢到了卧铺票,有了一个较舒服的乘车环境。与我通行的还有很多大学川籍同学,因此一路上也变得并不孤独,时而和他们“摆龙门阵”,累了就回床铺上面去休息一下,我记得我当时带了一本《浪潮之巅》。

2017年7月18日 温州-太原:第一次坐飞机,带着许多憧憬出发,但让人失望的是我错过了那一天的飞机,原本安排的好好的行程,一下子把我的计划打乱了。这时候我真的很迷茫,我不知道该去哪里,我也不知道该做什么,整个脑子是一片空白,在街上走走,最后回学校见到了精神支柱。然后去了太原平遥古城,喜欢太原老大爷那种街边悠然下着象棋的生活。

2017年7月10日 嘉兴-福州:又是一次暑假,我去嘉兴看望我爸,然后我爸他们提议去福州找我的大姑爷家玩,这一次我和我的父亲快吵了一架,因为我想让他买一张动车票,可父亲执着的只买绿皮车票,他觉得能节约钱,可是我觉得时间才是最重要的,那时候我没有一点收入,我说服不了他,绿皮车从晚上八点开到第二天早上八点,整个一晚我没有怎么睡,感慨太多,我也告诉我自己要努力。当时也写过一篇随笔:2017年710随笔 于嘉兴-福州列车

2017年9月02日 温州-杭州:这一次算是第一次以异地恋的身份去见了那时喜欢的人,一路忐忑。记得那时候带的是一本《小王子》,但是感觉没有看太懂,好像过了天真的年纪,已经看不太明白小王子的天真了。

2018年8月23日 杭州-太原: 或许真的是和太原有缘分,时隔一年再次去太原,这次我并没有错过,过了一年,变得成熟些了,这时候我已经在参加实习了。这次是代表整个学校唯一一只队伍进电子商务全国总决赛,压力与动力并存吧。

2019年2月14日 德阳-成都:这是最近一次从家出发去杭州在成都中转,在德阳站中因为高铁晚点,我遇到了一个女孩子,或许这是读大学以后认识的第一个非本大学的四川女孩子吧,在她回头的那一下,我感觉我好像心动了一下。或许是缘分,我和她是同一辆车,在德阳到成都只有短短半个小时的行车时间,居然车晚点将近两个小时,于是和她就聊啊,聊啊,从小学说到初中高中,从高中说到大学,再从大学说到实习。原来她也和我一样是大四的学生,原来我和她大学同学是初中高中同学,真的是缘分吧。第一次是多么的希望列车晚点的时间能够再长一点。只可惜,我本将心照明月,奈何明月照沟渠。

……

2019年6月10日 杭州-青岛、烟台、威海、大连? 或许是在被公司同事的鼓励下:现在有时间多出去玩玩吧,实习一天也没有多少钱,等以后正式工作了,有钱也没有时间了,趁现在,多出去走走吧。感谢芳姐姐对我的支助,让我有机会去计划这次旅行。

这一次,我是第一次一个人的旅行,我想在这炎热的夏天里,走出屋子,踏上行程。去坐一次轮渡、我还想再使用学生证享受一次学生特权、然后去看看海、去吹吹风、去看看更多的天空、去认识的人并说:“很高兴认识你”。也算是对整个大学青春岁月画上一个句号吧。
那接下来的一路上又会发生什么故事呢?

最后以王小波的《黄金时代》结尾吧:那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消逝,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。

我还年轻,我还可以到处走走,我还有很多想要去憧憬的、盼望的……

–于2019.06.04 7:24 即将下班回家

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×