Neil's blog

Let's start from here


  • 首页

  • 归档

  • 标签

  • 关于

Android Gradle构建入门 (一)

发表于 2017-12-30 |

Android Gradle入门

本文内容来自官方文档整理

一、查看生成的Gradle文件列表

默认情况下,Android Studio将以“Android”模式下的“项目视图”开始,如下图所示:

“asmodel”

Android项目是Gradle多项目构建,具有顶级build.gradle文件和名为app的子目录,并具有自己的build.gradle文件。 顶层构建文件在图中标记为(Project:HelloWorldGradle),并且应用程序构建文件(Module:app)附加到其中。

在项目中,可能有两个名为gradle.properties的文件。 一个是本地项目。 另一个只有在主目录的.gradle子目录中具有全局gradle.properties文件的同名文件才存在。

  • settings.gradle

Gradle使用文件settings.gradle来配置多项目构建。 它应该由一行代码组成:

include ':app'

这就是告诉Gradle,app也是一个Gradle project,当项目中还会依赖别的library,那么还需要在后面添加相应的信息。

  • gradle-wrapper.properties

gradle-wrapper.properties,它配置了所谓的Gradle Wrapper。 这使您可以构建Android项目,而无需首先安装Gradle。 该文件的内容应该类似于:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

前四行表示当包装首次运行时,它将下载Gradle发行版并将其存储在您的主目录中的.gradle / wrapper / dists目录中。

最后一行显示了distributionUrl的值,这是Gradle将下载指定分配的地址。(具体版本号可能与此处(4.1)中显示的版本号不同,并且URL可能引用二进制版本(-bin),而不是此示例中显示的完整版本(-all)。)

二、查看整个项目的build.gradle

project的build.geadle文件:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'


        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

各个代码块具体作用:

  1. buildscript - 下载插件
  2. dependencies - 标识Android插件
  3. allproject - top-level and module projects的配置
  4. task clean - 特设任务

Gradle定义了一种基于Groovy的DSL语言来构建,buildscript标签是DSL的一部分。 它告诉Gradle构建需要一个可能不是基线Gradle分布的一部分的插件,并告诉Gradle在哪里找到它。 在这种情况下,使用坐标语法“group:name:version”指定所需的插件,其中组为com.android.tools.build,名称为gradle,版本为3.0.1。

当Gradle第一次构建这个项目时,插件将被下载并缓存,所以这个任务只执行一次。

allprojects标签保存适用于顶层项目及其包含的任何子项目的配置细节。 在这种情况下,该块指定任何所需的依赖项应该从https://jcenter.bintray.com上的公共Bintray Artifactory存储库的google或jcenter下载。

最后,构建文件包含一个名为clean的自定义(或临时)任务。 它使用内置的任务类型Delete并对其进行配置,以便干净的任务将删除rootProject中的buildDir。 两者都是项目属性,其值默认为该应用程序驻留的项目中的构建目录。

三、查看app module的build.gradle

看一下 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.liushihan.gradletest"
        minSdkVersion 21
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

第一行:

apply plugin: 'com.android.application'

应用Android插件(在项目构建文件的构建脚本部分中引用)到当前项目。Gradle中的插件可以将自定义任务,新配置,依赖关系和其他功能添加到Gradle项目中。 在这种情况下,应用Android插件会添加各种各样的任务,这些任务由接下来显示的android块配置。

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.liushihan.gradletest"
        minSdkVersion 21
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

对这些属性进行简单的介绍:

  • compileSdkVersion:Android SDK版本
  • defaultConfig:defaultConfig部分包含应用程序的所有变体(构建类型和产品风格的组合)共享的属性。
    • applicationId:applicationId基于创建应用程序时指定的域名和项目名称,并且在Google Play商店中必须是唯一的。
* minSdkVersion:minSdkVersion是你愿意支持这个应用程序的最低Android API,targetSdkVersion应该是最新的Android版本。


* versionCode:versionCode的值应该是在将新版本的应用上传到Google Play商店之前递增的整数。 这个值和applicationId一起告诉Google,这是一个现有应用程序的新版本,而不是一个新的应用程序。

* versionName:版本名称值用于自己的内部版本跟踪。

* testInstrumentationRunner:testInstrumentationRunner属性配置为使用为Android应用程序配置的JUnit 4测试运行器。

在这个代码块下面,就是buildTypes.默认情况下,Android应用程序支持两种构建类型,debug和release.这里没有显示debug配置部分,即使用所有调试的默认设置。

buildTypes {
      release {
          minifyEnabled false
          proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
      }
  }

继续往下面看,就是这个应用程序所依赖的库

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

