一款快速生成安卓shape的插件--NoCodeShape

一、NoCodeShape介绍

NoCodeShape是一个能可视化界面操作生成Android中Shape.xml的Android Studio插件。对于新手能更好的去接受Android中相对应的属性,对于资深程序员则能简化操作,快速生成shape.xml。

二、NoCodeShape使用方法

1. 下载和安装

同一般的Android Studio插件下载一样,可以直接Preferences->Plugins 搜索 NoCodeShape搜索出来结果直接安装再重启即可。
在这里插入图片描述
也可以去jetbrains插件管理的官网下载各个版本的插件
https://plugins.jetbrains.com/plugin/13325-nocodeshape/versions
然后Preferences->Plugins 再Install plugins from disk从本地安装

2. 如何使用

在新建一个shape.xml文件后,右键选择NoCodeShape或者直接按快捷键Common+U
在这里插入图片描述
然后选择自己想要的属性,随着点击事件的进行会生成相关xml代码,并在Android Studio右边有对应的shape形状的展示。如果对应shape.xml有相关属性,NoCodeShape也会生成对应shape.xml属性的操作界面,非常方便。
示例:
在这里插入图片描述

三、实现原理

实现原理总体来说并不复杂,主要是界面相关操作逻辑比较繁琐。

对于新生成的一个shape.xml来说只需要弹出一个新的操作界面,用户只需要点击对应模块的属性即可。项目使用了单例模式+Bulider建造者模式去管理各个Shape属性,分别生成 Shape、Solid、Corners、Stroke、Gradient的单例,其内部拥有一个Builder用来去承各类型的具体属性。

通过界面的操作,对其内部的Builder进行数据的填充,最后在完成各类操作后,将各类型中的Builder中的所有属性提取出并生成一份完整的xml字符串并将其粘贴到Android Studio的操作界面上。

实现主要分为两大类:

1. 拼接生成xml字符串

