Long Luo's Life Notes

每一天都是奇迹

By Long Luo

Android中自定义控件一直都是Android开发中的一个难点。

最近看大牛 @Tomcat的猫 写的 《Android群英传》 里面的第六章Android绘图机制及处理技巧,里面通过Canvas实现了一个如下所示的仪表盘:

仪表盘

在书中详细描述了如何实现这个仪表盘,这里就不赘述了,可以参考其 具体实现代码

但是这个表盘的指针是静止不动的,如果我们能让这个表盘的指针展示当前时间,随着时间而转动,那么我们就可以一款模拟时钟了。

那么,问题就来了:

如何才能让时钟的指针动起来呢?

这个问题,我们可以考虑分为2步去实现,第一步先绘制出当前的时间,第二部再让指针动起来。通过这样我们就可以实现一款模拟时钟

一、绘制当前时间

1.1 获取当前时间

要绘制当前时间,我们要先获取当前时间。

1
mTime.setToNow();

1.2 获取指针角度

当前时间可分为时,分,秒

我们最终要获取的是时分秒3个指针对应的精确角度

秒针角度:最简单,直接乘以6即可获得。 分针角度:当前分钟 * 6 + 6 * 秒 / 60F 时针角度:(当前小时 % 12) * 30 + 30 * 当前分钟角度 / 360F;

1.3 绘制指针

获取到指针角度之后,接下来就是在表盘绘制指针了。这里我们要利用canvas.rotate()这个方法来实现绘制指针,具体实现代码如下所示:

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
private void drawTime(Canvas canvas) {
// 当前时间
mTime.setToNow();

// 当前时间对应的角度
float secRot = mTime.second * 6;
float minRot = mTime.minute * 6 + 6 * mTime.second / 60F;
float hrRot = (mTime.hour % 12) * 30 + 30 * minRot / 360F;

Log.d(TAG, "hrRot=" + hrRot + ",minRot=" + minRot + ",secRot=" + secRot);

// 画指针
Paint paintHour = new Paint();
paintHour.setStrokeWidth(20);
Paint paintMinute = new Paint();
paintMinute.setStrokeWidth(10);
Paint paintSecond = new Paint();
paintSecond.setStrokeWidth(5);

canvas.save();

// 时针
canvas.rotate(hrRot, mWidth / 2, mHeight / 2);
canvas.drawLine(mWidth / 2, mHeight / 2, mWidth / 2, mHeight / 2 - mWidth / 2 + 320,
paintHour);
canvas.rotate(-hrRot, mWidth / 2, mHeight / 2);

// 分针
canvas.rotate(minRot, mWidth / 2, mHeight / 2);
canvas.drawLine(mWidth / 2, mHeight / 2, mWidth / 2, mHeight / 2 - mWidth / 2 + 230,
paintMinute);
canvas.rotate(-minRot, mWidth / 2, mHeight / 2);

// 秒针
canvas.rotate(secRot, mWidth / 2, mHeight / 2);
canvas.drawLine(mWidth / 2, mHeight / 2, mWidth / 2, mHeight / 2 - mWidth / 2 + 150,
paintSecond);
canvas.rotate(-secRot, mWidth / 2, mHeight / 2);

canvas.restore();
}
阅读全文 »

By Long Luo

前言

随着 VR 产业渐渐进入人们的视线,VR热潮正处于爆发前期。纵观国内几家VR领域的尖端企业,中国的VR硬件与国际水准相比并不低。如果以“国际标准”Oculus的VR硬件来说,可以说与其基本持平。对于过国内广大普通产品使用者来说无疑是个好消息,国内的VR团队在保持硬件的技术过关的前提下,性价比颇高。

然而,就算是最尖端的VR产品,目前依旧有致命的“硬伤”:晕眩感太强

很多VR体验者表示,在一段时间使用VR产品后,会出现不适,恶心,甚至呕吐。这成为了VR前进的最大的一个绊脚石,解决晕眩问题成为VR的燃眉之急

因此我们有必要对VR晕眩有一个更深入的了解,那么下面我们将详细讲解VR晕眩及目前市场上解决晕眩的一些技术

一、为什么会产生VR晕眩?

VR眩晕感根本原因是视觉大脑对运动的认知不同步,具体点就是眼睛看到的(VR)画面与从耳朵接收到的(真实位置)信息不匹配,导致脑负担加大,从而产生晕眩感。

VR眩晕有硬件引起的晕眩和软件引起的晕眩。

1. 硬件晕眩

VR硬件带来的晕眩主要是4个地方:GPU、感应器、显示屏、芯片成像透镜,以及瞳距和距离调整结构