配置依赖关系是构建Gradle应用程序的基础部分。 在这种情况下,依赖项部分显示了Implementation,testImplementation和androidTestImplementation配置的值。

testImplementation依赖只包含最新的稳定的JUnit 4发行版。 JUnit类和测试注释将在编译时在src / test / java层次结构中可用。

androidTestImplementation依赖项是指Espresso测试库,用于Android应用程序的集成测试。 在这种情况下,Espresso在没有通常包含的support-annotations库的情况下被请求,因为已经通过其他依赖项包含了不同的版本。 在后面的步骤中,您将看到如何找出该库的版本以及原因。

fileTree(dir: 'libs', include: ['*.jar'])

是一个fileTree依赖项,它将libs文件夹中的所有jar文件添加到编译类路径中

com.android.support:appcompat-v7:26.1.0

将Android兼容性库添加到项目中。 这使您可以在SDK版本7以前的任何Android应用程序中使用材质设计主题和其他功能。

com.android.support.constraint:constraint-layout:1.0.2

将Android约束布局添加到项目中。 这允许您在任何像SDK版本9一样早的Android应用程序中使用ConstraintLayout布局类。

四、运行标准的Gradle任务

Android Studio通过IDE可以轻松构建和部署应用程序的调试版本,但最终Gradle还是参与其中。可以在Android Studio中命令行使用gradle命令构建

$ ./gradlew build

构建完成,Build Successful,可以在app/build/outputs/apk目录查看生成的apk,包括debug和release版本的apk.

  • 可以在命令行中显示在项目中使用的support-annotations模块的版本。

执行命令,可以看到结果:

$ ./gradlew :app:dependencies --configuration releaseCompileClasspath
:app:dependencies

------------------------------------------------------------
Project :app
------------------------------------------------------------

releaseCompileClasspath - Resolved configuration for compilation for variant: release
+--- com.android.support:appcompat-v7:26.1.0
|    +--- com.android.support:support-annotations:26.1.0
|    +--- com.android.support:support-v4:26.1.0
|    |    +--- com.android.support:support-compat:26.1.0
|    |    |    +--- com.android.support:support-annotations:26.1.0
|    |    |    \--- android.arch.lifecycle:runtime:1.0.0
|    |    |         +--- android.arch.lifecycle:common:1.0.0
|    |    |         \--- android.arch.core:common:1.0.0
|    |    +--- com.android.support:support-media-compat:26.1.0
|    |    |    +--- com.android.support:support-annotations:26.1.0
|    |    |    \--- com.android.support:support-compat:26.1.0 (*)
|    |    +--- com.android.support:support-core-utils:26.1.0
|    |    |    +--- com.android.support:support-annotations:26.1.0
|    |    |    \--- com.android.support:support-compat:26.1.0 (*)
|    |    +--- com.android.support:support-core-ui:26.1.0
|    |    |    +--- com.android.support:support-annotations:26.1.0
|    |    |    \--- com.android.support:support-compat:26.1.0 (*)
|    |    \--- com.android.support:support-fragment:26.1.0
|    |         +--- com.android.support:support-compat:26.1.0 (*)
|    |         +--- com.android.support:support-core-ui:26.1.0 (*)
|    |         \--- com.android.support:support-core-utils:26.1.0 (*)
|    +--- com.android.support:support-vector-drawable:26.1.0
|    |    +--- com.android.support:support-annotations:26.1.0
|    |    \--- com.android.support:support-compat:26.1.0 (*)
|    \--- com.android.support:animated-vector-drawable:26.1.0
|         +--- com.android.support:support-vector-drawable:26.1.0 (*)
|         \--- com.android.support:support-core-ui:26.1.0 (*)
\--- com.android.support.constraint:constraint-layout:1.0.2
     \--- com.android.support.constraint:constraint-layout-solver:1.0.2

(*) - dependencies omitted (listed previously)


BUILD SUCCESSFUL

从命令行打印的信息可以看到support-annotations module,version 26.1.0,是appcompat-v7所依赖的一个库

  • 另一种方式查看所需的版本

执行命令及结果:

$ ./gradlew :app:dependencyInsight --dependency support-annotations --configuration releaseCompileClasspath
:app:dependencyInsight
com.android.support:support-annotations:26.1.0
+--- com.android.support:appcompat-v7:26.1.0
|    \--- releaseCompileClasspath
+--- com.android.support:support-compat:26.1.0
|    +--- com.android.support:support-vector-drawable:26.1.0
|    |    +--- com.android.support:appcompat-v7:26.1.0 (*)
|    |    \--- com.android.support:animated-vector-drawable:26.1.0
|    |         \--- com.android.support:appcompat-v7:26.1.0 (*)
|    +--- com.android.support:support-v4:26.1.0
|    |    \--- com.android.support:appcompat-v7:26.1.0 (*)
|    +--- com.android.support:support-media-compat:26.1.0
|    |    \--- com.android.support:support-v4:26.1.0 (*)
|    +--- com.android.support:support-fragment:26.1.0
|    |    \--- com.android.support:support-v4:26.1.0 (*)
|    +--- com.android.support:support-core-utils:26.1.0
|    |    +--- com.android.support:support-v4:26.1.0 (*)
|    |    \--- com.android.support:support-fragment:26.1.0 (*)
|    \--- com.android.support:support-core-ui:26.1.0
|         +--- com.android.support:animated-vector-drawable:26.1.0 (*)
|         +--- com.android.support:support-v4:26.1.0 (*)
|         \--- com.android.support:support-fragment:26.1.0 (*)
+--- com.android.support:support-core-ui:26.1.0 (*)
+--- com.android.support:support-core-utils:26.1.0 (*)
+--- com.android.support:support-media-compat:26.1.0 (*)
\--- com.android.support:support-vector-drawable:26.1.0 (*)

(*) - dependencies omitted (listed previously)


BUILD SUCCESSFUL

这两种方式都可以帮助追踪和解决与library版本冲突的任何问题。

五、查看Gradle窗口可执行的命令

"gradle window"

可以看一下,android-signingReport查看签名报告任务,签名报告任务不需要任何参数,可以双击执行,结果如下:

Executing tasks: [signingReport]

Configuration on demand is an incubating feature.
:app:signingReport
Variant: release
Config: none
----------
Variant: releaseUnitTest
Config: none
----------
Variant: debug
Config: debug
Store: /Users/liushihan/.android/debug.keystore
Alias: AndroidDebugKey
MD5: 8C:43:C9:2D:61:38:E1:AC:18:7B:93:4B:43:4B:CE:3B
SHA1: 2A:29:82:41:7F:BA:AE:50:00:B8:DA:94:35:86:10:32:30:9C:A8:87
Valid until: 2047年5月1日 星期三
----------
Variant: debugAndroidTest
Config: debug
Store: /Users/liushihan/.android/debug.keystore
Alias: AndroidDebugKey
MD5: 8C:43:C9:2D:61:38:E1:AC:18:7B:93:4B:43:4B:CE:3B
SHA1: 2A:29:82:41:7F:BA:AE:50:00:B8:DA:94:35:86:10:32:30:9C:A8:87
Valid until: 2047年5月1日 星期三
----------
Variant: debugUnitTest
Config: debug
Store: /Users/liushihan/.android/debug.keystore
Alias: AndroidDebugKey
MD5: 8C:43:C9:2D:61:38:E1:AC:18:7B:93:4B:43:4B:CE:3B
SHA1: 2A:29:82:41:7F:BA:AE:50:00:B8:DA:94:35:86:10:32:30:9C:A8:87
Valid until: 2047年5月1日 星期三
----------

BUILD SUCCESSFUL in 14s
1 actionable task: 1 executed

从打印的结果可以看到,公钥存储路径(这里是用户根目录下的debug.keystore文件)

下面的install,可以通过installDebug来安装应用

也可用命令行来执行:

$ ./gradlew installDebug

Glide4.x源码解析(1)-基本使用

发表于 2017-12-11 |

一、关于Glide

  • 官方定义:

    • Glide是一个快速高效的Android图片加载库,注重于平滑的滚动。Glide提供了易用的API,高性能、可扩展的图片解码管道(decode pipeline),以及自动的资源池技术。

      • Glide 支持拉取,解码和展示视频快照,图片,和GIF动画。Glide的Api是如此的灵活,开发者甚至可以插入和替换成自己喜爱的任何网络栈。默认情况下,Glide使用的是一个定制化的基于HttpUrlConnection的栈,但同时也提供了与Google Volley和Square OkHttp快速集成的工具库。
    • 虽然Glide 的主要目标是让任何形式的图片列表的滚动尽可能地变得更快、更平滑,但实际上,Glide几乎能满足你对远程图片的拉取/缩放/显示的一切需求。

  • 性能

    两个关键点:

    • 图片的解码速度

    • 解码图片带来的资源压力也就是内存消耗

      Glide使用了多个步骤来确保在Android上加载图片尽可能的快速和平滑:

    • 自动、智能地下采样(downsampling)和缓存(caching),以最小化存储开销和解码次数;

    • 积极的资源重用,例如字节数组和Bitmap,以最小化昂贵的垃圾回收和堆碎片影响;
    • 深度的生命周期集成,以确保仅优先处理活跃的Fragment和Activity的请求,并有利于应用在必要时释放资源以避免在后台时被杀掉。

二、使用配置