字符串拼接算是其中最复杂的部分,一是各shape的类型拥有较多数据,其中一些属性有逻辑存在性,二是生成最终Android Studio的xml字符串的时候格式存在比较多的处理。对于各类属性都继承于`BaseXml`,其内部拥有一个静态内部`Builder`类,以相对简单的Solid类来举例,如下所示:
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
public class Solid extends BaseXml {

private static Builder builder;
private static Solid instance = null;

public static Solid getInstance() {
if (instance == null) {
builder = new Builder();
instance = new Solid();
}
return instance;
}

public static class Builder extends BaseBuilder {
String color;
String colorValue;

public void setColor(String color) {
this.colorValue = color;
this.color = getAttrWithOutUnitStr("color", color);
}
@Override
public String getBuilderString() {
return StringUtils.getString(color);
}

@Override
public void clearData() {
StringUtils.clearObjectData(this);
}

@Override
public void analysisAttribute(Attributes attributes) {
Solid.getInstance().setChecked(true);
setColor(attributes.getValue("android:color"));
}
}

其类继承于抽象类BaseXml,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class BaseXml {
private boolean isChecked = false;
public String getCloser() {
return " />";
}
public String getStartTag() {
return "";
}
public String generateXmlString() {
return "";
}
protected String getLineFeedString() {
return "\n";
}
public boolean isChecked() {
return isChecked;
}
public BaseXml setChecked(boolean checked) {
isChecked = checked;
return this;
}
}

抽象类提取出来在字符串拼接阶段,各类型常用的基本操作,例如:返回“<solid”这类的开始标签,” />”结束标签等

其内部BaseXml拥有对应类拥有的所有属性的常用操作,其继承与抽象类BaseBuilder,代码如下所示

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
public abstract class BaseBuilder {
public abstract String getBuilderString();
public abstract void clearData();
public abstract void analysisAttribute(Attributes attributes);
protected final String getAttrWithUnitStr(String attributeType, String value) {
String unit;
if (TextUtils.isEmpty(value)) {
return "";
}
if (value.contains("px") || value.contains("dp")) {
unit = "";
} else {
unit = DefaultData.UNIT;
}
return "android:" + attributeType + "=\"" + value + unit + "\"";
}
protected final String getAttrWithOutUnitStr(String attributeType, String value) {
if (TextUtils.isEmpty(value)) {
return "";
}
return "android:" + attributeType + "=\"" + value + "\"";
}
protected final String getValueOutUnit(String value) {
if (TextUtils.isEmpty(value)) {
return value;
}
return value.replace("dp", "").replace("px", "");
}
}

BaseBuilder内部封装了一些属性的常用操作,例如生成:android:color="#FFFFFF"这样的字符串,获取是否带有单位的字串等。
并提供三个抽象方法:

1
2
3
public abstract String getBuilderString(); //获取Builder中所有属性拼接好的字符串
public abstract void clearData(); //清空Builder内部属性值
public abstract void analysisAttribute(Attributes attributes); //分析xml数据中的值,这个在第二大点“将原有shape.xml字符串转化为对应操作界面”中将会讲到

以上是基本数据的构造,在最后在控件交互的地方会调用CommonAction类的refreshAndWriteData

1
2
3
4
5
6
7
8
abstract class CommonAction {
JComponent component;
NoShapeDialog noShapeDialog;

void refreshAndWriteData() {
NoCodeShapeAction.callWriteData();
}
}

最后会调用基本的Action中的writeData()方法,其具体逻辑为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 数据写入xml文件
*/
private static void writeData() {
final Document document = FileDocumentManager.getInstance().getDocument(file);
if (document == null) {
try {
throw new Exception("Document对象为空");
} catch (Exception e) {
e.printStackTrace();
}
return;
}
new WriteCommandAction.Simple(project) {
@Override
protected void run() {
document.setText(XMLString.getInstance().generateXmlString());
//formatCode();
}
}.execute();
}

利用XMLString.getInstance().generateXmlString()获取各操作类型的所有属性将其拼接为一份完整的shape.xml文件的字段,最后调用插件系统的相关命令将字符串粘贴在系统对应的输入框中。

2. 将原有shape.xml字符串转化为对应操作界面

上面已经讲述类如何拼接生成xml字符串并将其粘贴到Android Studio界面上,此外NoCodeShape不仅支持新生成的shape.xml,同样也支持对旧shape.xml进行修改的能力。与第一步相比较主要多了一步读取Android Studio xml文档并将其转化为对应操作界面的过程。其主要是在操作界面初始化之前执行了如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void initSax() {
String text = FileDocumentManager.getInstance().getDocument(file).getText();
ShapeSaxHandler handler = new ShapeSaxHandler();
try {
handler.createViewList(text);
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

利用ShapeSaxHandler去解析xml里面的元素,参考了FindViewByMe的解析原理。

具体操作逻辑如下:

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
public void createViewList(String string) throws ParserConfigurationException, SAXException, IOException {
InputStream xmlStream = new ByteArrayInputStream(string.getBytes("UTF-8"));
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(xmlStream, this);
}
@Override
public void startDocument() throws SAXException {
if (shapePartList == null) {
shapePartList = new ArrayList<ShapePart>();
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
switch (qName) {
case "shape":
Shape.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "stroke":
Stroke.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "solid":
Solid.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "gradient":
Gradient.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "corners":
Corners.getInstance().getBuilder().analysisAttribute(attributes);
break;
default:
break;
}

逻辑其实很清楚,主要对startTag的判断,然后通过调用 public abstract void analysisAttribute(Attributes attributes);的方法对相应的 类型的Buidler进行一个赋值操作。

以Stroke为例:

1
2
3
4
5
6
7
8
@Override
public void analysisAttribute(Attributes attributes) {
Stroke.getInstance().setChecked(true);
setColor(attributes.getValue("android:color"));
setDashGap(attributes.getValue("android:dashGap"));
setWidth(attributes.getValue("android:width"));
setDashWidth(attributes.getValue("android:dashWidth"));
}

主要是获取到其中的属性,并对初始化的界面进行一些操作(例如选中或者赋值相关操作)。

四、总结

这个插件算是自己第一次做一个相对较实用的插件,都是利用工作的空闲事件进行编写,前前后后进行了将近一个月,其中收获颇多,但也踩过了各种各样的坑。在开发过程中由于相关文档较少,通过阅读官方文档还是有点小吃力,不过一步步还是走了下来,自己也得到了成长。其中坑也都添平了,但由于一些基础技术的欠缺,比如对Java GUI界面编程不太熟悉,导致开发过程中有很大一段时间都在跟界面作对,因此后面有机会将去深入了解Java的界面编程,争取能够将页面交互能够做到更好。

另外由于自身开发精力的原因,不能将插件做到完美,目前插件中还有如下几个问题:

1、Gradient中对应相关逻辑还需要再优化
2、还未支持Size跟Pading(从自身所处环境考虑用得较少,所以暂未支持)
3、对于颜色选择器默认打开后不支持对本地颜色字符串处理
4、存在大量的界面操作逻辑代码,需要优化

最后,希望大家能在使用过程中提出相关的意见或建议,也欢迎能一起加入到开发中,从而能将该插件做得更加完美。

项目地址:

https://github.com/VomPom/NoCodeShape

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就能够正常工作了。

Your browser is out-of-date!

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

×