0%

背景

在windows 10 上使用Android studio 开发,分分钟让你怀疑人生,编译运行一次看效果,5、6分钟很正常,7、8分钟是常有的事,忍无可忍,决心切换到 linux 环境。以个人的理解,Linux 的发行版都是基于相同的内核,所以比较各个发行版的时候,我个人主要比较 美观程度、使用方便程度以及可使用的软件数目。

在尝试包括ubuntu、中兴的新支点、优麒麟、deepin 之后,最终选择使用 deepin(深度技术)。deepin 有自己的应用商店,里面有包括 wps、qq、微信、TIM、foxmail、百度云盘、MindMaster、搜狗输入法等常用软件(有些是deepin-win版本的,但是优化得很好),并且如果需要安装其他的常用软件,也能通过deepin-wine方式安装;除此之外,deepin的界面极为美观,操作起来也特别方便,很多操作与windows类似。最重要的是,deepin天生对中文环境友好,并且无需费神选择镜像源,几乎安装上就能够正常使用,因此在这里,我强烈安利一波这个国产的,难得的linux发行版。

软件安装

一、安装Android studio

按照官方给的教程下载,并且解压到相应的目录即可,你可以把快捷方式的图标固定在decor上,如果之后偶尔通过这个图标不能正常启动,就进入 AS 的解压目录,进入bin 目录中,双击 studio.sh 运行就可以正常使用了。

二、安装rtx

打开终端,输入以下命令:

sudo apt install deepin.com.qq.rtx2015

卸载就输入:

sudo apt remove deepin.com.qq.rtx2015

三、安装openJdk

Android 开发使用 openJdk 就足够了,并且安装过程也是超级方便,以下命令就能搞定:

sudo apt-get install openjdk-8-jdk

四、安装git

作为developer,版本管理是必不可少的,git 最初是为 linux 版本管理而生,因此在linux上安装 git 也是极为方便:

sudo apt-get install git

其他软件

可以从deepin自带的应用商店安装。

卸载软件

linux彻底卸载软件步骤:以卸载wine为例

1、 删除软件及配置文件

sudo apt-get –purge remove wine

2、 删除没用的依赖包

sudo apt-get autoremove wine

3、 此时dpkg的列表中有”rc”状态的软件包,可以执行以下命令进行最后清理

sudo dpkg -l |grep ^rc|awk ‘{print $2}’ |sudo xargs dpkg -P

4、 然后删除安装包,位于/root/.wine和/home/usrname/.wine

sudo rm -rf /root/.wine
sudo rm -rf /home/usrname/.wine

Android Studio 使用

有时候,需要在 Android studio 的 terminal 中输入相应的gradle 命令,尤其是build出错又找不到错误的时候,就想使用命令:

gradlew compileDebugSources –stacktrace -info

但是我们直接在as的terminal中输入这行命令是不行的,并且我一般也懒得去配置环境。这时候,其实我们可以在项目的根目录下能发现 gradlew 的一个文件,我们能不能使用这个gradlew呢?事实上是可以的,只需要执行:

./gradlew compileDebugSources –stacktrace -info

如果提示权限不足,只需要chmod 777 项目的根目录修复下权限即可:

sudo chmod -R 777 chmod -R 777 /media/test/_dde_data/work/test

Android 源码下载

安装repo

Android 源码是使用 repo 管理的,所以我们首先安装repo,在安装repo前,你可能需要先安装 curl :

sudo apt-get install curl

安装完成后,可以通过命令查看 curl 的版本号,同时验证 curl是否安装完成

curl –version

步入正题,在deepn下,我们只需要在终端输入以下3条简单的命令即可完成repo的安装:

mkdir ~/bin
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

接着,我们还需要为 repo 配置环境变量,在终端中输入:

sudo vim /etc/bash.bashrc

当然,如果你安装了 sublime 的话,可以使用如下命令会更方便一些:

sudo subl /etc/bash.bashrc

在这个打开的文件尾部添加你的环境变量,比如我的就是这样的(你需要把路径换成你的,一般来说你将我的例子中的 glassx 换成你的用户名即可):

export PATH=$PATH:/home/glassx/bin:$REPOPATH

使环境变量生效:

source /etc/bash.bashrc

接下来,你可以正式下载Android 源码了。

下载 Android 源码

由于众所周知的原因,我们下载Android 源码链接不可能成功,因此推荐使用清华大学的源,官方主页为: https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/

你也可以直接直接复制链接: https://mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-latest.tar 放在迅雷中下载,这个文件略大,估计下载时间按照天来计算。

下载完成后,解压到指定文件夹(-C 用来指定解压后的文件存放位置):

tar -xvf /media/glassx/AndroidSourceCode/aosp-lastest.tar -C /media/glassx/AndroidSourceCode

在解压后的目录下, ls 的话什么也看不到,因为只有一个隐藏的 .repo 目录,cd 到这个文件夹下,执行 :

repo sync

就可以同步所有分支了,这个过程消耗的时间也略长,可能需要若干个小时,请耐心等待。至此,我们已经将 Android 源码成功下载下来并且解压同步分支了。

由于我同事有这个tar文件了,所以我就直接从他那里copy了过来,如果你同学或者同事也已经有这个tar文件了,可以直接复制这个文件。这里顺带说一下操作方式:如果你是windows ,并且他也是windows,则只需要他将这个文件所属的目录设置为共享文件夹,之后你在你的文件管理器中输入类似以下地址:

\192.168.12.13\d\共享文件夹

其中192.168.12.13 是你同事的ip,后面接的是共享文件夹的路径。

如果你的是deepin,你同事的也是linux,那么也好办,使用 ssh 登录你同事的电脑:

ssh 192.168.12.13

登录成功后,进入你同事的电脑,通过 scp 命令可以把文件给copy到你的电脑上。

很多时候我们想要做的就是自己编译一个 Android 系统安装在模拟器上,这个我目前还未完成,等完成后,再来续上。

deepin遇到的问题

今天遇到问题了,安装软件和升级都不行,报错:

1
2
3
4
5
sudo apt-get upgrade
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
E: 软件包 mindmaster 需要重新安装,但是我无法找到相应的安装文件。

或者是:

1
2
3
4
5
sudo apt-get install mindmaster
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
E: 软件包 mindmaster 需要重新安装,但是我无法找到相应的安装文件。

怎么着解决也不行,最后在网上找到这个方法,首先:

rm -rf /var/lib/dpkg/info/mindmaster*

然后:

sudo dpkg –remove –force-remove-reinstreq mindmaster

Android 进程和线程

进程(process) 是程序的一个运行实例,而线程(Thread)则是 CPU 调度的基本单位。

对于Android应用开发者而言,通常面对的都是 Activity、Service等组件,并不需要特别关心进程是什么,因而产生了一些误区,如部分研发者认为系统四大组件就是进程的载体。

很遗憾,虽然四大组件很符合我们对进程的印象,但是他们不能算是完整的进程实例,最多只能算进程的组成部分,从 AndroidManifest.xml 中也可以得到一点提示(这个xml是对应用程序的声明和描述):

1
2
3
4
<application android:label="launch performance">
<activity android:name="SimpleActivity">

...

可以看到,Activity的外围有一个名为 的标签,换句话说,四大组件都只是 “application”的零件。通过例子来分析让读者有个更全面的认识,通过Activity A启动Activity B,在 B 的onCreate(Bundle savedInstanceState) 打断点,查看断点详情可以看到如下图所示的内容:

activity启动

从这个实验中还解决了一个重要问题,即主线程到底怎么产生的,从上图的函数堆栈可以知道:主线程由ZygoteInit启动,经由一系列调用后最终才执行Activity的onCreate函数,并且,Zygote为Activity创建的主线程是 ActivityThread。以下是源码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
CloseGuard.setEnabled(false);

//只有主线程才能调用这个函数,普通线程应该使用prepare(),具体见对 Looper 的讲解
Looper.prepareMainLooper();

//主线程对应的handler
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

//这个main()是static的,因此这里需要创建一个实例
ActivityThread thread = new ActivityThread();

//Activity是由界面显示的,这个函数将与WindowManagerService 建立联系
thread.attach(false, startSeq);

//主循环开始
Looper.loop();

//如果程序运行到了这里,说明退出了上面的Looper循环
throw new RuntimeException("Main thread loop unexpectedly exited");
}

其实,启动Activity后,除了main thread 外还有多个binder线程,如下图所示(用于Binder的那些线程是在什么时候创建的,这个问题留到后面Binder章节详细解答):

运行的thread图

图中可以看到,B 被启动之后,主线程也始终只有一个,此时A退出了运行,但没有被杀掉,只是被压入了栈中。同样,如果我们启动一个Service,并把断点打在Service的onCreate方法的中,我们会发现,Service也是寄存于ActivityThread之中的,并且启动流程和Activity基本上一致,并且启动Service时,同样有Binder线程支持。限于篇幅,这里不做截图和代码。

按照Android系统设计:”By default,all componets of the same application run in the same process and thread(called the “main” thread)”,这可以理解为,对于同一个AndroidManifest中定义的四大组件,除非有特别的声明,否则它们都运行在同一个进程中(并且均由主线程处理事件)。如何证明呢?根据前面操作系统的基础知识,如果两个对象处于同一个进程空间,那么内存区域应该是可共享访问的,利用这个原理我们可以论证下:

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
//第1个activity
public class MainActivity extends BaseActivity {

static int ConstTemp = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//在启动SecondActivity 前,将ConstTemp值改为2,如果他们不是处于同一个进程,那么在
// SecondActivity中是无法获得更新后的值 2 的,只可能是 -1
ConstTemp = 2;
startActivity(new Intent(this,SecondActivity.class));
}
}