1.依赖

Gradle

使用 Gradle,可从 Maven Central 或 JCenter 中添加对 Glide 的依赖。还需要添加 Android 支持库的依赖

repositories {
  mavenCentral()
  maven { url 'https://maven.google.com' }
}

dependencies {
    compile 'com.github.bumptech.glide:glide:4.4.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.4.0'
}
Maven

使用 Maven,同样可以添加对 Glide 的依赖。同样需要添加Android支持库的依赖

<dependency>
  <groupId>com.github.bumptech.glide</groupId>
  <artifactId>glide</artifactId>
  <version>4.4.0</version>
  <type>aar</type>
</dependency>
<dependency>
  <groupId>com.google.android</groupId>
  <artifactId>support-v4</artifactId>
  <version>r7</version>
</dependency>
<dependency>
  <groupId>com.github.bumptech.glide</groupId>
  <artifactId>compiler</artifactId>
  <version>4.4.0</version>
  <optional>true</optional>
</dependency>

2.相关设置

权限
  • 网络

当需要的图片是需要网络的情况,需在AndroidManifest添加网络权限。

<uses-permission android:name="android.permission.INTERNET"/>
  • 连接状态

如果你正在从 URL 加载图片,Glide 可以自动帮助你处理片状网络连接:它可以监听用户的连接状态并在用户重新连接到网络时重启之前失败的请求。如果 Glide 检测到你的应用拥有 ACCESS_NETWORK_STATUS 权限,Glide 将自动监听连接状态而不需要额外的改动。

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  • 本地存储

要从本地文件夹或 DCIM 或图库中加载图片,你将需要添加 READ_EXTERNAL_STORAGE 权限。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

三、基本使用

Glide的基本使用很简单:

Glide.with(fragment)
    .load(myUrl)
    .into(imageView);

一行就可以搞定从网络加载图片,丝柔顺滑。

具体实现:

public class MainActivity extends AppCompatActivity {

    Button mBtn;
    ImageView imageView;
    String myUrl = "http://ot29getcp.bkt.clouddn.com/map.png";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtn = (Button)findViewById(R.id.downloadimage);
        imageView = (ImageView) findViewById(R.id.image);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Glide.with(getApplicationContext())
                        .load(myUrl)
                        .into(imageView);
            }
        });

    }
}

核心代码:

就这一行。

Glide.with(getApplicationContext())
                            .load(myUrl)
                            .into(imageView);

Java并发编程:volatile关键字解析

发表于 2017-12-08 |

原文出处:http://www.cnblogs.com/dolphin0520/p/3920373.html

一.内存模型的相关概念

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

i = i + 1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  1. 通过在总线加LOCK#锁的方式

  2. 通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

二.并发编程中的三个概念

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:

  1. 原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

同样地反映到并发编程中会出现什么结果呢?

举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

i = 9;

假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

  1. 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

举个简单的例子,看下面这段代码:

//线程1执行的代码
int i = 0;
i = 10;

//线程2执行的代码
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

  1. 有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

这段代码有4个语句,那么可能的一个执行顺序是:

语句2 语句1 语句3 语句4

那么可不可能是这个执行顺序呢:

语句2 语句1 语句4 语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

 //线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

三.Java内存模型

在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

举个简单的例子:在java中,执行下面这个语句:

i  = 10;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?

  1. 原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

请分析以下哪些操作是原子性操作:

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

所以上面4个语句只有语句1的操作具备原子性。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

  1. 可见性

对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  1. 有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

这8条原则摘自《深入理解Java虚拟机》。

这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

下面我们来解释一下前4条规则:

对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

四.深入剖析volatile关键字

在前面讲述了很多东西,其实都是为讲述volatile关键字作铺垫,那么接下来我们就进入主题。

  1. volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  • 禁止进行指令重排序。

先看一段代码,假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是用volatile修饰之后就变得不一样了:

  • 第一,使用volatile关键字会强制将修改的值立即写入主存;

  • 第二,使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  • 第三,由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

那么线程1读取到的就是最新的正确的值。

  1. volatile保证原子性吗?

从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?

下面看一个例子:

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加了1。

解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

把上面的代码改成以下任何一种都可以达到效果:

采用synchronized:

public class Test {
    public  int inc = 0;

    public synchronized void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();

    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();