解决硬件晕眩也很简单,使用最好的硬件就可以尽可能减少解决硬件层面的晕眩。但是现阶段消费者认为众多VR硬件是不成熟的呢?原因是因为厂商从成本的角度考虑,将VR设备的“性价比”做得更高一些。

如果从硬件层面着手试图消除眩晕的问题,一方面需要硬件市场降低成本,另外一方面还需要产业链的供给。    从设备的角度来说,硬件和软件设计的不合理造成了晕眩,那么解决方案也是从这两方面入手。目前来看,各大厂商都提出了自己的办法。硬件能发挥的作用更大一些,但也绝不能孤立来看。    ## 2. 软件晕眩

软件晕眩的概念就比较大了。

VR晕眩有以下几大原因:

<1>. 游戏内容

很多VR游戏,本身的内容就会产生晕眩。比如说你在坐“VR 过山车”时,视觉上你正处于画面中的状态,在做剧烈的高速运动,但是前庭系统却表示你并没有在运动,这时就会导致头晕。

<2>. 画面与现实世界的差异

VR显示器给出的画面变形严重、视野小,这些都与现实世界有差异,时间长了就会感觉头晕。

<3>. 画面滞后于动作

VR硬件的延迟造成时间上的不同步,当人转动视角或是移动的时候,画面呈现的速度跟不上,在VR这样全视角的屏幕中,这样的延迟是造成晕眩的最大问题,目前降低延迟是当下减弱VR眩晕的主要手段。

在VR这样全视角的屏幕中,这样的延迟是造成晕眩的最大问题。

<4>. 瞳距不一

由于每个人的瞳距不一,对某些人来说,人眼瞳孔中心、透镜中心、画面中心三点并非一线,从而出现重影现象,看久了人也会非常容易头晕。

<5>. 景深不同步

景深不同步,也是眩晕的原因之一。比如说,在你的面前,有一张桌子,在桌子上,近处放了一个杯子,远处放了一个玩偶。你看着近处的杯子,按理来说远处的玩偶应该模糊不清,但是现在,远处的玩偶也看的非常清晰。

总的来说,VR 设备对现实的模拟不够真实,还无法真正地欺骗到大脑,受到困扰的大脑不堪重负,才会造成眩晕的问题。

阅读全文 »

HashMap也是我们使用非常多的Collection,它是基于哈希表的Map接口的实现,以key-value的形式存在。在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置,我们总是可以通过key快速地存、取value。下面就来分析HashMap的存取。

一、定义

HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供Map接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map,这里标注Map LZ觉得应该是更加清晰吧!

1
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable

二、构造函数

HashMap提供了三个构造函数:

  • HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空HashMap。
  • HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空HashMap。
  • HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空HashMap。

在这里提到了两个参数:初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

HashMap是一种支持快速存取的数据结构,要了解它的性能必须要了解它的数据结构。

三、数据结构

我们知道在Java中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap也是如此。实际上HashMap是一个“链表散列”,如下是它数据结构:

HashMap数据结构图:

从上图我们可以看出HashMap底层实现还是数组,只是数组的每一项都是一条链。其中参数initialCapacity就代表了该数组的长度。下面为HashMap构造函数的源码:

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 HashMap(int initialCapacity, float loadFactor) {
//初始容量不能<0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: "
+ initialCapacity);
//初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能 < 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: "
+ loadFactor);

// 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;

this.loadFactor = loadFactor;
//设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
threshold = (int) (capacity * loadFactor);
//初始化table数组
table = new Entry[capacity];
init();
}

从源码中可以看出,每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;

/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
.......
}

其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。

上面简单分析了HashMap的数据结构,下面将探讨HashMap是如何实现快速存取的。

阅读全文 »

不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,所以线程相关的问题在面试中经常会被提到。