//第2个acitivity
public class SecondActivity extends BaseActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);

Log.e("SecondActivity","ConstTemp = " + MainActivity. ConstTemp);
}
}

最终输出的结果是2,说明都是在同一个进程中,当然,我们还可以通过查看二者的 PID 和 TID 的方法证明这两个Activity默认确实在同一个进程中。当然,Android还提供了特殊方式让不是同一个包的组件也可以运行于相同的进程中,这样做的优势是,它们可以方便地资源共享,而不用大费周章地进程间通信。这可以分为两种情况:

  1. 在AndroidManifest中的四大组件标签中加入 android:process 来表明这一组件想要运行在哪个进程空间。
  2. 针对整个程序包,可以直接在 标签中,加入 android:process 属性来指明想要依存的进程环境。

Handler、MessageQueue、Runable与Looper

参考专题里面的内容(现在还没放上去)

UI主线程-ActivityThread

前面提到Activity的部分源码,这里精简下再贴出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {

Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);

if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

普通线程使用Looper的代码如下:

1
2
3
4
5
6
7
8
9
10
11
class LooperThread extends Thread{
public Handler mHandler;
public void run(){
Looper.prepare();
mHandler = new Handler(){
public void handleMessage(Message msg){}
}

Looper.loop();
}
}

比较二者的代码可知其中的区别主要体现在:

  • prepareMainLooper 和 prepare,普通线程prepare就可以了,而主线程使用 prepareMainLooper ,主要是多了一步给sMainLooper 赋值的操作,这样,我们就能从主线程中通过 getMainLooper() 方式获得主线程的 Looper。

  • Handler不同,普通线程申城一个与Looper绑定的Handler,而主线程是从当前线程中获取的Handler,也就是说,ActivityThread 提供了一个“事件管家”,以处理主线程中各种消息。

Thread 类

Thread 类的内部原理

Thread 实现了 Runnable ,也就是说线程是“可执行的代码”。我们一般通过2种方式使用Thread :

1
2
3
4
5
6
//第1种
MyThread thr = new MyThread(...);
thr.start();

//第2种
new Thread(Runnable target).start();

这两种方法最终都通过 start 启动,它会间接调用Runable 的 run 实现.

线程的休眠和唤醒

控制线程的相关方法我们至少可以想到以下几个: wait()、notify()、notifyAll()、interrupt()、join() 和 sleep()。

  1. wait 和 notify/notifyAll

和其他方法不同,这3个函数是由 Object 类定义的——意味着它们是任何类的共有“属性”,那为什么这么设计呢?官方对wait的解释是:

Causes the calling thread to wait until another thread calls the notify() or notifyAll() method of this object

当某个线程调用一个 Object 的wait 方法时,系统就要在这个 Object 中记录这个请求。因为调用者很可能不止一个,所以可使用列表的形式来逐一添加它们。当后期唤醒条件满足时, Object 既可以使用 notify 来唤醒列表中的一个等待线程,也可以通过 notifyAll 来唤醒列表中的所有线程。值得注意的是,调用者只有称为 Object 的 monitor 后,才能调用它的 wait 方法,而称为一个对象的 monitor 有以下3种途径:

  • 执行这个 object 的 synchronize 方法
  • 执行一段 synchronize 代码,并且是基于这个 object 做的同步
  • 如果 object 是 Class 类,可以执行它的 synchronize static 方法
  1. interrupt :如果说wait是自愿行为, 那 interrupt 就是 “被迫” 的了,它的意思就是“中断”。
  2. join,用以下例子说明:
1
2
3
4
5
Thread t1 = new Thread(new ThreadA());
Thread t2 = new Thread(new ThreadB());
t1.start();
t1.join();
t2.start();

它希望达到的目的就是只有当 t1 线程执行完成时,我们才接着执行后面的 t2.start() 。这样就保证了两个线程顺序执行。

  1. sleep:它和 wait 一样都是属于“自愿”的行为,只不过 wait 是等待某个 object ,而sleep 是等待时间,一旦设置的时间到了就会被唤醒。

Thread 实例

讲一个典型范例来理解Thread:假如我们使用 SeekBar 开控制系统音效,要求:(1)UI界面响应流畅,(2)并且要能反映出音效的变化,(3)并且系统稳定。同时,有以下几个前提:

  1. 向系统发送音效调整的命令是个耗时操作
  2. 频繁向系统发送调整命令会导致死机
  3. 用户的操作是随意的,没有规律的(用户可能飞快地拉动,也有可能慢慢拉动)

对于要求1,可以将发送命令这个耗时操作放到一个独立线程执行,这样就不会影响UI,保证流畅。
而根据3个前提条件,条件2和条件3是有矛盾的,如果要实时听到音效变化,在seekbar进度变化错城中需要不停地发送请求,而用户快速滑动导致产生大量请求,可能会引起死机,从而违背第3个要求。

再来想想其它简单方法,当启动一个新的线程处理调整请求时,显然需要把这些请求先放入消息队列中再排队处理,假设用户1s内产生了24个请求,那么队列中的数量将会陆续增加(假设500ms才处理完一个),直到用户操作结束。那么,实际上这些请求值是有优先级的,即后产生的调整值更贴近用户想要的效果。根据这个思想,我们可以适当控制消息队列中元素的数量,比如:

  • 当产生新的调整值时,先清空消息队列,然后再把请求入队。
  • 当产生新的调整值时,先判断消息队列的数量,根据实际情况删除部分消息,然后才请求入队。
    采用这种方式可以保证最后一个入队的请求总是可以被处理的,这也就意味着用户最终选择的音效值是可以体现出来的。示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Thread mBusinessThread = null;
private boolean mBusinessThreadStarted = false;
private BusinessThreadHandler mBusinessThreadHandler = null;
private void startBusinessThread(){
if(true == mBusinessThreadStarted)
return;
else
mBusinessThreadStarted = true;

mBusinessThread = new Thread(new Runnable(){
@Override
public void run(){
Looper.prepare();
mBusinessThreadHandler = new BusinessThreadHandler();
Looper.loop();
}
});
mBusinessThread.start();
}

上述代码使用Looper.loop 来不断处理调整请求,这些请求是通过 mBusinessThreadHandler 发送到 mBusinessThread 的消息队列中的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BusinessThreadHandler extends Handler{
//重写 sendMessage
@Override
public boolean sendMessage(int what,int arg1,int arg2){
//清理消息队列中未处理的请求
removeMessages(what);
//发送消息到队列
return super.sendMessage(obtainMessage(what,arg1,arg2));
}

public void handleMessage(Message msg){
switch(msg.what){
case MSG_CODE:
//执行耗时操作
break;
default:
break;
}
}
}

sendMessage 方法中首先清除了消息队列中还未被处理的请求,这样一方面降低了程序向系统发送请求的频率,加快了相应速度和UI流畅性;另一方面保证 BusinessThreadHandler 下次取到的是优先级较高的调整请求值,保证用户听到实时的音效变化。

Android 应用程序的典型启动流程

我们了解到 Android 系统中一个应用程序的主体是由 ActivityThread 构成的,并且 Android 系统是基于 Linux 的,原则上说它的应用程序并不只是 APK 一种类型,换句话说,所有 Linux 支持的应用程序都可以通过一定方式运行在 Android 上(一些系统级应用程序就是以这种方式存在的),为了叙述统一,我们这里所指的应用程序都是 APK 类型的应用程序。它们通常由两种方式在系统中被启动:

  • 在 Launcher 中点击相应的应用程序图标启动
  • 通过 startActivity 启动

这两种启动方式的流程基本上是一致的,最终都会调用 ActivityManagerService(以下简称 AMS) 的 startActivity 来完成。在新的 Activity 启动钱,原先处于 resume 状态的 Activity 会被 pause ,这种管理方式比 Windows 的多窗口系统简单得多,将一个 Activity 置为 pause 主要通过此 Activity 所属进程的 ApplicationThread.schedulePauseActivity 方法完成,ApplicationThread 是应用程序进程提供给 AMS 的一个 Binder 通道。假设即将启动的 Activity 所属的进程并不存在,那么 AMS 还需要先把它启动起来。

ActivityThread 启动并做好相应的初始化工作后,需要调用 attachApplication 来通知 AMS ,后者才能继续执行未完成的 startActivity 流程。具体而言, AMS 通过 ApplicationThread.scheduleLaunchActivity 请求应用程序来启动一个指定的 Activity ,之后一系列工作就要靠应用进程自己来完成,如 Activity 创建 Window,遍历 View Tree 等。

在进入后面的章节前,这里先大略熟悉 startActivity 的流程。

概述

在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,所有的虚拟机执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、方法返回地址等信息。每一个方法从调用开始到执行完成的过程,对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响。典型的栈帧结构如下图:

类的生命周期

一个线程中的方法调用链可能会很长,在活动线程中,只有位于栈顶的栈帧才是有效的。

局部变量表