    public  void increase() {
        inc.getAndIncrement();
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

  1. volatile能保证有序性吗?

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可能上面说的比较绕,举个简单的例子:

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

  1. volatile的原理和实现机制

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

五.使用volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

  • 对变量的写操作不依赖于当前值

  • 该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

下面列举几个Java中使用volatile的几个场景。

  1. 状态标记量

    volatile boolean flag = false;
    
    while(!flag){
        doSomething();
    }
    
    public void setFlag() {
        flag = true;
    }
    
    volatile boolean inited = false;
    //线程1:
    context = loadContext();  
    inited = true;            
    
    //线程2:
    while(!inited ){
    sleep()
    }
    doSomethingwithconfig(context);
    
  2. double check

    class Singleton{
        private volatile static Singleton instance = null;
    
        private Singleton() {
    
        }
    
        public static Singleton getInstance() {
            if(instance==null) {
                synchronized (Singleton.class) {
                    if(instance==null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }
    

为什么要用volatile修饰instance?

主要在于ImportNew
首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源
你真的了解volatile关键字吗?
2017/04/01 | 分类: 基础技术 | 6 条评论 | 标签: VOLATILE

分享到: 35
原文出处: Ruheng
一、Java内存模型
想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。

Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。

举个简单的例子:在java中,执行下面这个语句:

1
i = 10++;
执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

比如同时有2个线程执行这段代码,假如初始时i的值为10,那么我们希望两个线程执行完之后i的值变为12。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的工作内存当中,然后线程1进行加1操作,然后把i的最新值11写入到内存。此时线程2的工作内存当中i的值还是10,进行加1操作之后,i的值为11,然后线程2把i的值写入内存。

最终结果i的值是11,而不是12。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

那么如何确保共享变量在多线程访问时能够正确输出结果呢?

在解决这个问题之前,我们要先了解并发编程的三大概念:原子性,有序性,可见性。

二、原子性
1.定义
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

2.实例
一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

同样地反映到并发编程中会出现什么结果呢?

举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

1
i = 9;
假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

3.Java中的原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

请分析以下哪些操作是原子性操作:

1
2
3
4
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
咋一看,可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

所以上面4个语句只有语句1的操作具备原子性。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

关于synchronized和Lock的使用,参考:关于synchronized和ReentrantLock之多线程同步详解

三、可见性
1.定义
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

2.实例
举个简单的例子,看下面这段代码:

1
2
3
4
5
6
//线程1执行的代码
int i = 0;
i = 10;

//线程2执行的代码
j = i;
由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到工作内存中,然后赋值为10,那么在线程1的工作内存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到线程2的工作内存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3.Java中的可见性
对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

四、有序性
1.定义
有序性:即程序执行的顺序按照代码的先后顺序执行。

2.实例
举个简单的例子,看下面这段代码:

1
2
3
4
5
6
int i = 0;

boolean flag = false;

i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

1
2
3
4
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
这段代码有4个语句,那么可能的一个执行顺序是:

那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

1
2
3
4
5
6
7
8
9
10
//线程1:

context = loadContext(); //语句1
inited = true; //语句2

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

3.Java中的有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):
①程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

②锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作

③volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

④传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

⑤线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

⑥线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

⑦线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

⑧对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

下面我们来解释一下前4条规则:

对于程序次序规则来说,就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,但是虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

五、深入理解volatile关键字
1.volatile保证可见性
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

先看一段代码,假如线程1先执行,线程2后执行:

1
2
3
4
5
6
7
8
//线程1
boolean stop = false;
while(!stop){
doSomething();
}

//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是用volatile修饰之后就变得不一样了:

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

那么线程1读取到的就是最新的正确的值。

2.volatile不能确保原子性
下面看一个例子:

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 Test {
public volatile int inc = 0;

public void increase() {
    inc++;
}

public static void main(String[] args) {
    final Test test = new Test();
    for(int i=0;i<10;i++){
        new Thread(){
            public void run() {
                for(int j=0;j<1000;j++)
                    test.increase();
            };
        }.start();
    }

    while(Thread.activeCount()>1)  //保证前面的线程都执行完
        Thread.yield();
    System.out.println(test.inc);
}

}
大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加了1。

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过AtomicInteger。

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

3.volatile保证有序性
在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可能上面说的比较绕,举个简单的例子:

1
2
3
4
5
6
7
8
//x、y为非volatile变量
//flag为volatile变量

x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

那么我们回到前面举的一个例子:

1
2
3
4
5
6
7
8
9
//线程1:
context = loadContext(); //语句1
inited = true; //语句2

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

六、volatile的实现原理
1.可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

2.有序性
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

七、volatile的应用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

下面列举几个Java中使用volatile的几个场景。

①.状态标记量

1
2
3
4
5
6
7
8
9
volatile boolean flag = false;
//线程1
while(!flag){
doSomething();
}
//线程2
public void setFlag() {
flag = true;
}
根据状态标记,终止线程。

②.单例模式中的double check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton{
private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {
    if(instance==null) {
        synchronized (Singleton.class) {
            if(instance==null)
                instance = new Singleton();
        }
    }
    return instance;
}

}

为什么要使用volatile 修饰instance?

主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 instance 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

参考资料:

《Java 中的双重检查(Double-Check)》http://blog.csdn.net/dl88250/article/details/5439024

和http://www.iteye.com/topic/652440

《Java编程思想》

《深入理解Java虚拟机》

http://jiangzhengjun.iteye.com/blog/652532

http://blog.sina.com.cn/s/blog_7bee8dd50101fu8n.html

http://ifeve.com/volatile/

http://blog.csdn.net/ccit0519/article/details/11241403

http://blog.csdn.net/ns_code/article/details/17101369

http://www.cnblogs.com/kevinwu/archive/2012/05/02/2479464.html

http://www.cppblog.com/elva/archive/2011/01/21/139019.html

http://ifeve.com/volatile-array-visiblity/

http://www.bdqn.cn/news/201312/12579.shtml

http://exploer.blog.51cto.com/7123589/1193399

http://www.cnblogs.com/Mainz/p/3556430.html

PackageManager使用姿势

发表于 2017-10-25 |

PackageManager它的主要职责是管理应用程序包。 通过PackageManager,我们就可以获取应用程序信息。

一、主要功能

  1. 安装、卸载应用
  2. 查询权限
  3. 查询Application相关信息

二、常用方法

1)、public abstract PackageInfo getPackageInfo(String packageName, int flags)根据包名获取对应的PackageInfo,注意,此处的flags标签:
GET_ACTIVITIES
GET_GIDS
GET_CONFIGURATIONS
GET_INSTRUMENTATION
GET_PERMISSIONS
GET_PROVIDERS
GET_RECEIVERS
GET_SERVICES
GET_SIGNATURES
GET_UNINSTALLED_PACKAGES
(2)、public abstract int[] getPackageGids(String packageName),根据包名获取group-ids

(3)、public abstract PermissionInfo getPermissionInfo(String name, int flags),根据包名和指定的flags获取指定的授权信息

(4)、public abstract List getAllPermissionGroups(int flags);获取所以PermissGroup集合

(5)、public abstract PermissionGroupInfo getPermissionGroupInfo(String name,
int flags)根据指定的Group名称获取PermissionGroupInfo对象。

(6)、public abstract ApplicationInfo getApplicationInfo(String packageName,
int flags),根据指定的包名获取ApplicationInfo信息。

(7)、public abstract ActivityInfo getActivityInfo(ComponentName component,
int flags),根据指定的组件,获取ActivityInfo信息

(8)、public abstract ServiceInfo getServiceInfo(ComponentName component,
int flags),根据指定组件获取ServiceInfo

(9)、public abstract ProviderInfo getProviderInfo(ComponentName component,
int flags),根据指定组件名称获取ProviderInfo信息

(10)、public abstract List getInstalledPackages(int flags);获取所有安装的PackagInfo信息

(11)、public abstract List getPackagesHoldingPermissions(
String[] permissions, int flags);获取具有特定权限的PackagInfo

(12)、public abstract List getInstalledApplications(int flags);获取安装的ApplicationInfo信息

(13)、public abstract boolean addPermission(PermissionInfo info);添加权限

(14)、public abstract void removePermission(String name);移除权限

2、PackageInfo用于描述mainfest中所有描述信息。
常见字段:
(1)、public String packageName;包名
(2)、public String[] splitNames;
(3)、public int versionCode;版本号     
(4)、public String versionName;版本名称
(5)、public ApplicationInfo applicationInfo;
(6)、public long firstInstallTime;第一次安装时间
(7)、public long lastUpdateTime;上次更新时间
(8)、public ActivityInfo[] activities;所有的Activity信息
(9)、public ActivityInfo[] receivers; 所有的广播接收者
(10)、public ServiceInfo[] services;所有的服务信息
(11)、public ProviderInfo[] providers;获取ContentProvide
(12)、public PermissionInfo[] permissions;所有的权限信息

三、AndroidManifest文件结构

首先看一下AndroidManifest.xml文件结构。

例如:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test">

<uses-permission android:name="android.permission.INTERNET" />

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".TestActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <service
        android:name="testservice"
        android:enable="true"
        android:exproted="true">
        </service>
</application>

</manifest>

根据AndroidManifest的各个节点可以看出来其中的对应关系:

* 一个Package对应一个Application
* 一个Application对应n个Activity、Service等等
  • 通过包名判断APP是否安装
  • 通过遍历获取AndroidManifest.xml文件信息

四、获取信息

1.ApplicationInfo类测试:获取Application节点信息
  • 示例:

    ApplicationInfo applicationInfo = getApplicationInfo();  
     Log.d("lsh",applicationInfo.className + "\n" +  
            applicationInfo.dataDir+"\n" +  
            applicationInfo.permission + "\n"  
            + applicationInfo.packageName + "\n"  
            + applicationInfo.processName + "\n"  
            + applicationInfo.taskAffinity + "\n"  
    );  
    
  • Logcat:

    3402-3402/com.example.liushihan.glidedemo D/lsh: null                                                 
        /data/data/com.example.liushihan.glidedemo
        null
         com.example.liushihan.glidedemo
         com.example.liushihan.glidedemo
         com.example.liushihan.glidedemo
    
2. 获取所有安装的Packages
  • 示例:

    List<PackageInfo> listPack = getPackageManager().getInstalledPackages(PackageManager.GET_ACTIVITIES);
    
  • Logcat:
    部分日志

     D/lsh: PackageInfo{132b54a7 com.android.smoketest}
    com.example.liushihan.glidedemo D/lsh: PackageInfo{3d681d54 com.example.android.livecubes}
    com.example.liushihan.glidedemo D/lsh: PackageInfo{213c44fd com.android.providers.telephony}
    
3. 获取指定应用的PackageInfo
  • 示例:

    PackageManager packageManager = getPackageManager();
            PackageInfo packageInfo = null;
            try {
                packageInfo = packageManager.getPackageInfo("com.example.liushihan",
                        PackageManager.GET_ACTIVITIES);
                Log.d("lsh", packageInfo.packageName + "\n"
                        + packageInfo.versionName + "\n"
                );
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
    
  • Logcat

    D/lsh: com.example.liushihan.glidedemo
                                1.0
    
4. 获取应用程序中的permission
  • 示例:

    try {
             PermissionInfo permissionInfo = getPackageManager().getPermissionInfo("android.permission.INTERNET",
                     PermissionInfo.PROTECTION_NORMAL);
             List<PermissionGroupInfo> list = getPackageManager().getAllPermissionGroups(PackageManager.PERMISSION_GRANTED);
             Log.d("lsh",
                     permissionInfo.group + "\n"
                             + permissionInfo.packageName + "\n"
                             + permissionInfo.name + "\n"
                             + permissionInfo.flags + "\n"
             );
         } catch (PackageManager.NameNotFoundException e) {
             e.printStackTrace();
         }
    
  • Logcat:

    D/lsh: android.permission-group.NETWORK
                                      android
                                      android.permission.INTERNET
                                      0
    
5. 获取应用程序中执行的ActivityInfo
  • 示例:
ComponentName componentName = new ComponentName("com.example.liushihan.glidedemo","com.example.liushihan.glidedemo.MainActivity");
        try {
            @SuppressLint("WrongConstant") ActivityInfo activityInfo = getPackageManager().getActivityInfo(componentName, PackageManager.GET_ACTIVITIES);
            Log.d("lsh:activityInfo",activityInfo.name + "\n"
                    + activityInfo.packageName +"\n"
                    + activityInfo.targetActivity
            );
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }  
  • Logcat:
4829-4829/com.example.liushihan.glidedemo D/lsh:activityInfo: com.example.liushihan.glidedemo.MainActivity
                                                                                 com.example.liushihan.glidedemo
                                                                                 null

Android View事件分发机制

发表于 2017-10-11 |

一、点击事件的传递规则

首先,要明白点击事件的分发就是MotionEvent事件的分发过程。

1.MotionEvent

常见的动作:

常见的动作常量:

  • public static final int ACTION_DOWN = 0;单点触摸动作

  • public static final int ACTION_UP = 1;单点触摸离开动作

  • public static final int ACTION_MOVE = 2;触摸点移动动作
  • public static final int ACTION_CANCEL = 3;触摸动作取消
  • public static final int ACTION_OUTSIDE = 4;触摸动作超出边界
  • public static final int ACTION_POINTER_DOWN = 5;多点触摸动作
  • public static final int ACTION_POINTER_UP = 6;多点离开动作
主要就是:

* ACTION_DOWN--手指刚接触屏幕
* ACTION_MOVE--手指在屏幕上滑动
* ACTION_UP--手指离开屏幕

2.点击事件的分发中最重要的三个方法

  • dispatchTouchEvent(MotionEvent event)–事件分发

    • 用来进行事件分发,如果事件能够传递给当前View,那么这个方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消耗了当前事件。
  • onInterceptTouchEvent(MotionEvent event)–事件拦截

    • 用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会再次调用,返回结果表示是否拦截当前事件。
  • onTouchEvent(MotionEvent event)–事件消费

    • 在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再次接收到事件

3. 伪代码

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean touch = false;
    if(onInterceptTouchEvent(ev)){
        touch = onTouchEvent(ev);
    }else{
        touch = child.dispatchTouchEvent(ev);
    }
    return touch;
}
  • 点击事件的传递规则:

    • 对于根ViewGroup来说,点击事件产生后,首先传递给它,这个时候它的dispatchTouchEvent就会被调用

    • 首先判断自身是否处理,处理的话就会调用onInterceptTouchEvent(MotionEvent event),来判断是否处理,自身处理的话,就会调用onTouchEvent(MotionEvent event),自己就把这次点击事件处理了

    • 如果自身不处理,就不拦截,交给子View去处理。子View处理同样按照ViewGroup自身处理一致。子View不处理的话,最后则调用ViewGroup自身的onTouchEvent(MotionEvent event)消费掉此次点击事件

      经典的伪代码如上。

4.同一次点击事件只能被一个 View 消费

安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。
如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。

借用GcsSloop源码分析:

public boolean dispatchTouchEvent(MotionEvent ev) {
      // 调试用
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

      // 判断事件是否是针对可访问的焦点视图(很晚才添加的内容,个人猜测和屏幕辅助相关,方便盲人等使用设备)
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 处理第一次ACTION_DOWN.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 清除之前所有的状态
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 检查是否需要拦截.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);    // 询问是否拦截
                ev.setAction(action);                         // 恢复操作,防止被更改
            } else {
                intercepted = false;
            }
        } else {
              // 没有目标来处理该事件,而且也不是一个新的事件事件(ACTION_DOWN), 进行拦截。
            intercepted = true;
        }

          // 判断事件是否是针对可访问的焦点视图
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // 检查事件是否被取消(ACTION_CANCEL).
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;

          // 如果没有取消也没有被拦截    (进入事件分发)
        if (!canceled && !intercepted) {

            // 如果事件是针对可访问性焦点视图,我们将其提供给具有可访问性焦点的视图。
              // 如果它不处理它,我们清除该标志并像往常一样将事件分派给所有的 ChildView。 
            // 我们检测并避免保持这种状态,因为这些事非常罕见。
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // 清除此指针ID的早期触摸目标,防止不同步。
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);    // 获取触摸位置坐标
                    final float y = ev.getY(actionIndex);
                    // 查找可以接受事件的 ChildView
                    final ArrayList<View> preorderedList = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                      // ▼注意,从最后向前扫描
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ? children[childIndex] : preorderedList.get(childIndex);

                        // 如果有一个视图具有可访问性焦点,我们希望它首先获取事件,
                          // 如果不处理,我们将执行正常的分派。 
                          // 尽管这可能会分发两次,但它能保证在给定的时间内更安全的执行。
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                          // 检查View是否允许接受事件(即处于显示状态(VISIBLE)或者正在播放动画)
                          // 检查触摸位置是否在View区域内
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                          // getTouchTarget 中判断了 child 是否包含在 mFirstTouchTarget 中
                          // 如果有返回 target,如果没有返回 null 
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // ChildView 已经准备好接受在其区域内的事件。
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;    // ◀︎已经找到目标View,跳出循环
                        }