在典型的Java面试中,面试官会从线程的基本概念问起,如:为什么你需要使用线程,如何创建线程,用什么方式创建线程比较好(比如:继承thread类还是调用Runnable接口,然后逐渐问到并发问题像在Java并发编程的过程中遇到了什么挑战,Java内存模型,JDK1.5引入了哪些更高阶的并发工具,并发编程常用的设计模式,经典多线程问题如生产者消费者,哲学家就餐,读写器或者简单的有界缓冲区问题。仅仅知道线程的基本概念是远远不够的,你必须知道如何处理死锁,竞态条件,内存冲突和线程安全等并发问题。掌握了这些技巧,你就可以轻松应对多线程和并发面试了。

1. 什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成该任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。

2. 线程和进程有什么区别?

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

3. 如何在Java中实现线程?

在语言层面有两种方式。java.lang.Thread类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread类或者直接调用Runnable接口来重写run()方法实现线程。

4. 用Runnable还是Thread?

这个问题是上题的后续,大家都知道我们可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,哪个方法更好呢?什么情况下使用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。

6. Thread类中的start()和run()方法有什么区别?

这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。 start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程

7. Java中Runnable和Callable有什么不同?

Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的call()方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。

8. Java中CyclicBarrier和CountDownLatch有什么不同?

CyclicBarrier和CountDownLatch都可以用来让一组线程等待其它线程。与CyclicBarrier不同的是,CountdownLatch不能重新使用。

9. Java内存模型是什么?

Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行。这个关系定义了一些规则让程序员在并发编程时思路更清晰。

  • 线程内的代码能够按先后顺序执行,这被称为程序次序规则。
  • 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
  • 前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
  • 一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
  • 一个线程的所有操作都会在线程终止之前,线程终止规则。
  • 一个对象的终结操作必须在这个对象构造完成之后,也叫对象终结规则。
  • 可传递性

我强烈建议大家阅读《Java并发编程实践》第十六章来加深对Java内存模型的理解。

10. Java中的volatile变量是什么?

volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生,就是上一题的volatile变量规则。

11. 什么是线程安全?Vector是一个线程安全类吗?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector是用同步方法来实现线程安全的,而和它相似的ArrayList不是线程安全的。

12. Java中什么是竞态条件?举个例子说明。

竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。这种bugs很难发现而且会重复出现,因为线程间的随机竞争。一个例子就是无序处理,详见答案。

13. Java中如何停止一个线程?

Java提供了很丰富的API但没有为停止线程提供API。JDK1.0本来有一些像stop(),suspend()和resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后JavaAPI的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。 当run()或者call()方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile布尔变量来退出run()方法的循环或者是取消任务来中断线程。

14. 一个线程运行时发生异常会怎样?

这是我在一次面试中遇到的一个很刁钻的Java面试题,简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。 当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

15. 如何在两个线程间共享数据?

你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。这篇教程《Java线程间通信》(涉及到在两个线程间共享对象)用wait和notify方法实现了生产者消费者模型。

16. Java中notify和notifyAll有什么区别?

这又是一个刁钻的问题,因为多线程可以等待单监控锁,Java API的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

17. 为什么wait,notify和notifyAll这些方法不在thread类里面?

这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

18. 什么是ThreadLocal变量?

ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。 首先,通过复用减少了代价高昂的对象的创建个数;其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。

阅读全文 »

By Long Luo

  1. 一个.java源文件中是否可以包括多个类(不是内部类)?有什么限制?

可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致

  1. Java有没有goto?

Java中的保留字,现在没有在java中使用。

  1. 说说&&&的区别。

&&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。

&&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str != null && !str.equals("")表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。if(x==33 & ++y>0) y会增长,if(x==33 && ++y>0)不会增长

&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。

  1. switch语句能否作用在byte上,能否作用在long上,能否作用在String上?

switch(expr1)中,expr1只能是一个整数表达式或者枚举常量,整数表达式可以是int基本类型或Integer包装类型,由于,byte, short, char都可以隐含转换为int,所以,这些类型以及这些类型的包装类型也是可以的。显然,long和String类型都不符合switch的语法规定,并且不能被隐式转换成int类型,所以,它们不能作用于swtich语句中。

  1. short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1;有什么错?

对于short s1 = 1; s1 = s1 + 1;由于s1+1运算时会自动提升表达式的类型,所以结果是int型,再赋值给short类型s1时,编译器将报告需要强制转换类型的错误。

对于short s1 = 1; s1 += 1;由于 += 是java语言规定的运算符,java编译器会对它进行特殊处理,因此可以正确编译。

  1. char型变量中能不能存贮一个中文汉字?为什么?

char型变量是用来存储Unicode编码的字符的,unicode编码字符集中包含了汉字,所以,char型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在unicode编码字符集中,那么,这个char型变量中就不能存储这个特殊汉字。补充说明:unicode编码占用两个字节,所以,char类型的变量也是占用两个字节。

  1. 使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?

使用final关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的。

例如,对于如下语句:

1
final StringBuffer a = new StringBuffer("immutable");

执行如下语句将报告编译期错误:

1
a=new StringBuffer("");

但是,执行如下语句则可以通过编译:

1
a.append(" broken!"); 

有人在定义方法的参数时,可能想采用如下形式来阻止方法内部修改传进来的参数对象:

1
2
3
public void method(final  StringBuffer  param) {

}

实际上,这是办不到的,在该方法内部仍然可以增加如下代码来修改参数对象:

1
param.append("a");
阅读全文 »
0%