局部变量表示一组变量值存储空间,用于存放 方法参数 和 方法内部蒂尼的局部变量。在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static方法),那局部变量表中第0位索引默认用于传递方法所属实例的引用,在方法中可以通过”this”关键字来访问这个隐含参数。剩下的参数则按照参数表顺序排列,参数表分配完毕后,再分配方法体内部定义的变量。

为了节省栈帧内存,局部变量表的空间是可以重用的,方法体中定义的变量,其作用域不一定会覆盖整个方法体,当超出作用范围时,它的空间就可能交给其他变量使用,不过这样的设计出了节省栈帧空间外,还会伴随额外的副作用,比如导致垃圾不能及时回收,以下举例说明():

1
2
3
4
public static void main(String args){
byte[] placeholder = new byte[64 * 1024 * 1024];
System.gc();
}

代码中向内存中填充了64MB数据,然后通知虚拟机进行垃圾收集,但是我们可以发现结果并没有回收。不过,这里没有回收placeholder所占的内存还说得过去,因为在执行gc时,placeholder还处于作用域之呢,虚拟机自然不会回收,下面把代码改下(代码8-2):

1
2
3
4
5
6
7
8
public static void main(String args){

{
byte[] placeholder = new byte[64 * 1024 * 1024];
}

System.gc();
}

加入花括号之后,placeholder的作用域被限制在花括号之内,从代码逻辑上讲,gc的时候,placeholder就已经不可能再被访问了,但是执行以下,发现还是没有被回收。在解释之前,再次修改下代码试试(代码8-3):

1
2
3
4
5
6
7
8
9
public static void main(String args){

{
byte[] placeholder = new byte[64 * 1024 * 1024];
}

int a = 0;//添加这句
System.gc();
}

再次运行,发现内存被正确地回收了,看起来很莫名其妙。placeholder 能否被回收的根本原因是:局部变量表中是否还存有关于placeholder数组的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作,placeholder原本所占的空间还没有被其他变量复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的引用。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。

但是如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、实际上已经不会再使用的变量,手动将其设置为null值(代替上面例子中的 int a = 0,把变量对应的局部变量表中的空间清理掉)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到JIT的编译条件)下的“奇技”来使用。

上述例子说明了在某些情况下赋null值操作确实是有用的,但是不应对这种操作有过多依赖,更没必要当做普遍的编码规则来推广,原因有两点:一是从编码角度讲,以恰当的变量作用域来控制变量的回收时间才是最优雅的解决方案。二是从执行角度讲,使用赋null值操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上的,赋null值的方式在经过JIT编译优化之后就会被消除掉,这时候将变量设置为null是没有意义的。以前面的例子来说,代码8-2 的形式经过JIT编译后,System.gc() 执行时,就可以正确地回收内存了,无需再写成代码 8-3 的样子。

还有一点需要注意,类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;一次在初始化阶段,赋予程序员定义的初始值,因此,即使在初始化阶段没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量不一样,它并不存在“准备阶段”,因此如果定义了局部变量但是没有赋初始值是不能使用的。

这段做的笔记有点多,这是因为个人以前在一些书籍上看到有观点说,推荐及时将不使用的对象手动置为null,但是解释语焉不详,在这里从虚拟机角度看到了解释,故详细记下来

操作数栈

操作数栈(operand stack)是一个后入先出的栈,痛局部变量表一样,操作数栈的最大深度也在编译时写入到Code属性中了。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容。

举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令是,会将这两个int值出栈并相加,然后将相加的结果入栈。

方法调用

方法调用并不等同于方法执行,方法调用唯一的任务就是确定被调用方法的版本(即调用哪一个方法),还不设计方法内部的具体运行。前面已经讲过,Class文件的编译过程不包含传统编译中的连接步骤,一切方法调用在Class文件里面都只是符号引用,而不是方法在时机运行时内存布局中的入口地址(相当于之前说的直接引用)。

解析

前面提到,所有方法调用中的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用。这种解析能成立的前提是:“编译期可知,运行期不可变”,这类方法的调用就称为“解析(Resolution)”。符合这种特性的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。

只能被invokestatic 和 invokespecial 指令调用的方法都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实力构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法,与之对应的称为虚方法(final方法除外)。

被final 修饰的方法虽然是通过invokevertual 指令调用的,但是由于它无法被覆盖,没有其他版本,所以Java语言规范中明确说明final是一种非虚方法。

分派

Java具备面向对象的3个基本特征:继承、封装以及多态。分派调用过程将会揭示多态性特征的一些基本体现,如“重载”和“重写”在Java虚拟机中是如何实现的。

1、 静态分派

在讲解静态分派之前,看一段经常出现在面试题中的代码,方法静态分派代码如下面代码8-6所示:

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
public class StaticDispatch{
static abstract class Human{
}

static class Man extends Human{
}

static class Woman extends Human{
}

public void sysHello(Human guy){
System.out.println("hello ,guy !");
}

public void sysHello(Man guy){
System.out.println("hello ,gentleman !");
}

public void sysHello(Woman guy){
System.out.println("hello ,lady !");
}

public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}

打印结果:

hello ,guy!
hello ,guy!

这段代码实际上是考验读者对重载的理解程度,但这里为什么会选择执行参数类型为 Human 的重载呢?解决问题前,先按如下代码定义两个重要概念:

Human man = new Man();

上面代码中的”Human”称为变量的静态类型(Static Type)或叫做外观类型(Apparent Type),后面的 “Main” 则称为变量的实际类型(Actual Type)。静态类型和实际类型在程序中都可以发生一些变化,**区别是,静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才确定,例如下面代码:

1
2
3
4
5
6
7
//实际类型变化
Human man = new Man();
man = new Woman();

//静态类型变化
sr.sayHello((Man)man);
sr.sayHello((Woman)man)

回到上面代码 8-6 中,由于虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的,并且静态类型是编译期可知的,因此在编译阶段,根据静态类型选择了 sayHello(Human) 作为调用目标,并把这个方法的符号引用写到main方法里的两条invokevirtual指令参数中。静态分派的典型应用是方法重载,另外,编译器虽然能确定出方法的重载版本,但很多情况下这个重载版本并不是”唯一“的,往往只能确定一个”更加合适的“版本,以下代码 8-7演示了何为”更加合适“的版本:

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 class Overload{
public static void sysHello(Object arg){
System.out.println("hello Object");
}

public static void sysHello(int arg){
System.out.println("hello int");
}

public static void sysHello(long arg){
System.out.println("hello long");
}

public static void sysHello(Character arg){
System.out.println("hello Character");
}

public static void sysHello(char arg){
System.out.println("hello char");
}

public static void sysHello(char... arg){
System.out.println("hello char...");
}

public static void sysHello(Serializable arg){
System.out.println("hello Serializable");
}

public static void main(String[] args){
sayHello('a')
}
}

打印结果:

hello char

如果注释掉 sysHello(char arg) 方法,则会打印 “hello int”;再注释掉 sysHello(int arg) 方法,则会打印 “hello long”;再注释掉 sysHello(long arg) 方法,则会打印 “hello Character”;如此下去,输出的结果会不断变化。这其实也还好理解: ‘a’首先是个char,自然首先输出 hello char;如果没有该方法,则自动类型转换为int,如果再没有此方法,则会进一步转换为long类型(按照 char -> int -> long -> float -> double的顺序进行匹配,但不会匹配到byte和short类型,因为转型到这两种是不安全的)。所以上述代码在注释掉 sysHello(long arg) 后,输出变为 “hello Character” ,此时发生了自动装箱。如此注释下去,”hello char…” 将会是最后一个打印的,可见变长参数的重载优先级是最低的,甚至比Object还低。值得注意的是,有一些在单个参数中成立的自动转型,如char转型为 int,在变长参数中是不成立的。

2、 动态分派

动态分派和多态性的另一个重要体现——重写(Override)有很密切的关联,结合前面Man和Woman一起sayHello的例子来看如下代码:

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
public class DynamicDispatch{
static abstract class Human{
protected abstract void sayHello();
}

static class Man extends Human{
@Override
protected void sayHello(){
System.out.println("man say hello");
}
}

static class Woman extends Human{
@Override
protected void sayHello(){
System.out.println("woman say hello");
}
}


public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();

man = new Woman();
man.sayHello();
}
}

打印结果:

man say hello
woman say hello
woman say hello

结果不出人意料,但是虚拟机是如何知道要调用哪个方法?这里显然不能再根据静态类型来决定,因为静态类型都是Human的两个变量 man 和woman 在调用 sayHello 方法时执行了不同的行为,并且man在两次调用中执行了不同的方法。因此可以看出,这只是因为变量的实际类型不同。因为invokevirtual指令的运行时解析过程大致如下:

第一步:找到操作数栈顶的第一个元素所指向的对象的实际类型,记做C
第二步:如果在C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过,则返回方法的直接引用;否则,返回 java.lang.IllegalAccessError异常。
第三步:否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验证。
第四步: 如果始终没有找到合适的方法,则抛 java.lang.AbstractMethodError异常。

3、 单分派与多分派