                        resetCancelNextUpFlag(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 没有找到 ChildView 接收事件
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // 分发 TouchTarget
        if (mFirstTouchTarget == null) {
            // 没有 TouchTarget,将当前 ViewGroup 当作普通的 View 处理。
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 分发TouchTarget,如果我们已经分发过,则避免分配给新的目标。 
              // 如有必要,取消分发。
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 如果需要,更新指针的触摸目标列表或取消。
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

源码注释已经很详细了。

二、小结

1. 同一个事件是从DOWN开始到UP结束的

2. 一个事件最终只能由一个View拦截和消费掉

3. 某个View一旦拦截了,那么久都由它来处理,并且它的onInterceptTouchEvent不会再被调用。因为事件交给要拦截的View处理,就不需要再调用onInterceptTouchEvent去询问是否要拦截事件了

4. 如果一个View拦截了事件,但是如果它不消耗ACTION_DOWN事件,那么同一事件序列就不再会交由该View处理了。

5. ViewGroup默认是不拦截任何事件的,源码中ViewGroup的onInterceptTouchEvent默认返回false

6. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。

7. View没有onInterceptTouchEvent方法,一旦事件分发到View,那么它的onTouchEven就会被调用

8. View的onTouchEvent方法默认会消耗事件即返回true,除非是不可点击的。

9. 事件的传递过程是由外向内的,即先传给父元素,然后再由父元素分发给子View

10. 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

1…678…14
Neil Liu

Neil Liu

优秀不够,你是否无可替代

68 日志
25 标签
GitHub
© 2019 Neil Liu
由 Hexo 强力驱动
主题 - NexT.Muse