方法的接收者与方法的参数统称方法的宗量。根据分派基于多少种宗量,可以将分派划分为但分派和多分派,单分派是根据一个宗量对目标方法进行选择;多分派就是根据多个宗量对目标方法进行选择。定义比较拗口,对照下面这个例子,分析 Father 和 Son 做“艰难决定”之后,就不难理解了:

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
//单分派、多分派的例子
public class Dispatch{
static class QQ {}

static class _360 {}

public static class Father{
public void hardChoice(QQ arg){
System.out.println("father choose qq");
}

public void hardChoice(_360 arg){
System.out.println("father choose 360");
}
}

public static class Son extends Father{
public void hardChoice(QQ arg){
System.out.println("son choose qq");
}

public void hardChoice(_360 arg){
System.out.println("son choose 360");
}

}

public static void main(String[] args){
Father father = new Father();
Father son = new Son();

father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}

运行结果:

father choose 360
son choose qq

根据以上代码,我们看看编译阶段编译器的选择过程,也即静态分配过程。这时候选择目标方法的依据有两点:静态类型以及参数,最终产物是产生了两条invokevirtual 指令,两条指令的参数分别为常量池中指向 Father.hardChoice(_360) 以及 Father.hardChoice(QQ) 方法的符号引用,因为是根据两个宗量选择,所以Java语言的静态分派属于多分派类型。

再看看运行阶段虚拟机的选择,也即动态分派的过程。在执行 “son.hardChoice(new QQ())” 时,由于编译期已经决定目标方法的签名必须为 hardChoice(QQ) ,此时,参数的静态类型和参数的实际类型都对方法的选择不会构成任何影响(只要是QQ类型,管你是“腾讯QQ”还是“奇瑞QQ”),唯一可以影响虚拟机选择的因素只有方法接受者的实际类型是 Father 还是 Son ,因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

因此,到目前为止(Java 1.8),我们可以说Java语言是一门静态多分派、动态单分派的语言。

4、虚拟机动态分派的实现

上述的分派结局虚拟机在分派过程中“会做什么”,具体如何做到的,不同虚拟机之间会有差异。由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜寻合适的目标方法。但是在实际实现中并不会进行如此频繁的搜索,面对这种情况,最常用的手段就是为类在方法区中建立一个**虚方法表(Vitual Method Table,即vtab),使用虚方法表索引来替代元数据查找以提高性能.基于上方的代码,做如下虚方法表示意:

虚方法表

虚方法表中存放着各个方法的实际入口地址,如果方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类方法相同,否则,替换为子类具体实现版本的地址入口。因此图中Son的hardChoice方法并没有和父类指向同一处。

5、静态变量、方法的继承、重写、重载(非书本内容,自己测试过后添加)

首先看一段代码:

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 StaticClassFarther {
public static int a = 3;

public static void tt(){
System.out.println("father 的 tt 方法");
}
}

public class StaticClassSon extends StaticClassFarther {
public static int a = 4;

//注释1
/*public void tt(int b){
System.out.println("son 的 非静态 tt 重载方法");
}*/

public static void tt(int b){
System.out.println("son 的 静态 tt 重载方法");
}

//注释2
/*public void tt(){
System.out.println("son 的非静态 tt 方法");
}*/

public static void tt(){
System.out.println("son 的 tt 方法");
}
}

public class StaticMainClass {
public static void main(String[] args) {
StaticClassFarther farther = new StaticClassFarther();
StaticClassFarther mix = new StaticClassSon();
StaticClassSon son = new StaticClassSon();

farther.tt();
System.out.println("farther 中a = " + farther.a + "\n");


mix.tt();
System.out.println("mix 中a = " + mix.a + "\n");

son.tt();
son.tt(4);
System.out.println("son 中a = " + son.a + "\n");

System.out.println("StaticClassFarther.a = " + StaticClassFarther.a);
System.out.println("开始调用:StaticClassFarther.tt ");
StaticClassFarther.tt();

}
}

其中注释1 是非静态重载,是可以编译通过的,但是注释2非静态重写是不行的,一定要静态重写。再看看输出结果:

father 的 tt 方法
farther 中a = 3

father 的 tt 方法
mix 中a = 3

son 的 tt 方法
son 的 静态 tt 重载方法
son 中a = 4

StaticClassFarther.a = 3
开始调用:StaticClassFarther.tt
father 的 tt 方法

从结果可以看出,子类重写并没有改变父类的值,通过 StaticClassFarther.tt 调用结果还是没变化。并且,具体实现已经是子类的情况下:StaticClassFarther mix = new StaticClassSon();
输出的结果还是父类的(即输出以下内容): father 的 tt 方法 和 mix 中a = 3

动态语言支持

略,后续看到这里补上

基于栈的字节码解释执行引擎

Java 语言经常被人们定位为“解释执行”的语言,在Java 1.0 时代,这定义还算准确,但当主流的虚拟机中都包含了即时编译器后,Class 文件中的代码到底会被解释执行还是编译执行,就成了只有虚拟机才知道的事情。再后来,Java也发展处了可以直接生成本地代码的编译,而C/C++也出现了通过解释器执行的版本,这时候再笼统地说“解释执行”,对于整个Java语言来说几乎没有意义。

基于栈的指令集与基于寄存器的指令集

Java 编译器输出的指令,基本上是一种基于栈的指令集架构,它们依赖操作数栈进行工作;与之相对的另一套常用的指令集架构是基于寄存器的指令集。那么二者有何不同呢?举个简单的例子,分别使用两种指令集计算“1+1”,基于栈的指令集会是这个样子:

iconst_1
iconst_1
iadd
istore 0

两条icons_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈、相加,然后把结果放回栈顶,最后 istore_0把栈顶的值放到局部变量表的第0个Slot中。而如果基于寄存器,那程序可能就会是这样:

mov eax, 1
add eax, 1

mov 指令把EAX寄存器的值设为 1,然后add指令再把这个值加1,结果就保存在 EAX寄存器里面。了解了区别之后,那么这两套指令集哪一种更好?其实是各有所长,基于栈的指令集的主要优点是可移植,而基于寄存器的话,程序要直接依赖于这些硬件寄存器而不可便面地受到硬件的约束。栈架构指令集的主要缺点是执行速度相对来说会稍慢一些,完成相同功能所需要的指令一般会比寄存器的要多,因为出栈和入栈操作本身就产生了相当多的指令数量,更重要的是,栈实现在内存之中,频繁的栈访问也意味着频繁的内存访问,对处理器来说,内存始终是执行速度的瓶颈。

基于栈的解释器执行过程

以示例讲述解释器执行过程, 略

本章小结

6、7、8章,我们分析了 Java程序是如何存储的、如何载入(创建)的以及如何执行的问题,第9章将一起看看这些理论知识在具体开发中的经典应用。

概述

上一章了解了Class文件存储格式,在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用,那么,虚拟机如何加载这些文件?

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在Java语言中,类型的加载、连接和初始化都是在程序运行期间完成的,这令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性。

例如,可以等到运行时再指定其实际的实现类:用户可以通过Java预定义和自定义的类加载器,让一个本地的应用程序可以在运行时从网络或者其他地方加载一个二进制流作为程序代码的一部分。

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期如下图所示:

类的生命周期

总共有7个阶段,其中验证、准备、解析 这3个部分统称为连接(Linking),并且加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载按照上述顺序按部就班地开始(不是“进行”,也不是“完成”,只是按部就班地“开始”,因为这些阶段通常都是相互交叉地混合式进行)。

虚拟机规范中严格规定有且只有5种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

这4条指令的最常见的Java代码中的场景是:使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

  1. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则先触发其初始化。
  2. 当初始化一个类,如果发现其父类还没有进行过初始化,则需要先触发其父类初始化。
  3. 虚拟机启动时,虚拟机会先初始化用户指定的主类(包含main()方法的那个类)
  4. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上5种场景中的行为称为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用。以下举3个例子来说明何为被动引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//被动使用类字段演示一:
//通过子类引用父类的静态字段,不会导致子类初始化

public class SuperClass{
static{
Systemt.out.printlin("SuperClass init!");
}

public static int value = 123;
}

public class SubClass extends SuperClass{
static{
Systemt.out.printlin("SubClass init!");
}
}

//非主动使用类字段演示
public class NotInitialization{
public static void main(String[] args){
System.out.printlin(Subclass.value);
}
}

上述代码运行后,只会输出 “SuperClass init!” ,对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

1
2
3
4
5
6
7
8
//被动使用类字段演示二:
//通过数组定义来引用类,不会触发此类的初始化

public class NotInitialization{
public static void main(String[] args){
SuperClass[] sca = new SuperClass[10];
}
}

这里复用了上面的 SuperClass ,运行之后,发现没有输出 “SuperClass init!” ,说明没有触发 SuperClass 的初始化阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//被动使用类字段演示二:
//常量在便一阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

public class ConstClass{
static{
Systemt.out.printlin("ConstClass init!");
}

public static final String HELLOWORLD = "hello world";
}

public class NotInitialization{
public static void main(String[] args){
Systemt.out.printlin(ConstClass. HELLOWORLD);
}
}

上述代码运行之后,也没有输出 “ConstClass init!” ,虽然在 Java 源码中引用了 ConstClass 中的常量 HELLOWORLD ,但其实在编译阶段通过常量传播优化,已经将此常量值 “hello world” 存储到了 NotInitialization 类的常量池中,以后 NotInitialization 对常量 ConstClass. HELLOWORLD 的引用时机都被转化为 NotInitialization 对自身常量池的引用了。也就是说,实际上 NotInitialization 的 Class 文件之中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 之后就不存在任何联系了。

接口的加载过程与类加载过程稍有一些不同,接口与类真正有所区别的是前面讲述的5种“有且仅有”需要开始初始化场景中的第3种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

顺带说明一下,上面的代码都是用静态语句块 “static{}”来输出初始化信息的,但接口中不能使用 “static{}”语句块。

类加载过程

加载

“加载”是“类加载(Class Loading)”过程的一部分,不要混淆。在加载阶段,虚拟机需要完成以下3件事情:

  1. 通过类的全限定名获取类的二进制字节流
  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

这3点规范并不算具体,例如第一条,并没有指明要从哪里获取,怎么获取,因此充满创造力的开发人员玩出了各种花样:

  • 从 zip 包中读取,最终成为日后 jar、ear、war 格式的基础
  • 从网络中获取,比如Applet
  • 运行时计算生成,这种场景用得最多的就是动态代理技术。

验证

验证是连接阶段的第一步,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击。从整体来看,验证阶段大致上会完成下面4个阶段的检验动作:

文件格式验证

魔数0xCAFEBABE开头、主次版本是否在虚拟机处理范围内等。

元数据验证

是否有父类-除Object外,所有类都有父类、父类是否继承了不允许被继承的类-final修饰,是否实现了父类或者接口要求实现的所有方法。

字节码验证

通过数据流和控制流分析,确定程序语义是否合法、符合逻辑的。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。

符号引用验证

这个校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段发生。通常校验:通过全限定名是否能找到对应的类、符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可以被当前类访问 等等。

准备

准备阶段正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这里两个容易产生的混淆的概念强调一下:首先,这时候进行内存分配的仅仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时一起分配在java堆中。其次,这里说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:

public static int value = 123;

那变量value在准备阶段过后的初始值为 0 而不是 123。把value赋值为123的动作将在初始化阶段才会执行

解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用(Symbolic References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

初始化

到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序自定的主观计划去初始化类变量和其他资源。换一个角度来表达就是:初始化阶段是执行类构造器()方法的过程。以下介绍下()方法执行过程中一些可能会影响程序运行行为的特点和细节:

  • ()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。一个例子说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{

static int i =1;

static{
Systemt.out.print(i);//正常使用

j = 0;//给变量赋值可以正常编译
Systemt.out.print(j);//这句提示“非法向前引用”
}

static int j =1;
}
  • ()方法与类的构造函数(实例构造器())不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的 () 执行之前,父类的()已经执行完毕。因此,在虚拟机中第一个被执行()方法的类肯定是 java.lang.Object。这其实也意味着,父类定义的静态语句块要优先于子类变量赋值操作。一个例子说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class Parent{
public static A = 1;

static{
A = 2;
}
}

static class Sub extentds Parent{
public static int B = A;
}

public static void main(String args[]){
Systemt.out.println(Sub.B)
}

以上代码将打印出 2 而不是 1。

  • ()方法对于类或者接口不是必需的,如果类中没有静态语句块,也没有对变量的赋值,那么编译器可以不为这个类生成()方法。

  • 接口中不能使用静态语句块,但是可以有变量初始化的赋值操作,因此接口与类一样都会生成 ()方法 。但接口的 ()方法 无须先执行父接口的()方法,只有父接口中定义的变量使用时,父接口才会初始化(接口的实现类在初始化时也不会执行接口的()方法)。

  • 虚拟机会保证一个类的 ()方法 在多线程环境中会被正确地加锁、同步,所以,多线程同时去初始化一个类,那么只会有一个县城执行这个类的<clinit>()方法,其他的线程都会阻塞等待。举个例子:

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
static class DeadLoopClass{
static{
//如果不加上这个if语句,编译器将报错,并拒绝编译
if(true){
System.out.println(Thread.currentThread() + "init DeadLoopClass");

while(true){
}
}
}


public static void main(String[] args){
Runnable script = new Runnable(){
public void run(){
Systemt.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
Systemt.out.println(Thread.currentThread() + "run over");
}

};

Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}

上述代码在死循环以模拟长时间操作,另外一条线程在阻塞等待,将打印以下结果:

Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。

类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗地说,比较两个类是否“相等”,只有在这两个类都是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,都被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里的“相等”,包括代表类的Class对象的 equals() 、isAssignableFrom()、isInstance() 方法返回的结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。以下代码演示了不同类加载器对instanceof关键字运算的结果的影响:

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
public class ClassLoaderTest{
public static void main(String[] args) throws Exception{
ClassLoader myLoader = new ClassLoader(){
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
try{
String fileName = name.substring(name.lastIndexOf(".") + 1);
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
}catch(){
throw new ClassNotFoundException();
}
}
}

Object obj = myLoader.loadClass("org.example.classloading. ClassLoaderTest");

Systemt.out.println(obj.getClass());
System.out.println(obj instanceof org.example.classloading. ClassLoaderTest);
}
}

返回结果:

class org.example.classloading. ClassLoaderTest
false

从结果可以看出,对象确实是类 “org.example.classloading. ClassLoaderTest” 的类,并实例化了这个类的对象。但这个对象与类 org.example.classloading. ClassLoaderTest 做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个 ClassLoaderTest 类,一个由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类。

双亲委派模型

从java虚拟机角度来讲,只存在两种类加载器:一种是启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader。

如下图所示的类加载器之间的这种层次关系,称为双亲委派模型(Parent Delegation Model)。

类的生命周期

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派模型一个显而易见的好处就是:Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如类 java.lang.Object ,无论哪一个类加载器都要加载这个类(因为所有的类都直接或间接地继承了它),最终都是委派给处于模型最顶端的启动类加载器进行加载,因此,Object 类在程序中的各种类加载器环境中都是同一个类。相反,如果不使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在 ClassPath 中,那系统中将将会出现多个不同的Object 类,Java类型体系中最基础的行为也就无法保证。

破坏双亲委派模型

概述

计算机只认识0和1,但是最近10年虚拟机以及大量建立在虚拟上的语言的发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

无关性的基石

Sun公司及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现程序的“一次编写,到处运行”。而各种不同平台的虚拟机以及平台无关的字节码(ByteCode)是构成平台无关性的基石。

到目前为止,或许大部分程序员都还认为Java虚拟机执行Java程序是一件理所当然的事情,但在Java发展之初,Java的规范就分为Java语言规范以及Java虚拟机规范,时至今日,Java语言之外已经有一大批语言运行在Java虚拟机之上,如 Groovy、JRuby、Jython等。

Java 虚拟机不和包括Java在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联,任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。

Class 类文件的结构

Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。遇到需要占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。

Class 文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种结构只有两种数据类型: 无符号数和表。

无符号数属于基本的数据类型,以u1、u2、u4、u8分别代表1、2、4、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8 编码构成的字符串值。

表示由多个无符号数或者其他表作为数据项的复合数据类型,表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。

魔数与Class文件的版本

Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class 文件的魔数值为 0xCAFEBABE (咖啡宝贝?),很有浪漫气息。紧接着魔数的4个字节存储着Class文件的版本号,高版本的JDK能乡下兼容以前版本的Class文件,但不能运行以后版本的Class文件。

很多文件存储标准中都使用魔数来进行身份识别,如gif和jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来识别主要是基于安全考虑: 文件扩展可以随意改动。

魔数示意图

常量池

版本号之后是常量池入口,该区域可以理解为Class文件中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。

常量池中主要存放两大类常量:字面量(Literal) 和 符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如 文本字符串、声明为final的常量值等,而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Java代码在javac编译的时候,并不像c和c++那样有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接,也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息。虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时解析、翻译到具体的内存地址中。

访问标志

常量池结束后,紧接着2个字节代表访问标志(access_flags
),这个标志用于识别一些类或者接口层次的访问信息,包括: 这个Class 是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等。

类索引、父类索引与接口索引集合

类索引(this_class) 和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,class 文件中由这三项数据来确定这个类的继承关系。

其中,类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此,父类索引都不为0。接口索引集合用来描述这个类实现了哪些接口,按照implement语句后的接口顺序从左到右排列在接口索引集合中。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量,包括类级变量和实例级变量,不包括方法内的局部变量。这个描述可以包括的信息有: 字段的作用域(public、private、protected修饰符)、实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符) 等等。

方法表集合

Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,只是些微的区别,比如volatile关键字和transient关键字不能修饰方法 。

属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息(这一块内容也没耐心看下去,下次看明白再补)。

字节码指令简介

这章离App开发比较远,因此第一遍的时候先略过。

公有设计和私有实现

理解公有设计与私有实现之间的分界线是非常有必要的,Java虚拟机实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的语义。按照Java虚拟机规范一成不变地逐字实现其中要求的内容是一种可行的途径,但是一个优秀的虚拟机实现,在满足虚拟机规范的约束下对具体实现做出修改和优化也是完全可行的。虚拟机实现的方式主要有以下两种:

  • 将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。
  • 将输入的Java虚拟机代码在加载或执行翻译成宿主机CPU的本地指令集(即 JIT 代码生成技术)

Class 文件结构的发展

略。

坐标系

Android 系统中有两种坐标系:Android 坐标系和 View 坐标系,了解这两种坐标系能够帮助我们实现View的各种操作。

Android坐标系

Android坐标系中, 将屏幕左上角的顶点作为原点, 这个原点向右是X轴正方向, 向下是Y轴正方向, 如下图所示。

Android坐标系

View坐标系

View坐标系与Android坐标系并不冲突,两者是共同存在的,一起来帮助开发者更好地控制View。对于View坐标系,搞明白下图的信息即可:

View坐标系

MotionEvent提供的方法:假设上图中间的那个圆点就是我们触摸点,无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理。MotionEvent提供了获取焦点坐标的各种方法:

  • getX():获取点击事件距离控件左边的距离,即视图坐标。
  • getY():获取点击事件距离控件顶边的距离,即视图坐标。
  • getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标。
  • getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

View的滑动

View的滑动基本思想:当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。 实现View滑动有很多种方法, 在这里主要讲解6种滑动方法, 分别是layout()、offsetLeftAndRight() 与 offsetTopAndBottom()、LayoutParams、动画、scollTo 与 scollBy ,以及Scroller。

layout方法

View进行绘制的时候会调用onLayout()方法来设置显示的位置, 因此我们同样也可以通过修改View的left、 top、 right、 bottom属性来控制View的坐标。以下是实现一个随手指滑动的自定义view的步骤:

  1. 首先获取触摸点的坐标
1
2
3
4
5
6
7
8
9
10
11
12
public boolean onTouchEvent(MotionEvent event){
//获取手指触摸点的横坐标和纵坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
}
}
  1. 在ACTION_MOVE事件中计算偏移量,再调用layout( ) 方法重新放置这个自定义View的位置即可:
1
2
3
4
5
6
7
8
9
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//调用layout方法来重新确定它的位置
layout(getLeft() + offsetX,getTop()+offsetY,
getRight()+offsetX,getBottom()+offsetY)

break

在每次移动时都会触发layout()方法对屏幕重新布局,从而达到移动View的效果。

offsetLeftAndRight() 与offsetTopAndBottom()

这两种方法和layout()方法的效果以及使用方式都差不多,只需要将上面ACTION_MOVE中的代码替换为以下代码即可:

1
2
3
4
5
6
7
8
9
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//对left 及 right 进行偏移
offsetLeftAndRight(offsetX);
//对top及bottom进行偏移
offsetTopAndBottom(offsetY);
break;

LayoutParams( 改变布局参数)

LayoutParams主要保存了View的布局参数, 因此可以通过改变它来达到改变View位置的效果。 我们只需将 ACTION_MOVE 中的代码替换成如下代码即可(注意是:MarginLayoutParams):

1
2
3
4
5
6
7
8
9
10
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();

params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
setLayoutParams(params)
break;

动画

采用动画来移动,在res目录新建anim文件夹并创建如下translate.xml文件:

1
2
3
4
5
6
7
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<translate
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="300"/>
</set>

在Java中调用:

view.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate))

需要注意的是,如果动画文件中没有添加 android:fillAfter=”true” ,则方块向右平移300像素后,又返回原来的位置。并且,View动画不能改变View的位置参数,如果对一个Button加上如上的平移动画,当Button平移300像素停留在当前位置时,我们点击这个Button并不会触发点击事件,但是点击原始位置却触发了点击事件,这是因为对于系统来说,Button并没有改变原来位置。

在Android 3.0出现的属性动画解决了上述问题,它不仅可以执行动画,还能改变View的位置参数,其操作如下:

ObjectAnimator.ofFloat(view,”translationX”,0,300).setDuration(1000).start()

scrollTo 与 scrollBy

scollTo、scollBy移动的是View的内容,如果在ViewGroup中使用, 则是移动其所有的子View。scrollTo(x,y)表示移动到一个具体的坐标点,而scrollBy(dx,dy)则表示移动的增量为dx、dy。 其中, scollBy最终也是要调用scollTo的。二者的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void scrollTo(int x,int y){
if(mScrollX != x || mScrollY != y){
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;

invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if(!awakenScrollBars()){
postInvalidateOnAnimation();
}
}
}

public void scrollBy(int x,int y){
scrollTo(mScrollX + x, mScrollY + y)
}

如果要实现上面view随手指滑动的效果,就需要将ACTION_MOVE中的代码替换成如下代码:

((View)getParent()).scrollBy(-offsetX,-offsetY);

请注意,这里是对view的parent进行scroll,这是因为如果对view本身scroll的话,就是对自己的内容进行移动,而不是整个view。并且注意,这里设置的偏移量值都为负值,以下具体讲解一下。

假设我们正用放大镜来看报纸,放大镜用来显示字的内容。同样我们可以把放大镜看作我们的手机屏幕,它们都是负责显示内容的;而报纸则可以被看作屏幕下的画布,它们都是用来提供内容的。放大镜外的内容,也就是报纸的内容不会随着放大镜的移动而消失,它一直存在。同样,我们的手机屏幕看不到的视图并不代表其不存在。过程的示意图如下:

scrollBy之前:

scrollBy初始状态

调用scrollBy(50,50)之后:

scrollBy之后

Scroller

我们在用scollTo/scollBy方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller本身是不能实现View的滑动的,它需要与View的computeScroll() 方法配合才能实现弹性滑动的效果。具体代码如下示意:

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
public CustomView(Context context,AttributeSet attrs){
private Scroller mScroller;

super(context,attrs);
//初始化mScroller
mScroller = new Scroller(conetxt);

@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrentX(),mScroller.getCurrentY());
invalidate();
}

}


//提供调用的方法
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int scrollY = getScrollY();

int deltaX = destX - scrollX;
int deltaY = destY - scrollY;
mScroller.startScroll(scrollX, scrollY, deltaX, deltaY);
}
}

我们首先初始化Scroller,之后重写computeScroll方法,系统会在绘制View的时候在 draw 方法中调用该方法。在computeScroll方法中, 我们调用父类的scrollTo() 方法并通过Scroller来不断获取当前的滚动值, 每滑动一小段距离我们就调用invalidate() 方法不断地进行重绘,重绘就会调用computeScroll()方法, 这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。这里我们设定CustomView沿着X轴向右平移400像素(至于为什么是负数,上面已经解释过了):

mCustomView.smoothScrollTo(-400,0);

属性动画

在属性动画出现之前,Android系统提供的动画只有帧动画和 View 动画。View 动画我们都了解,它提供了AlphaAnimation、 RotateAnimation、 TranslateAnimation、 ScaleAnimation这4种动画方式,并提供了AnimationSet动画集合来混合使用多种动画。 随着Android 3.0属性动画的推出, View动画不再风光。 相比属性动画, View动画一个非常大的缺陷突显, 其不具有交互性。 当某个元素发生View动画后,其响应事件的位置依然在动画进行前的地方, 所以View动画只能做普通的动画效果, 要避免涉及交互操作。 但是它的优点也非常明显: 效率比较高, 使用也方便。

在属性动画中使用最多的就是AnimatorSet和ObjectAnimator配合: 使用 ObjectAnimator 进行更精细化的控制, 控制一个对象和一个属性值, 而使用多个ObjectAnimator组合到AnimatorSet形成一个动画。 属性动画通
过调用属性get、 set方法来真实地控制一个View的属性值, 因此, 强大的属性动画框架基本可以实现所有的动画效果。

ObjectAnimator

ObjectAnimator 是属性动画最重要的类, 创建一个 ObjectAnimator 只需通过其静态工厂类直接返还一个ObjectAnimator对象。 参数包括一个对象和对象的属性名字, 但这个属性必须有get和set方法, 其内部会通
过Java反射机制来调用set方法修改对象的属性值。 一般使用方式如下:

ObjectAnimator.ofFloat(view,”translationX”,200,0).start()

ObjectAnimator的使用方法就不介绍了,需要注意的是, 在使用ObjectAnimator的时候, 要操作的属性必须要有get和set方法, 不然ObjectAnimator 就无法生效。 如果一个属性没有get、 set方法, 也可以通过自定义一个属性类或包装类来间接地给这个属性增加get和set方法。 如以下示例这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static class MyView{
private View mTarget;
private MyView(View target){
this.mTarget = target;
}

public int getWidth(){
return mTarget.getLayoutParams().widht
}

public void setWidth(int width){
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}

使用时只需要操作包类就可以调用get、 set方法了:

1
2
MyView mMyView = new MyView(mButton);
ObjectAnimator.ofInt(mMyView,"width,500).setDuration(500).start()

ValueAnimator

ValueAnimator不提供任何动画效果, 它更像一个数值发生器, 用来产生有一定规律的数字, 从而让调用者控制动画的实现过程。 通常情况下, 在ValueAnimator的AnimatorUpdateListener中监听数值的变化, 从而完成动画的变换。

AnimatorSet

在XML中使用属性动画

1
2
3
4
5
6
7
8
<objectAnimator 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duration="3000"
android:propertyName="scaleX"
android:valueFrom="1.0"
android:valueTo="2.0"
android:valueType="floatType"/>

在代码中引用xml属性动画也很简单:

1
2
3
Animator animator = AnimatorInflater.loadAnimator(this,R.anim.scale);
animator.setTarget(view);
animator.start();

解析Scroller

略,去看源码,看不懂再来添加这块内容

View 事件分发机制

这里了解Activity的构成就好了,如下图:

Activity构成

事件分发机制则看之前写的文章还容易理解一些,这里就略过了。

View的工作流程

这一章太长,作为第二部分内容。

常用的git命令(摘抄自官网)

列显已有的标签

列出现有标签的命令非常简单,直接运行 git tag 即可:

$ git tag
v0.1
v1.3

我们可以用特定的搜索模式列出符合条件的标签。在 Git 自身项目仓库中,有着超过 240 个标签,如果你只对 1.4.2 系列的版本感兴趣,可以运行下面的命令:

$ git tag -l ‘v1.4.2.*’
v1.4.2.1
v1.4.2.2
v1.4.2.3
v1.4.2.4

新建标签

含附注的标签

创建一个含附注类型的标签非常简单,用 -a (译注:取 annotated 的首字母)指定标签名字即可:

$ git tag -a v1.4 -m ‘my version 1.4’

轻量级标签

轻量级标签实际上就是一个保存着对应提交对象的校验和信息的文件。要创建这样的标签,一个 -a,-s 或 -m 选项都不用,直接给出标签名字即可:

$ git tag v1.4-lw

分享标签

默认情况下,git push 并不会把标签传送到远端服务器上,只有通过显式命令才能分享标签到远端仓库。其命令格式如同推送分支,运行 git push origin [tagname] 即可:

$ git push origin v1.5

如果要一次推送所有本地新增的标签上去,可以使用 –tags 选项:

$ git push origin –tags

删除分支

删除本地分支

删除本地分支可以使用一下命令:

$ git branch -d testing

如果删除失败,需要强制删除可以使用 -D 选项强制删除它:

$ git branch -D testing

删除远程分支

可以运行带有 –delete 选项的 git push 命令来删除一个远程分支。 如果想要从服务器上删除 serverfix 分支,运行下面的命令:

$ git push origin –delete serverfix
To https://github.com/schacon/simplegit

  • [deleted] serverfix

merge 某个分支的改动,但是merge过来都是作为未 commit 状态的文件

git merge master_glassx –squash

目前我主要使用该方法操作不能提交的分支:

  1. 当前 A 分支commit 了很多次,但是在push 的时候,提示 commit 的卡片已经关闭了,不能push,必须使用新卡片才能push
  2. 基于 A 中最后一次已经 push 的 commit 新建一个分支 B
  3. 之后,在 B 分支下,执行 git merge A –squash ,将所有改动 merge 到 B 中来
  4. 在 B 中,commit 操作,并将新的卡片信息填入这次commit,之后就能顺利push

View 基础知识

1、 MotionEvent 和 TouchSlop

MotionEvent

这里只需要注意一点,通过MotionEvent 可以获得点击事件发生的x和y坐标,系统提供了两组方法,getX/getY 和 getRawX/getRawY ,他们的区别很简单,前者返回的是相对于当前View的左上角的 x 和 y 坐标,后者返回的是相对于手机屏幕左上角的 x和y 坐标。

TouchSlop

TouchSlop 是系统所能识别出的被认为是滑动的最小距离,小于这个值就不认为是滑动操作。TouchSlop 是一个常量,和设备有关,在不同的设备上这个值可能是不同的。通过如下方式可以获取这个常量:

ViewConfiguration.get(context).getScaledTouchSlop()

2、VelocityTracker、GestureDetector 和 Scroller

VelocityTracker

用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。它的使用过程很简单,首先在 View 的 onTouchEvent 方法中追踪当前单击事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

接着,当我们想知道当前的滑动速度时,可以采用如下方法获得:

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();

这里需要注意的是: 一、获取速度之前必须先计算速度;二、这里的速度是指一段时间内手指所划过的像素数,比如将时间间隔设置为 1000ms,在 1s 内水平从左至右滑过100像素,那么水平速度就是100 。如果将时间间隔设置为100ms,在这100ms 内滑过10像素,则水平速度会变成 10 。此外,这个速度是可以为负值的,当水平方向从右往左滑动时,水平方向的速度即为负值。

GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。比如要监听双击行为,代码如下:

GestureDetector mGestureDetector = new GestureDetector(listener);
//再加上这行可以解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);

接着,接管目标View的onTouchEvent方法,在待监听View的 onTouchEvent 方法中如下实现:

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

OnGestureListener 与 OnDoubleTapListener 里面的方法有很多,常用的有:onSingleTapUp(单击)、onScroll(拖动)、onLongPress(长按)、onFling(快速滑动)、onScroll(拖动)、onDoubleTap(双击)。

Scroller

弹性滑动对象,用于实现View的弹性滑动,我们知道,当使用View的scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成的,这时候就可以使用 Scroller 来实现有过渡效果的滑动,它需要和View的conputeScroll方法配合使用才能共同完成这个功能,典型的使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Scroller mScroller = newScroller(context);

//缓慢滑动到指定位置
privte void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms内滑向destX就是,效果就是慢慢滑动
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}

@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate()
}
}

View 的滑动

未完待续。。。

提到IPC的使用场景就必须提到多进程,如果只有一个进程,又何必使用进程间通信。多进程的情况分为两种:第一种是应用本身需要采用多进程模式实现(比如通过多个进程来获取多份内存空间);第二种是当前应用需要向其他应用获取数据,由于是两个应用,因此必须采用跨进程通信方式。

Android中的多进程模式

我们不讨论两个应用之间的多进程情况。

1、开启多进程

,Android中使用多进程有两种方法:第一种是给四大组件在AndroidManifest中指定 android:process 属性;第二种是非常规方式,通过JNI在native层去fork一个新的进程。第二种情况属于特殊情况,暂时不考虑。

2、多线程模式的运行机制

如果用一句话形容多进程,那就是:“当应用开启了多进程以后,各种奇怪的现象都出现了”,开启多线程只需要给四大组件指定 android:process 属性,但是是否正常运行就是另外一回事了。看个例子:

有 MainActivity 和 SecondActivity,其中 SecondActivity 指定运行在一个新的进程中,并且项目还新建一个 UserManager 类,类中有个public 的静态变量:

1
2
3
public class UserManager{
public static int sUserId = 1;
}

在 MainActivity 的 onCreate 中把 sUserId 的值改为2,打印sUserId,之后再启动 SecondActivity ,在 SecondActivity 中打印 sUserId 。

可以发现在 MainActivity 中打印的值是2,在 SecondActivity 中打印的值是 1 ,看到这里,大家应该明白了多进程带来的问题,绝非只是指定一个 android:process 这么简单

分析:我们知道,Android 系统为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。拿上面的例子来说,两个进程中都存在一个 UserManager 类,并且这两个类是互相不干扰的,在一个进程中修改 sUserId 的值只会影响当前进程,对其他进程不会造成任何影响。

通过以上可以知道,运行在不同进程中的四大组件,只要他们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。一般来说,使用多进程会造成如下几个方面的问题:

  • 静态成员和单例模式完全失效

上面做了分析

  • 线程同步机制完全失效

既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象

  • SharedPreference 的可靠性下降

是因为 SharedPreference 不支持两个进程同时去执行写操作,否则会导致可能的数据丢失(因其本质是通过读写xml文件来实现的)

  • Application 会多次创建

这个问题是显而易见的,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,因此相当于系统又把应用重新启动了一遍,自然就创建了新的Application。还可以这么理解,运行在同一个进程中的组件是属于同一个虚拟机和同一个Application的;同理,运行在不同进程中的组件是属于两个不同的虚拟机和Application。

IPC 基础概念介绍

IPC 中的基础概念包括3方面内容: Serializable 接口、Parcelable接口、Binder。

1、Serializable 接口

Serializable 是Java提供的一个空的序列化接口,为对象提标准的序列化和反序列化操作。使用 Serializable 实现序列化非常简单,只需要类实现 Serializable 接口,并且在类的声明中指定一个类似下面的标识:

private static final long serialVersionUID = 12345L

实际上,这个 serialVersionUID 也不是必需的,因为serialVersionUID 的机制是这样的: 序列化时,系统会把当前类的 serialVersionUID 写入序列化的文件中;当反序列化的时候,会去检测文件中的 serialVersionUID 是否和当前类的 serialVersionUID 一致,如果一致说明序列化的类版本和当前类的版本是相同的,就可以成功反序列化;否则的话,说明当前类和序列化的类相比发生了某些变换,就无法正常反序列化。以下例子说明 Serializable 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//序列化的类
public class User implements Serializable{
private static final long serialVersionUID = 12345L;
public int id;
public String userName;
}


/**********使用*****************/

//序列化
User user = new User(12,"tom");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();

//反序列化过程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();

另外,系统默认的序列化过程也是可以改变的,通过重写 writeObject 和 readObject 方法即可,只不过大部分情况下我们无需去重写这两个方法。

2、Parcelable 接口

Parcelable 也是一个接口,只要实现这个接口,类的对象就可以实现序列化并通过 Intent 和 Binder 传递。

具体使用方法可以查看官方文档

既然 Parcelable 和 Serializable 都能实现序列化并且都可用于 Intent 间的数据传递,那如何取舍呢?Serializable 是Java中的序列化接口,序列化和反序列化需要大量I/O操作;而Parcelable 是 Android 中的序列化方式,主要用在内存序列化上,使用起来稍显麻烦,但是效率高,所以这是 Android 官方推荐的序列化方式。综上所述,将对象序列化存储到设备或者通过网络传输时使用 Serializable ,否则使用 Parcelable 。

3、Binder

Binder 是 Android 中的一种 IPC 方式,还可以理解为一种虚拟的物理设备,它的设备驱动是 dev/binder。

Android 中的 IPC 方式

使用Bundle

由于Bundle 实现了 Parcelable 接口,所以它可以方便地再不同的进程间传输,基于这一点,当我们在一个进程中启动了另一个进程的 Activity、Service 和 Receiver,我们就可以在Bundle 中附加我们需要传给其他进程的信息,并通过Intent 发送出去,这是一种最简单的进程间通信方式。

使用文件共享

共享文件是一种不错的进程间通讯方式,适合在对数据同步要求不高的进程间通信。当然,SharedPreferences 是个特例,由于系统对它的读写会有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,面对高并发的的读写会有很大几率丢失数据,因此不建议在进程间通信中使用 SharedPreferences。

使用 Messenger

顾名思义可以翻译成信使,通过它可以在不同的进程中传递 Message 对象,它是轻量级的 IPC 方案,底层实现是 AIDL 。Messenger 只是一串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理。具体可以参考官方文档

使用 AIDL

由于 Messenger 服务端只能串行处理,所以可以使用 AIDL 来实现跨进程调用。具体内容可以参考官方文档

使用 ContentProvider

这是Android 中提供的专门用于不同应用建进行数据共享的方式。

使用 Socket

Socket 是网络通信中的概念,也称为“套接字”,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议

选择合适的 IPC 方式

名称 优点 缺点 适用场景
Bundle 简单易用 只能传输Bundle支持的数据类型 四大组件之间的通信
文件共享 简单易用 不适合高并发 无并发,数据实时性要求不高
AIDL 功能强大,支持一对多,支持实时 使用复杂 一对多通信且有RPC需求
Messenger 一对多串行通信,支持实时 高并发困难,不支持RPC,只能传输Bundle支持的数据 低并发的一对多通信
ContentProvider 数据源访问功能强大 理解为受约束的AIDL,主要提供数据源的 CRUD 操作 一对多的进程间数据共享
Socket 支持一对多并发实时通信,支持字节流 实现繁琐 网络数据交换

Activity 的生命周期全面分析

本节将分为两部分内容,一部分是典型情况下的生命周期,一部分是异常情况下的生命周期(比如被系统回收或者由于当前设备Configuration 发生改变而导致 Activity 被销毁重建)。

一、典型情况下的生命周期分析

正常情况下,Activity 会经历如下生命周期:

  1. onCreate : 表示Activity 正在创建,这是生命周期第一个方法。
  2. onRestart : 表示Activity 正在重新启动。一般情况下,当前Activity 从不可见重新变为可见时,onRestart 就会被调用。
  3. onStart : 表示Activity 正在被启动,这时候 Activity 已经可见了,但是还没出现在前台,无法和用户交互。可以理解为 Activity 已经显示出来了,但是我们还看不到
  4. onResume : 表示 Activity 已经可见了,并且出现在前台可以交互。
  5. onPause: 表示 Activity 正在停止,正常情况下,紧接着 onStop 会被调用;在特殊情况下,如果这个时候快速地再回到当前 Activity ,那么 onResume 将会被调用。此时可以做一些存储数据、停止动画等工作,但是注意不能太耗时,因为onPause必须先执行完,新Acitivty 的 onResume 才会执行
  6. onStop : 表示 Activity 即将停止,可以做一些稍微重量级的回收工作,同样不能太耗时。
  7. onDestroy: 表示 Activity 即将被销毁,这是Activity 生命周期中的最后一个回调,我们可以做一些回收工作和最终的资源释放。

以下再针对 Activity 的生命周期具体说明:

  • 针对特定的Activity,第一次启动回调如下: onCreate->onStart->onResume
  • 打开新的Activity 或者(按Home键)回到桌面的时候,回调 onPause->onStop ;这里有种特殊情况,打开的新Activity 如果是透明主题(意味着当前Activity还是可见的),那么当前Activity 不会回调 onStop 。
  • 当用于再次回到原来的 Activity 时,回调 onRestart->onStart->onResume
  • 当用户按返回键返回上一个页面时,回调 onPause->onStop->onDestroy
  • 从整个生命周期来看,onCreate与onDestroy是配对的,分别标识着Activity的创建与销毁;onStart与onStop是配对的,标识着Activity是否可见;onResume 与 onPause 是配对的,标识着Activity是否在前台

一个问题:当前 Activity 标识为 A,启动一个新的Activity 标识为 B,那么B的onResume 和 A 的onPause 哪个先执行?

由上面的描述可知是限制性A的 onPause,再执行 B 的onResume ,具体看源码,官方文档也是这么解释(Always followed by onPause())。

二、异常情况下的生命周期分析

1、资源相关的系统配置发生改变导致Activity被杀死并重建

如果没有做特殊处理,当横竖屏切换的时候,由于系统配置发生了改变,Activity 会加载不同的资源(比如横竖屏加载两张不同图片),此时 Activity 会被销毁并且重新创建。由于 Acitivity 是在异常情况下终止的,因此在销毁Activity的时候,确切来说是在onStop之前(但是跟onPause没有顺序关系,有可能在其之前,也可能在其之后)会调用 onSaveInstanceState 来保存当前 Activity 状态

Activity 被重新创建后,会把销毁时 onSaveInstanceState 方法保存的Bundle对象作为参数传给 onCreate 方法和 onRestoreInstanceState 方法,因此可以从这两个方法恢复之前保存的数据。从时序上来说,onRestoreInstanceState 调用时机在 onStart 之后。

这两个方法恢复数据的区别是:onRestoreInstanceState 一旦被调用,其参数 savedInstanceState 是一定有值的,我们不需要额外地判空;而 onCreate 中的数据是可能为空的,官方文档建议采用 onRestoreInstanceState 去恢复数据。

如果没有覆写的话,onSaveInstanceState 和 onRestoreInstanceState 方法中,系统会自动为我们做一定的恢复工作。

2、资源内存不足导致低优先级Activity被杀死

Activity 按照优先级从高到低可以分为如下三种:

(1)前台Activity——正在和用户交互的Activity,优先级最高
(2)可见但非前台Activity——比如Activity中弹出了dialog,导致Activity可见但是位于后台无法和用户直接交互。
(3)后台Activity——已经被暂停的的Activity,优先级最低。

我们知道,当系统配置发生改变时,Activity 会被销毁并重新创建,当然我们也可以通过给 Activity 指定configChanges 属性来阻止销毁重建:

android:configChanges=”orientation”

当然这个属性可以配置的项目还有很多,比如切换系统语言、使用了新字号、界面模式改变(比如 是否开启/关闭夜间模式)。

Activity 的启动模式

  • standard: 标准模式。不复用,每次请求都创建新实例,并且就运行在启动它的那个Activity所在的栈。
  • singleTop: 栈顶复用模式 。如果实例位于任务栈的栈顶就复用,复用时调用 onNewIntent方法,否则就创建新的实例。
  • singleTask: 栈内复用模式。当前栈内没有实例,则创建实例放入栈中;如果实例在当前栈内,则复用,复用时调用 onNewIntent方法,并把它之上的Activity出栈。
  • singleInstance: 单例模式。只能单独位于一个任务栈中,只要这个实例存在,后续的请求均不会创建新的Activity。复用时调用 onNewIntent方法。

在使用 ApplicationContext 启动standard 模式的Activity时会报错:

Callking startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag

这是因为standard模式的Activity 默认会进入启动它的Activity所属的任务栈中,由于非Activity类型的Context 没有所谓的任务栈,所以会报错。解决这个问题的方法是为待启动的Activity指定 FLAG_ACTIVITY_NEW_TASK 标记,这样启动的时候会为它创建一个新的任务栈(体会下,这时候实际上是以singleTask模式启动的)

参考其他博客的启动模式的原理

点击launcher 的startactivity 会调用到Instrumentation的execStartActivity,之后交给AMS 来处理启动操作,之后调用 ActivityStackSupervisor 的 startActivityMayWait 方来启动,而ActivityStackSupervisor 是管理Activity堆栈的类

另外提一下,Activity 在AMS中的形式是 ActivityRecord,task在AMS 中的形式是 TaskRecord,进程在 AMS 中的管理形式为 ProcessRecord

判断启动模式,根据当前的Activity 和 要启动的Activity 的启动模式,根据相应的启动模式设置launchFlags

在Android 5.0上 在 ActivityStackSupervisor 类的startActivityUncheckedLocked 里面判断launchmode

以上内容参考自简书的博客上的内容

Activity 的Flags

Activity 的Flags很多,这里分析比较常用的几个:

  • FLAG_ ACTIVITY_ NEW_ TASK

为Activity 指定singleTask启动模式,其效果和在XML中指定 singleTask 启动模式相同

  • FLAG_ ACTIVITY_ SINGLE_ TOP

为Activity 指定 singleTop 启动模式,其效果和在XML中指定 singleTop 启动模式相同

  • FLAG_ ACTIVITY_ CLEAR_ TOP

具有此标记的Activity,当它启动时,在同一个任务栈中所有位于它上面的Activity都要出栈,这个标记为一般会和FLAG_ACTIVITY_SINGLE_TOP标记位一起出现。由前面的分析可知,singleTask启动模式默认具有此标记位效果。

  • FLAG_ ACTIVITY_ EXCLUDE_ FROM_ RECENTS

具有这个标记位的Activity不会出现在历史Activity列表中,当某些情况下我们不希望用户通过历史列表回到我们的Activity的时候,这个标记比较有用。这个标记等同于xml中指定Activity 的属性 android:excludeFromRecents = “true”

IntentFilter 的匹配规则