0%

对象的布局其实就是对象在虚拟机中的组成部分。在Hotspot虚拟机中,对象分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头包括两部分信息:对象自身的运行时数据(官方称它为“Mark Word”)以及类型指针。

对象自身的运行时数据包含:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit。需要注意的是对象头信息是与对象自身定义的数据无关的额外存储成本。

类型指针是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,可以理解为对象的实例信息(即下文的oops)。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。

实例数据部分是对象真正存储的有效信息,也是在程序代码中定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。Hotspot虚拟机默认的分配策略为:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true,那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于Hotspot VM的自动内存管理系统要求对象其实地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

下面通过一个详细的例子说明JVM的分配策略以及对齐填充,以加深对象的布局理解。通过Unsafe拿到对象属性的offset与Instrumentation计算的大小对比是否一致,如下所以一段代码:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

package jvm;

import com.javamex.classmexer.MemoryUtil;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.Arrays;

public class SizeOfUnsafe {
static class Person {
private String s; //4
private int i;//4
private byte b;//1
private double d;//8
private byte b2;//1
private Score obj;//4
private byte b3;//1
private int i2;//4
}

static class Score {

}

public static void main(String[] args) {
byUnsafe();
byInstrumentation();
}

private static void byUnsafe() {
try {
Field[] declaredFields = Person.class.getDeclaredFields();
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

System.out.println("定义顺序");
for (Field f : declaredFields) {
System.out.println("field " + f.getName() + " \t offset : " + unsafe.objectFieldOffset(f) + "\t");
}


System.out.println();
System.out.println("重排顺序");

Arrays.sort(declaredFields, (left, right) -> Math.toIntExact(unsafe.objectFieldOffset(left) - unsafe.objectFieldOffset(right)));
for (Field f : declaredFields) {
System.out.println("field " + f.getName() + " \t offset : " + unsafe.objectFieldOffset(f) + "\t");
}

} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}

private static void byInstrumentation() {
System.out.println();
Person person = new Person();

//Shallow Size : 对象自身占用的内存大小,不包括它引用的对象。针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。针对数组类型的对象,它的大小是数组元素对象的大小总和。
System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(person) + " bytes");

//Retained Size : 当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。
System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(person) + " bytes");
}
}

打印结果如下所示,可以看到重排顺序offset是36,加上4个字节即36 + 4 = 40 bytes,同Instrumentation计算出来的40 bytes一致。运行Instrumentation时,我这里采用了 classmexer-0_03.zip 这个jar包来帮我统计大小。我采用IntelliJ运行,需要添加一下jvm的运行时参数 -javaagent:classmexer.jar,如下图所示:

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
定义顺序
field s offset : 32
field i offset : 12
field b offset : 28
field d offset : 16
field b2 offset : 29
field obj offset : 36
field b3 offset : 30
field i2 offset : 24

重排顺序
field i offset : 12
field d offset : 16
field i2 offset : 24
field b offset : 28
field b2 offset : 29
field b3 offset : 30
field s offset : 32
field obj offset : 36

Shallow Size: 40 bytes
Retained Size: 40 bytes

Process finished with exit code 0

jvm-object-object-layout-analysis

下面分析未采用分配策略与采用了分配策略之间的区别,针对上述代码中的Person对象,如果未采用分配策略,直接分配内存,在内存中的布局如下所示,有9个对齐位,可以看到需要用到48 bytes:

1
2
3
4
5
6
7
8
9
10
11
12
13
[HEADER:   8 bytes]  8
[oops: 4 bytes] 12
[s: 4 bytes] 16
[i: 4 bytes] 20
[b: 1 byte ] 21
[padding: 3 bytes] 24
[d: 8 bytes] 32
[b2: 1 byte ] 33
[obj: 4 bytes] 37
[b3: 1 byte ] 38
[padding: 2 byte ] 40
[i2: 4 bytes] 44
[padding: 4 byte ] 48

而采用分配策略后,按照longs/doubles、ints、shorts/chars、bytes/booleans、oops的顺序排列,在内存中的布局如下所示,只有1个对齐位,整体用到40 bytes,较上面的方式显然更加节省内存空间:

1
2
3
4
5
6
7
8
9
10
11
[HEADER:   8 bytes]  8
[oops: 4 bytes] 12
[i: 4 bytes] 16
[d: 8 bytes] 24
[i2: 4 bytes] 28
[b: 1 byte ] 29
[b2: 1 byte ] 30
[b3: 1 byte ] 31
[padding: 1 byte ] 32
[s: 4 bytes] 36
[obj: 4 bytes] 40

另外关于对象头占用空间大小,这里说明一下32位系统和64位系统中对象所占用内存空间的大小:

  • 在32位系统下,存放Class Pointer的空间大小是4字节,MarkWord是4字节,对象头为8字节;
  • 在64位系统下,存放Class Pointer的空间大小是8字节,MarkWord是8字节,对象头为16字节;
  • 64位开启指针压缩的情况下,存放Class Pointer的空间大小是4字节,MarkWord是8字节,对象头为12字节;
  • 如果是数组对象,对象头的大小为:数组对象头8字节+数组长度4字节+对齐4字节=16字节。其中对象引用占4字节(未开启指针压缩的64位为8字节),数组MarkWord为4字节(64位未开启指针压缩的为8字节);
  • 静态属性不算在对象大小内。

参考:

  1. 深入理解Java虚拟机(第2版)
  2. Java对象内存布局
  3. 聊聊JVM(三)两种计算Java对象大小的方法
  4. Java对象内存结构

这篇文章讨论一个在Java语言层面来看非常简单的问题:怎样创建对象?通常仅仅一个 new 关键字而已,但是在虚拟机中,对象(特指普通对象,不包括数组对象和Class对象)的创建流程又是怎样的呢?

根据 深入理解Java虚拟机(第2版) 中的知识,我画出了虚拟机创建普通对象的流程如,如下所示:

jvm-object-the-process-of-creating-common-objects-in-virtual-machines

虚拟机在遇到一条new指令时,首先会检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

在类加载检查通过后,接下来是虚拟机为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。Java堆是否规整决定了分配方式是 “指针碰撞” 还是 “空闲列表”。而Java堆是否规整依据的是GC的收集器采用的哪种分配算法,像Serial、ParNew等带整理算法的收集器,JVM会采用 “指针碰撞” 分配;而使用CMS这种标记-清除算法的收集器,JVM通常采用“空闲列表”分配。

  • “指针碰撞”仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。因为所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。

  • “空闲列表”是由于已使用内存和空间内存相互交错,虚拟机必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象示例,并更新列表上的记录。

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

接下来虚拟机要对对象进行必要的设置,通俗说就是设置对象头:类的元数据信息、对象的哈希表、对象的GC分带年龄、是否启用偏斜锁等。后续会对对象头做详细介绍。

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——方法还没有执行,所有的字段都还为零。所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。


参考:

  1. 深入理解Java虚拟机(第2版)

下面这张图是结合《深入理解Java虚拟机2》以及《Java核心36讲》中的内容画出来的,理论上看算是比较详细的描述了JVM运行时内存区域。从图中可以看出主要包含了两部分:运行时数据区与其他数据区(主要指直接内存和Code Cache),而在运行时数据区中又分为线程隔离和线程共享。

jvm-runtime-memory-area

1. 运行时数据区

1.1. 线程隔离

  • 程序计数器

    程序计数器(PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。

    该区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

  • Java虚拟机栈

    Java 虚拟机栈(Java Virtual Machine Stack)。每个线程在创建时都会创建一个虚拟机栈,它的生命周期与线程相同,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。

    在一个时间点,对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。

    栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。栈帧结构详细说明可以参考《Java虚拟机原理图解》3、JVM运行时数据区

    在Java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展且无法申请到足够内存时,将会抛出OutOfMemoryError异常。

  • 本地方法栈

    本地方法栈(Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。

    与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

1.2. 线程共享

  • 堆(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有对象都在堆上分配变得不是那么绝对了。堆被所有的线程共享,在虚拟机启动时,我们指定的 “Xmx” 之类参数就是用来指定最大堆空间等指标。理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。

    如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

  • 方法区

    方法区(Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、类型信息、字段信息、方法信息、类变量、指向类加载器的引用、指向Class实例的引用、方法表等。由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。方法区结构的详细说明可以参考《Java虚拟机原理图解》3、JVM运行时数据区

    运行时常量池(Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。 例如下面这段展示代码:

    1
    2
    3
    4
    public class ConstantTest {
    public static final int testI = 1;
    public static float testF = 1.234f;
    }

    经过javap命令后,观察它的常量池情况,第10行起:

    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
      D:\Project\IntellJ2\DemoIO\src>javap -v ConstantTest.class
    Classfile /D:/Project/IntellJ2/DemoIO/src/ConstantTest.class
    Last modified 2018-9-14; size 337 bytes
    MD5 checksum 4088cb12575ced51f0f0f09ac26c14ff
    Compiled from "ConstantTest.java"
    public class ConstantTest
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #5.#19 // java/lang/Object."<init>":()V
    #2 = Float 1.234f
    #3 = Fieldref #4.#20 // ConstantTest.testF:F
    #4 = Class #21 // ConstantTest
    #5 = Class #22 // java/lang/Object
    #6 = Utf8 testI
    #7 = Utf8 I
    #8 = Utf8 ConstantValue
    #9 = Integer 1
    #10 = Utf8 testF
    #11 = Utf8 F
    #12 = Utf8 <init>
    #13 = Utf8 ()V
    #14 = Utf8 Code
    #15 = Utf8 LineNumberTable
    #16 = Utf8 <clinit>
    #17 = Utf8 SourceFile
    #18 = Utf8 ConstantTest.java
    #19 = NameAndType #12:#13 // "<init>":()V
    #20 = NameAndType #10:#11 // testF:F
    #21 = Utf8 ConstantTest
    #22 = Utf8 java/lang/Object

2. 其他数据区

2.1. 直接内存

直接内存(Direct Memory)区域并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存会被频繁地使用,而且也可能导致OutOfMemoryError异常。本机直接内存显然不会受到Java堆大小的限制,但肯定还是会受到本机总内存大小以及处理器寻址控件的限制。

在JDK1.4中新引入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用操作,从而避免了Java堆和Native堆中来回复制数据的麻烦。

第12讲 | Java有几种文件拷贝方式?哪一种最高效? 这篇文章详细介绍了拷贝的实现机制以及NIO的特性,建议阅读以加深对DirectMemory的理解。

2.2. Code Cache等区域

JVM 本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT Compiler 在运行时对热点方法进行编译,就会将编译后的方法储存在 Code Cache 里面;GC 等功能需要运行在本地线程之中,类似部分都需要占用内存空间。这些是实现 JVM JIT 等功能的需要,但规范中并不涉及。


参考:

  1. 深入理解Java虚拟机(第2版)
  2. 第25讲 | 谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?

1. 基本介绍

1.1. 理解基本的Gradle

Gradle是一种构建工具,Gradle脚本不是像传统的xml文件那样,而是一种基于Groovy的动态DSL。

  • DSL: 指的是用于一个特定领域的语言(功能领域、业务领域),可以简单理解成就是一个配置文件,或者说是面向领域的某种配置文件,如Ant的build.xml某种意义上就是Ant的DSL文件。
  • Groovy语言: 是一种基于jvm的动态语言.

当我们把Gradle作为构建工具使用时,我们只需要掌握它的配置脚本的基本写法就OK了;而当我们需要对构建流程进行高度定制时,就务必要掌握Groovy等相关知识了。如果想开始创建自己的tasks和插件,那么你最好对Groovy有一个较深的理解。

1.2. 两个重要的概念

1.2.1. Project

每一个build.grade文件代表着一个project。每个project有至少一个tasks,一个apk文件的构建包含以下Task:Java源码编译、资源文件编译、Lint检查、打包以生成最终的apk文件等等。

每一次构建都是有至少一个project来完成,所以Android studio中的project和Gradle中的project不是一个概念但可以简单理解为:Android Studio中的一个Module即为Gradle中的一个Project。

1.2.2. Tasks

tasks在build.gradle中定义,当初始化构建进程时,gradle会基于build文件集合所有的project和tasks。一个tasks包含了一系列动作,然后它们将会按照顺序执行,一个动作就是一段被执行的代码,很像Java中的方法。

1.3. 构建的生命周期

初始化阶段:project实例在这儿创建,如果有多个模块,即有多个build.gradle文件,多个project将会被创建。

配置阶段:在该阶段,build.gradle脚本将会执行,为每个project创建和配置所有的tasks。

执行阶段:这一阶段,gradle会决定哪一个tasks会被执行,哪一个tasks会被执行完全依赖开始构建时传入的参数和当前所在的文件夹位置有关。

1.4. Gradle工作流程

gradle工作流程

首先是初始化阶段,就是执行settings.gradle。

Configration阶段的目标是解析每个project中的build.gradle,解析每个子目录中的build.gradle。在这两个阶段之间,我们可以加一些定制化的Hook。这当然是通过API来添加的。

Configuration阶段完了后,整个build的project以及内部的Task关系就确定了。前面说过一个Project包含很多Task,每个Task之间有依赖关系。Configuration会建立一个有向图来描述Task之间的依赖关系。所以我们可以添加一个HOOK,即当Task关系图建立好后,执行一些操作,另外不包含依赖的Tasks总是优先执行。

最后一个阶段就是执行任务了,当然任务执行完后,我们还可以加Hook。

1.5. 插件

思考下为什么能在build.gradle中使用android标签?

因为应用”com.android.application”这个插件来构建app模块,如下所示:

1
apply plugin:'com.android.application'

整个插件中定义了如下4个顶级任务

  1. assemble: 构建项目的输出(apk)
  2. check: 进行校验工作
  3. build: 执行assemble任务与check任务
  4. clean: 清除项目的输出

还有一个插件是library,但不能同时使用他们2个,否则导致构建失败,一个模块要么使用Android application或者Android library插件,而不是二者。

1
apply plugin:'com.android.library'

1.6. 使用Gradle Wrapper

grade只是一个构建工具,而新版本总是在更迭,所以使用Gradle Wrapper将会是一个好的选择去避免由于gradle版本更新导致的问题。

利用命令行./gradlew -v来查看当前gradle版本。

  • gradlew是gradle wrapper的缩写,也就是说它对gradle的命令进行了包装
  • 解压gradle-wrapper.jar看看

在gradle-wrapper.properties中可以改变该url来改变你的gradle版本,如导入github项目跑不起来,可以尝试修改url。参考

2. 基本配置

2.1. 根目录的build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects {
repositories {
jcenter()
}
}

buildscript方法是定义了全局的相关属性,repositories定义了jcenter作为仓库,一个仓库代表着你的依赖包的来源。

allprojects方法可以用来定义各个模块的默认属性,你可以不仅仅局限于默认的配置,未来你可以自己创造tasks在allprojects方法体内,这些tasks将会在所有模块中可见。

2.2. 模块内的build.gradle

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
//加载用于构建Android项目的插件
apply plugin: 'com.android.application'

android { //构建Android项目使用的配置
compileSdkVersion 23 //指定编译项目时使用的SDK版本
buildToolsVersion "23.0.1" //指定构建工具的版本

defaultConfig {
applicationId "com.absfree.debugframwork" //包名
minSdkVersion 15 //指定支持的最小SDK版本
targetSdkVersion 23 //针对的目标SDK版本
versionCode 1
versionName "1.0"
}
buildTypes { //针对不同的构建版本进行一些设置
release { //对release版本进行的设置
minifyEnabled false //是否开启混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //指定混淆文件的位置
}
}
}

dependencies { //指定当前模块的依赖
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
}

属性applicationId复写了AndroidManifest文件中的包名packagename,但是关于applicationId和package name有一些不同。package name在AndroidManifest.xml有两个作用:其作为一个app的唯一标示,并且其被用在了R资源文件的包名。

buildTypes方法定义了如何构建不同版本的app。参考

2.3. 依赖管理

依赖模块作为gradle默认的属性之一(这也是为什么其放在了Android的外面),为你的app定义了所有的依赖包。

2.3.1. 依赖第三方库

1
2
3
4
dependencies {
compile 'com.google.code.gson:gson:2.3'
compile 'com.squareup.retrofit:retrofit:1.9.0'
}

一个依赖需要定义三个元素:group,name和version。

  • group意味着创建该library的组织名,通常这会是包名
  • name是该library的唯一标示
  • version是该library的版本号

2.3.2. 依赖本地jar包

默认情况下,我们依赖了所有在libs文件下的jar文件,同时包含了AppCompat这个aar文件。

2.3.3. 依赖其它模块

compile project(‘:other’)

2.3.4. 动态版本

在一些情形中,在构建你的app或者library的时候通过动态版本能使用到最新的依赖包。

1
2
3
4
5
dependencies {
compile 'com.android.support:support-v4:22.2.+'//我们告诉gradle,得到最新的生产版本。
compile 'com.android.support:appcompat-v7:22.2+'//我们告诉gradle,我们想得到最新的minor版本,并且其最小的版本号是2。
compile 'com.android.support:recyclerview-v7:+'//我们告诉gradle,得到最新的library。
}

你应该小心去使用动态版本,如果当你允许gradle去挑选最新版本,可能导致挑选的依赖版本并不是稳定版,这将会对构建产生很多问题,更糟糕的是你可能在你的服务器和私人pc上得到不同的依赖版本,这直接导致你的应用不同步。

如果你在你的build.gradle中使用了动态版本,Android studio将会警告你关于动态版本的潜在问题。

关于版本号的介绍

  • Apache组织的目标是将APR独立出来形成单独的第三方库,因此对其而言稳定的API接口就成为一个非常重要的必须考虑的方面。不过由于APR需要不断的往前方展,因此API接口的变化又是必然的趋势,因此如何平衡稳定性和变化性是APR开发者面临的一个极需解决的问题。为此APR采用了严格的版本规则来实现这一点。用户只需要简单的判断APR版本号,就可以很容易确定当前版本的兼容性:向前兼容、向后兼容还是前后同时兼容。

  • APR中使用三个整数来记录APR版本号:MAJOR.MINOR.PATCH。MAJOR表示当前APR的主版本号,它的变化通常意味着APR的巨大的变化,比如体系结构的重新设计,API的重新设计等等,而且这种变化通常会导致APR版本的向前不兼容。

  • MINOR称之为APR的次版本号,它通常只反映了一些较大的更改,比如APR的API的增加等等,但是这些更改并不影响与旧版本源代码和二进制代码之间的兼容性。

  • PATCH通常称之为补丁版本,通常情况下如果只是对APR函数的修改而不影响API接口的话都会导致PATCH的变化。

2.4. 全局设置

更好的做法是你在全局的gradle文件中定义一些属性,然后在模块中运用它们。

1
2
3
4
5
6
7
8
9
ext {
compileSdkVersion = 22
buildToolsVersion = "22.0.1"
}

android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
}

2.5. aar文件

如果你想分享一个library,该依赖包使用了Android api,或者包含了Android资源文件,那么aar文件适合你。

应用工程和依赖工程的区别在于输出文件,应用工程会生成APK文件,并且其可以安装在Android设备上,而依赖工程会生成.aar文件,该文件可以被Android应用工程当做依赖来使用。

3. 构建变体

3.1. 构建版本

3.1.1. 创建自己的构建版本

1
2
3
4
5
6
staging.initWith(buildTypes.debug)
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
debuggable = false
}

initWith()方法创建了一个新版本的同时,复制所有存在的构建版本,类似继承。我们也可以复写该存在版本的所有属性。

3.1.2. Source sets

nothing to show

3.2. product flavors

和构建版本不同,product flavors用来为一个app创建不同版本。典型的例子是,一个app有付费和免费版。

如果你不确定是否需要一个新的构建版本或者product flavors,你应该问你自己,你是否需要内部使用和外部使用的apk。如果你需要一个完全新的app去发布,和之前的版本完全隔离开,那么你需要product flavors,否则你只是需要构建版本。

创建product flavors

1
2
3
4
5
6
7
8
9
10
11
12
13
android {
productFlavors {
red {
applicationId 'com.gradleforandroid.red'
versionCode 3
}
blue {
applicationId 'com.gradleforandroid.blue'
minSdkVersion 14
versionCode 4
}
}
}

4. Groovy入门介绍

4.1. 前提知识

Groovy语句可以不用分号结尾。

Groovy中支持动态类型,即定义变量的时候可以不指定其类型。变量定义可以使用关键字def,注意虽然def不是必须的,但是为了代码清晰,建议还是使用def关键字。

1
2
3
def variable1 = 1   //可以不使用分号结尾  
def varable2 = "I ama person"
def int x = 1 //变量定义时,也可以直接指定类型

函数定义时,参数的类型也可以不指定。比如:

1
2
3
String testFunction(arg1,arg2){//无需指定参数类型  
...
}

除了变量定义可以不指定类型外,Groovy中函数的返回值也可以是无类型的。比如:

1
2
3
4
5
6
7
8
9
10
11

//无类型的函数定义,必须使用def关键字
def nonReturnTypeFunc(){
last_line //最后一行代码的执行结果就是本函数的返回值
}

//如果指定了函数返回类型,则可不必加def关键字来定义函数
String getString(){
return"I am a string"
}

Groovy对字符串支持相当强大,充分吸收了一些脚本语言的优点。

单引号’’中的内容严格对应Java中的String,不对$符号进行转义:

1
defsingleQuote='I am $ dolloar'  //输出就是I am $ dolloar 

双引号””的内容则和脚本语言的处理有点像,如果字符中有$号的话,则它会$表达式先求值:

1
2
3
4
5
6
7
8
9
10
11
12
defdoubleQuoteWithoutDollar = "I am one dollar" //输出 I am one dollar  
def x = 1
defdoubleQuoteWithDollar = "I am $x dolloar" //输出I am 1 dolloar
```

三个引号'''xxx'''中的字符串支持随意换行 比如

```groovy
defmultieLines = ''' begin
line 1
line 2
end '''

最后,除了每行代码不用加分号外,Groovy中函数调用的时候还可以不加括号。比如:

1
println("test") ---> println"test"  

注意,虽然写代码的时候,对于函数调用可以不带括号,但是Groovy经常把属性和函数调用混淆。比如:

1
2
3
def getSomething(){  
"hello"
}

调用函数要不要带括号,我个人意见是如果这个函数是Groovy API或者Gradle API中比较常用的,比如println,就可以不带括号,否则还是带括号。

4.2. Collections

List:链表,其底层对应Java中的List接口,一般用ArrayList作为真正的实现类。

Map:键-值表,其底层对应Java中的LinkedHashMap。

Range:范围,它其实是List的一种拓展。

4.3. 方法

4.3.1. 闭包

闭包,英文叫Closure,是Groovy中非常重要的一个数据类型或者说一种概念了。

闭包,是一种数据类型,它代表了一段可执行的代码。其外形如下:

1
2
3
4
5
def aClosure = {//闭包是一段代码,所以需要用花括号括起来..  
String param1, int param2 -> //这个箭头很关键。箭头前面是参数定义,箭头后面是代码
println"this is code" //这是代码,最后一句是返回值,
//也可以使用return,和Groovy中普通函数一样
}

如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫it,和this的作用类似。it代表闭包的参数。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'

等同于:

def greeting = { it -> "Hello, $it!"}
assert greeting('Patrick') == 'Hello, Patrick!'

但是,如果在闭包定义时,采用下面这种写法,则表示闭包没有参数!

def noParamClosure = { -> true }
这个时候,我们就不能给noParamClosure传参数了!
noParamClosure ("test") <==报错喔!

Closure使用中的注意如下两点

  1. 省略圆括号
1
public static <T> List<T>each(List<T> self, Closure closure)

上面这个函数表示针对List的每一个元素都会调用closure做一些处理。这里的closure,就有点回调函数的感觉。但是在使用这个each函数的时候,我们传递一个怎样的Closure进去呢?比如:

1
2
3
4
def iamList = [1,2,3,4,5]  //定义一个List
iamList.each{ //调用它的each,这段代码的格式看不懂了吧?each是个函数,圆括号去哪了?
println it
}

上面代码有两个知识点:

each函数调用的圆括号不见了! 原来,Groovy中,当函数的最后一个参数是闭包的话,可以省略圆括号。比如

1
2
3
4
def testClosure(int a1,String b1, Closure closure){
//dosomething
closure() //调用闭包
}

那么调用的时候,就可以免括号!

1
2
3
testClosure (4, "test", {
println"i am in closure"
} ) //圆括号可以不写
  1. 如何确定Closure的参数
1
2
3
4
5
6
public static <T> List<T> each(List<T>self, Closure closure)

def iamList = [1,2,3,4,5] //定义一个List变量
iamList.each{ //调用它的each函数,只要传入一个Closure就可以了。
println it
}

对于each所需要的Closure,它的参数是什么?有多少个参数?返回值是什么?

Closure虽然很方便,但是它一定会和使用它的上下文有极强的关联。要不作为类似回调这样的东西,我如何知道调用者传递什么参数给Closure呢?只能通过查询API文档才能了解上下文语义

groovy-each

5. 其他实践

5.1. 加速模块构建

https://developer.android.com/studio/build/optimize-your-build.html

5.2. 集成签名配置

nothing to show

1. 实现机制

多态靠的是父类的引用变量可以指向子类的具体实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

2. 多态的前提

必须是类与类之间的关系。要么继承,要么实现。

通常还有一个前提就是:覆盖。

3. 多态的利弊

  • 多态的出现大大的提高了程序的扩展性。
  • 提高了扩展性的同时,只能使用父类的引用访问父类中的成员。
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
abstract class Animal {
abstract void eat();
}

class Activities {
void doActivity(Animal a) {
if (a != null) {
a.eat();
}
if (a instanceof Cat) {
((Cat) a).catchMouse();
} else if (a instanceof Dog) {
((Dog) a).kanJia();
} else if (a instanceof Pig) {
((Pig) a).sleep();
}
}
}

class Cat extends Animal {
@Override
void eat() {
System.out.println("吃鱼");
}

void catchMouse() {
System.out.println("抓老鼠");
}
}

class Dog extends Animal {
@Override
void eat() {
System.out.println("吃骨头");
}

void kanJia() {
System.out.println("看家");
}
}

class Pig extends Animal {
@Override
void eat() {
System.out.println("吃饲料");
}

void sleep() {
System.out.println("老是睡觉");
}
}

public class DuoTaiDemo1 {
public static void main(String[] args) {

// new Activities().doActivity(null);

Animal a = new Cat();//类型提升,向上转型
a.eat();
//如果想要调用猫的特有方法,如何操作??
//强制将父类的应用转成子类类型,向下转型
Cat c = (Cat)a;
c.catchMouse();

/**
* 注意:前往不能将父类对象转成子类类型
* Animal a = new Animal();
* Cat c = (Cat)a;
* 我们能转换的是父类引用执行了自己子类对象是,该应用可以被提升
* 多态自始至终都是子类对象在做着变化
*/

//下面代码是利用Activities类创建对象完成对Cat,Dog,Pig进行的活动
new Activities().doActivity(new Cat());
new Activities().doActivity(new Dog());
new Activities().doActivity(new Pig());
}
}
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 需求:电脑运行实例,电脑运行基于主板,提供扩展功能
*
* @author LeeYou
*
*/

interface PCI {
public void open();

public void close();
}

/**
* 主板,使用PCI接口
*
*/
class MainBoard {
public void run() {
System.out.println("mainBoard run...");
}

public void userPCI(PCI p) {
if (p != null) {
p.open();
p.close();
}
}
}

/**
* 网卡扩展类,实现PCI接口
*
*/
class NetCard implements PCI {

@Override
public void open() {
System.out.println("netcard open...");
}

@Override
public void close() {
System.out.println("netcard close...");
}

}

/**
* 声卡扩展类,实现PCI接口
*
*/
class SoundCard implements PCI {

@Override
public void open() {
System.out.println("soundcard open...");
}

@Override
public void close() {
System.out.println("soundcard close...");
}

}

class DuoTaiDemo3 {
public static void main(String[] args) {
MainBoard mb = new MainBoard();
mb.run();
mb.userPCI(new SoundCard());
mb.userPCI(new NetCard());
}
}

4. 多态的注意事项

4.1. 在多态中成员函数的特点

  • 在编译时期:参阅引用型变量所属的类是否有调用的方法。如果有,编译通过;如果没有,编译失败。
  • 在运行时期:参阅对象所属的类中是否有调用的方法。

4.2. 在多态中,成员变量的特点(常用于面试)

无论编译和运行,都参考左边(引用型变量所属的类)

4.3. 在多态中,静态成员函数的特点(常用于面试)

无论在编译和运行,都参考左边(引用型变量所属的类)

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
package com.leeeyou.test;

class Father {
int num = 10;
static String name = "Father Class";

void method1() {
System.out.println("Father method 1");
}

void method2() {
System.out.println("Father method 2");
}

static void method4() {
System.out.println("Father method 4");
}
}

class Son extends Father {
int num = 20;
static String name = "Son Class";

void method1() {
System.out.println("Son method 1");
}

void method3() {
System.out.println("Son method 3");
}

static void method4() {
System.out.println("Son method 4");
}
}

class DuoTaiDemo2 {
public static void main(String[] args) {
//演示6.1
Father f = new Son();
f.method1();
f.method2();//对应6.1.2
//f.method3(); //对应6.1.1,这一行会报错

//演示6.2
System.out.println(f.num);
System.out.println(f.name);

//演示6.3
f.method4();
}
}

运行结果

1. 成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Outer{
private int age = 99;
String name = "Coco";
public class Inner{
String name = "Jayden";
public void show(){
System.out.println(Outer.this.name);
System.out.println(name);
System.out.println(age);
}
}
public Inner getInnerClass(){
return new Inner();
}
public static void main(String[] args){
Outer o = new Outer();
Inner in = o.new Inner();
in.show();
}
}

Inner 类定义在 Outer 类的内部,相当于 Outer 类的一个成员变量的位置,Inner 类可以使用任意访问控制符,如 public 、 protected 、 private 等。

Inner 类中定义的 show() 方法可以直接访问 Outer 类中的数据,而不受访问控制符的影响,如直接访问 Outer 类中的私有属性age。

定义了成员内部类后,必须使用外部类对象来创建内部类对象,而不能直接去 new 一个内部类对象,即:内部类 对象名 = 外部类对象.new 内部类( ); 。

编译上面的程序后,会发现产生了两个 .class 文件: Outer.class,Outer$Inner.class{}

成员内部类中不能存在任何 static 的变量和方法,可以定义常量: 1. 因为非静态内部类是要依赖于外部类的实例,而静态变量和方法是不依赖于对象的,仅与类相关,简而言之:在加载静态域时,根本没有外部类,所在在非静态内部类中不能定义静态域或方法,编译不通过;非静态内部类的作用域是实例级别 2. 常量是在编译器就确定的,放到所谓的常量池了。静态内部类可以有静态成员(方法,属性),而非静态内部类则不能有静态成员(方法,属性)。

友情提示:1.外部类是不能直接使用内部类的成员和方法的,可先创建内部类的对象,然后通过内部类的对象来访问其成员变量和方法;2.如果外部类和内部类具有相同的成员变量或方法,内部类默认访问自己的成员变量或方法,如果要访问外部类的成员变量,可以使用 this 关键字,如:Outer.this.name。

2. 静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Outer{
private int age = 99;
static String name = "Coco";
public static class Inner{
String name = "Jayden";
public void show(){
System.out.println(Outer.name);
System.out.println(name);
}
}
public static void main(String[] args){
Inner i = new Inner();
i.show();
}
}

静态内部类不能直接访问外部类的非静态成员,但可以通过 new 外部类().成员 的方式访问。如果外部类的静态成员与内部类的成员名称相同,可通过“类名.静态成员”访问外部类的静态成员;如果外部类的静态成员与内部类的成员名称不相同,则可通过“成员名”直接调用外部类的静态成员。

创建静态内部类的对象时,不需要外部类的对象,可以直接创建 内部类 对象名 = new 内部类();

3. 方法内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
使用的形参为何要为 final???在内部类中的属性和外部方法的参数两者从外表上看是同一个东西,但实际上却不是,所以他们两者是可以任意变化的,也就是说在内部类中我对属性的改变并不会影响到外部的形参,而然这从程序员的角度来看这是不可行的,毕竟站在程序的角度来看这两个根本就是同一个,如果内部类该变了,而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用 final 来避免形参的不改变
*/
public class Outer{
public void Show(){
final int a = 25;
int b = 13;
class Inner{
int c = 2;
public void print(){
System.out.println("访问外部类:" + a);
System.out.println("访问内部类:" + c);
}
}
Inner i = new Inner();
i.print();
}
public static void main(String[] args){
Outer o = new Outer();
o.show();
}
}

局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的。只能访问方法中定义的 final 类型的局部变量,当方法被调用运行完毕之后,局部变量就已消亡了。但内部类对象可能还存在,直到没有被引用时才会消亡。此时就会出现一种情况,就是内部类要访问一个不存在的局部变量;而使用final修饰符不仅会保持对象的引用不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期。局部内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数;防止被篡改数据,而导致内部类得到的值不一致。

4. 匿名内部类

使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口;同时它没有class关键字,这是因为匿名内部类是直接使用 new 来生成一个对象的引用,当然这个引用是隐式的。

对于匿名内部类的使用存在一个缺陷,即它仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,该类的定义会立即消失,所以匿名内部类是不能够被重复使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OuterClass {
public InnerClass getInnerClass(final int num,String str2){
return new InnerClass(){
int number = num + 3;
public int getNumber(){
return number;
}
}; /* 注意:分号不能省 */
}
public static void main(String[] args) {
OuterClass out = new OuterClass();
InnerClass inner = out.getInnerClass(2, "chenssy");
System.out.println(inner.getNumber());
}
}

interface InnerClass {
int getNumber();
}

4.1. 注意事项

1 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。
2 匿名内部类中是不能定义构造函数的。
3 匿名内部类中不能存在任何的静态成员变量和静态方法。
4 匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
5 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

4.2. 使用的形参为什么要final修饰

在内部类中的属性和外部方法的参数两者从外表上看是同一个东西,但实际上却不是,内部类并不是直接调用方法传递的参数,而是利用自身的构造器对传入的参数进行备份,自己内部方法调用的实际上时自己的属性而不是外部方法传递进来的参数,所以他们两者是可以任意变化的

也就是说在内部类中对属性的改变并不会影响到外部的形参,然而这从程序员的角度来看这是不可行的,毕竟站在程序的角度来看这两个根本就是同一个,如果内部类该变了而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用final来避免形参的不改变。

简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。故如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。

Android 程序的大多数代码操作都必须执行在主线程,例如:系统事件(例如设备屏幕发生旋转),输入事件(例如用户点击滑动等),程序回调服务,UI 绘制以及闹钟事件等等。那么我们在上述事件或者方法中插入的代码也将执行在主线程

一旦我们在主线程里面添加了操作复杂的代码,这些代码就很可能阻碍主线程去响应点击/滑动事件,阻碍主线程的 UI 绘制等等。

我们知道,为了让屏幕的刷新帧率达到 60fps,我们需要确保 16ms 内完成单次刷新的操作。一旦我们在主线程里面执行的任务过于繁重就可能导致接收到刷新信号的时候因为资源被占用而无法完成这次刷新操作,这样就会产生掉帧的现象,刷新帧率自然也就跟着下降了(一旦刷新帧率降到 20fps 左右,用户就可以明显感知到卡顿不流畅了)。

1. Android 系统为我们提供的若干组工具类

1.1. AsyncTask

为 UI 线程与工作线程之间进行快速的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的使用场景。

默认情况下,所有的 AsyncTask 任务都是被线性调度执行的,他们处在同一个任务队列当中,按顺序逐个执行。假设你按照顺序启动20个 AsyncTask,一旦其中的某个 AsyncTask 执行时间过长,队列中的其他剩余 AsyncTask 都处于阻塞状态,必须等到该任务执行完毕之后才能够有机会执行下一个任务。

如何才能够真正的取消一个 AsyncTask 的执行呢?我们知道 AsyncTaks 有提供 cancel()的方法,但是这个方法实际上做了什么事情呢?线程本身并不具备中止正在执行的代码的能力,为了能够让一个线程更早的被销毁,我们需要在 doInBackground()的代码中不断的添加程序是否被中止的判断逻辑,一旦任务被成功中止,AsyncTask 就不会继续调用 onPostExecute(),而是通过调用 onCancelled()的回调方法反馈任务执行取消的结果。我们可以根据任务回调到哪个方法(是 onPostExecute 还是 onCancelled)来决定是对 UI 进行正常的更新还是把对应的任务所占用的内存进行销毁等。

使用 AsyncTask 很容易导致内存泄漏,一旦把 AsyncTask 写成 Activity 的内部类的形式就很容易因为 AsyncTask 生命周期的不确定而导致 Activity 发生泄漏。

1.2. HandlerThread

为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。

HandlerThread 比较合适处理那些在工作线程执行,需要花费时间偏长的任务。我们只需要把任务发送给 HandlerThread,然后就只需要等待任务执行结束的时候通知返回到主线程就好了。

另外很重要的一点是,一旦我们使用了 HandlerThread,需要特别注意给 HandlerThread 设置不同的线程优先级,CPU 会根据设置的不同线程优先级对所有的线程进行调度优化。

1.3. IntentService

适合于执行由 UI 触发的后台 Service 任务,并可以把后台任务执行的情况通过一定的机制反馈给 UI。

首先,因为 IntentService 内置的是 HandlerThread 作为异步线程,所以每一个交给 IntentService 的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。

其次,通常使用到 IntentService 的时候,我们会结合使用 BroadcastReceiver 把工作线程的任务执行结果返回给主 UI 线程。使用广播容易引起性能问题,我们可以使用 LocalBroadcastManager 来发送在程序内部传递的广播,从而提升广播的性能。我们也可以使用 runOnUiThread() 快速回调到主 UI 线程。

最后,包含正在运行的 IntentService 的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的

IntentService 继承自普通 Service 同时又在内部创建了一个 HandlerThread,在 onHandlerIntent()的回调里面处理扔到 IntentService 的任务。所以 IntentService 就不仅仅具备了异步线程的特性,还同时保留了 Service 不受主页面生命周期影响的特点。

1.4. ThreadPool

把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。

1.4.1. 线程池的基本概念

ThreadPoolExecutor有四个重载的构造方法

corePoolSize:线程池中核心线程的数量

maximumPoolSize:线程池中最大线程数量

keepAliveTime:非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长

unit:第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等

workQueue:线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。

  • workQueue是一个BlockingQueue类型,它是一个特殊的队列,当我们从BlockingQueue中取数据时,如果BlockingQueue是空的,则取数据的操作会进入到阻塞状态,当BlockingQueue中有了新数据时,这个取数据的操作又会被重新唤醒。同理,如果BlockingQueue中的数据已经满了,往BlockingQueue中存数据的操作又会进入阻塞状态,直到BlockingQueue中又有新的空间,存数据的操作又会被冲洗唤醒。

  • 1.ArrayBlockingQueue 这个表示一个规定了大小的BlockingQueue,ArrayBlockingQueue的构造函数接受一个int类型的数据,该数据表示BlockingQueue的大小,存储在ArrayBlockingQueue中的元素按照FIFO(先进先出)的方式来进行存取。

  • 2.LinkedBlockingQueue 这个表示一个大小不确定的BlockingQueue,在LinkedBlockingQueue的构造方法中可以传一个int类型的数据,这样创建出来的LinkedBlockingQueue是有大小的,也可以不传,不传的话,LinkedBlockingQueue的大小就为Integer.MAX_VALUE

  • 3.PriorityBlockingQueue 这个队列和LinkedBlockingQueue类似,不同的是PriorityBlockingQueue中的元素不是按照FIFO来排序的,而是按照元素的Comparator来决定存取顺序的(这个功能也反映了存入PriorityBlockingQueue中的数据必须实现了Comparator接口)。

  • 4.SynchronousQueue 这个是同步Queue,属于线程安全的BlockingQueue的一种,在SynchronousQueue中,生产者线程的插入操作必须要等待消费者线程的移除操作,Synchronous内部没有数据缓存空间,因此我们无法对SynchronousQueue进行读取或者遍历其中的数据,元素只有在你试图取走的时候才有可能存在。我们可以理解为生产者和消费者互相等待,等到对方之后然后再一起离开。

threadFactory :为线程池提供创建新线程的功能,这个我们一般使用默认即可

handler:拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的)默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。

1.4.2. 线程池的运行规则

  • 1.execute一个线程之后,如果线程池中的线程数未达到核心线程数,则会立马启用一个核心线程去执行
  • 2.execute一个线程之后,如果线程池中的线程数已经达到核心线程数,且workQueue未满,则将新线程放workQueue中等待执行
  • 3.execute一个线程之后,如果线程池中的线程数已经达到核心线程数但未超过非核心线程数,且wrkQueue已满,则开启一个非核心线程来执行任务
  • 4.execute一个线程之后,如果线程池中的线程数已经超过非核心线程数,则拒绝执行该任

1.4.3. ThreadPool的使用

参考leeyou.xyz

1.4.4. 线程池其他常用功能

  • shutDown() 关闭线程池,不影响已经提交的任务
  • shutDownNow() 关闭线程池,并尝试去终止正在执行的线程
  • allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收
  • submit 一般情况下我们使用execute来提交任务,但是有时候可能也会用到submit,使用submit的好处是submit有返回值

1.4.5. 使用时要注意的几点

使用线程池需要特别注意同时并发线程数量的控制,理论上来说,我们可以设置任意你想要的并发数量,但是这样做非常的不好。因为 CPU 只能同时执行固定数量的线程数,一旦同时并发的线程数量超过 CPU 能够同时执行的阈值,CPU 就需要花费精力来判断到底哪些线程的优先级比较高,需要在不同的线程之间进行调度切换。

一旦同时并发的线程数量达到一定的量级,这个时候 CPU 在不同线程之间进行调度的时间就可能过长,反而导致性能严重下降。另外需要关注的一点是,每开一个新的线程,都会耗费至少 64K+ 的内存。为了能够方便的对线程数量进行控制,ThreadPoolExecutor 为我们提供了初始化的并发线程数量,以及最大的并发数量进行设置。

另外需要关注的一个问题是:Runtime.getRuntime().availableProcesser()方法并不可靠,他返回的值并不是真实的 CPU 核心数,因为 CPU 会在某些情况下选择对部分核心进行睡眠处理,在这种情况下,返回的数量就只能是激活的 CPU 核心数。

2. Android中的任务线程模型

Looper:能够确保线程持续存活并且可以不断的从任务队列中获取任务并进行执行。

Handler:能够帮助实现队列任务的管理,不仅仅能够把任务插入到队列的头部,尾部,还可以按照一定的时间延迟来确保任务从队列中能够来得及被取消掉。

MessageQueue:使用 Intent,Message,Runnable 作为任务的载体在不同的线程之间进行传递。

三个组件打包到一起进行协作,这就是 HandlerThread

HandlerThread

3. 平衡并发的线程数和内存消耗的问题

多线程并发访问同一块内存区域有可能带来很多问题,例如读写的权限争夺问题,ABA 问题等等。为了解决这些问题,我们会需要引入锁的概念。

3.1. Android中多线程引起的问题

Android UI 对象的创建,更新,销毁等等操作都默认是执行在主线程,但是如果我们在非主线程对UI对象进行操作,程序将可能出现异常甚至是崩溃。

在非 UI 线程中直接持有 UI 对象的引用也很可能出现问题。例如Work线程中持有某个 UI 对象的引用,在 Work 线程执行完毕之前,UI 对象在主线程中被从 ViewHierarchy 中移除了,这个时候 UI 对象的任何属性都已经不再可用了,另外对这个 UI 对象的更新操作也都没有任何意义了,因为它已经从 ViewHierarchy 中被移除,不再绘制到画面上了。

View 对象本身对所属的 Activity 是有引用关系的,如果工作线程持续保有 View 的引用,这就可能导致 Activity 无法完全释放。除了直接显式的引用关系可能导致内存泄露之外,我们还需要特别留意隐式的引用关系也可能导致泄露。例如通常我们会看到在 Activity 里面定义的一个 AsyncTask,这种类型的 AsyncTask 与外部的 Activity 是存在隐式引用关系的,只要 Task 没有结束,引用关系就会一直存在,这很容易导致 Activity 的泄漏。更糟糕的情况是,它不仅仅发生了内存泄漏,还可能导致程序异常或者崩溃。

我们需要谨记的原则就是:不要在任何非 UI 线程里面去持有 UI 对象的引用。

系统为了确保所有的 UI 对象都只会被 UI 线程所进行创建,更新,销毁的操作,特地设计了对应的工作机制(当 Activity 被销毁的时候,由该 Activity 所触发的非 UI 线程都将无法对UI对象进行操作,否者就会抛出程序执行异常的错误)来防止 UI 对象被错误的使用。

4. Loaders

当启动工作线程的 Activity 被销毁的时候,我们应该做点什么呢?

为了方便的控制工作线程的启动与结束,Android 为我们引入了 Loader 来解决这个问题。
我们知道 Activity 有可能因为用户的主动切换而频繁的被创建与销毁,也有可能是因为类似屏幕发生旋转等被动原因而销毁再重建。在 Activity 不停的创建与销毁的过程当中,很有可能因为工作线程持有 Activity 的 View 而导致内存泄漏(因为工作线程很可能持有 View 的强引用,另外工作线程的生命周期还无法保证和 Activity 的生命周期一致,这样就容易发生内存泄漏了)。除了可能引起内存泄漏之外,在 Activity 被销毁之后,工作线程还继续更新视图是没有意义的,因为此时视图已经不在界面上显示了。

Loaders

Loader 的出现就是为了确保工作线程能够和 Activity 的生命周期保持一致

  • LoaderManager 会对查询的操作进行缓存,只要对应 Cursor 上的数据源没有发生变化,在配置信息发生改变的时候(例如屏幕的旋转),Loader 可以直接把缓存的数据回调到 onLoadFinished(),从而避免重新查询数据。另外系统会在 Loader 不再需要使用到的时候(例如使用 Back 按钮退出当前页面)回调 onLoaderReset()方法,我们可以在这里做数据的清除等等操作。

  • 在 Activity 或者 Fragment 中使用 Loader 可以方便的实现异步加载的框架,Loader 有诸多优点。但是实现 Loader 的这套代码还是稍微有点点复杂,Android 官方为我们提供了使用 Loader 的示例代码进行参考学习。

5. 线程优先级的重要性

Android 系统会根据当前运行的可见的程序和不可见的后台程序对线程进行归类,划分为 forground 的那部分线程会大致占用掉 CPU 的90%左右的时间片,background 的那部分线程就总共只能分享到5%-10%左右的时间片。之所以设计成这样是因为 forground 的程序本身的优先级就更高,理应得到更多的执行时间。

默认情况下,新创建的线程的优先级默认和创建它的母线程保持一致。如果主 UI 线程创建出了几十个工作线程,这些工作线程的优先级就默认和主线程保持一致了,为了不让新创建的工作线程和主线程抢占 CPU 资源,需要把这些线程的优先级进行降低处理,这样才能给帮助 CPU 识别主次,提高主线程所能得到的系统资源。

在 Android 系统里面,我们可以通过 android.os.Process.setThreadPriority(int) 设置线程的优先级,参数范围从-20到19,数值越小优先级越高。Android 系统还为我们提供了以下的一些预设值,我们可以通过给不同的工作线程设置不同数值的优先级来达到更细粒度的控制。

线程优先级

Android 系统里面的 AsyncTask 与 IntentService已经默认帮助我们设置线程的优先级,但是对于那些非官方提供的多线程工具类,我们需要特别留意根据需要自己手动来设置线程的优先级。

线程优先级

线程优先级

6. 工具篇

从 Android M 系统开始,系统更新了 GPU Profiling 的工具来帮助我们定位 UI 的渲染性能问题。早期的 CPU Profiling 工具只能粗略的显示出 Process,Execute,Update 三大步骤的时间耗费情况。但是仅仅显示三大步骤的时间耗费情况,还是不太能够清晰帮助我们定位具体的程序代码问题,所以在 Android M 版本开始,GPU Profiling 工具把渲染操作拆解成8个详细的步骤进行显示。

Sync & Upload:通常表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片本身的大小。

Measure & Layou:这里表示的是布局的 onMeasure 与 onLayout 所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题。

Animation:表示的是计算执行动画所需要花费的时间,包含的动画有 ObjectAnimator,ViewPropertyAnimator,Transition 等等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等。

Input Handling:表示的是系统处理输入事件所耗费的时间,粗略等于对于的事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作。

Misc/Vsync Delay:如果稍加注意,我们可以在开发应用的 Log 日志里面看到这样一行提示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。这意味着我们在主线程执行了太多的任务,导致 UI 渲染跟不上 vSync 的信号而出现掉帧的情况。

这里是将极客时间上杨晓峰老师的专栏文章,在这里整理出来方便自己后续复习用。文章分为5大模块:

  • Java 基础:我会围绕 Java 语言基本特性和机制,由点带面,让你构建牢固的 Java 技术工底。
  • Java 进阶:将围绕并发编程、Java 虚拟机等领域展开,助你攻坚大厂 Java 面试的核心阵地。
  • Java 应用开发扩展:从数据库编程、主流开源框架、分布式开发等,帮你掌握 Java 开发的十八般兵器。
  • Java 安全基础:让你理解常见的应用安全问题和处理方法,掌握如何写出符合大厂规范的安全代码。
  • Java 性能基础:你将掌握相关工具、方法论与基础实践。

对于Android开发人员来说,后面章节内容更多偏向后端开发,对于这部分内容,我自己是量力而行。很多知识点以前只是听说或是了解不透彻,比如CAS、AQS、内存模型、多线程并发等,这次在专栏中,收获满满;只要用心读,保持清晰的头脑,应该还会有进一步的提升。

技术之外,杨老师在文章中有意的提到了许多思想层面的内容,个人觉得对于意识的提升非常有帮助。比如说“你需要尽量表现出自己的思维深入并系统化”、“知其然并知其所以然,明白基本组成和机制”、“应用场景是我们在选择哪个API前需要考虑的,不能一概而论的说A比B绝对好”等等。当然意识层面的提高也绝非说说而已,还需结合实践和自己的思考不断摸索和总结。

在某些文章中,我添加了一些自己不太熟悉的内容补充,在相应的章节里面做好记录,也算是查漏补缺,消除自己的疑虑吧。

财务自由之路.jpg

“先完成财务保障,再实现财务安全,最后最后才 有点可能 实现财务自由。如若不遵循这一投资哲学,你将会处于危险之地,更别谈实现你的梦想了” , 这点对我而言可能是读这本书的最大收获了。

20180815135720.png

是的,正如上图展示的那样,只有当第一个水壶被住满水以后,你才能加注第二个水壶。你只能使用第二水壶盈余的部分(你不需要用来实现财务安全的金钱)来对第三个水壶进行加注。这样,你的财务安全就永远不会承担风险。而现在的我,解决财务保障可能是最最紧迫的事情了。

老实说,这本书的副标题 “7年内赚到你的第一个1000万” 有点夺眼球的噱头之意,鉴于作者的声望和MacTalk的推荐,读完以后,副标题带来的负面作用可以完全忽略不计。作者的一些思想或者说是一些行为准则,对我而言,有较大的启发;当然你也可以理解为鸡汤,但在我看来,确实值得干了它。比如作者系统的分析你的收入时,从能力、精力、影响力、自我评价和创意五个方面着手,并非空口说白话,而是给出具体某方面的指导和建议;再比如作者提到的责任、持续学习和成长、思维的转变这些无一不是当今竞争激烈的社会上,你我的立足之本。

我们真的可以想象下7年之后自己的财务状况,至少可以定下一个目标吧。达到财务安全线对我而言可能有点难度,但是你的思维方式铸就了你今天的样子,做出改变,持续不断地学习和成长,还是有很大机会搏一搏的,谁说不是呢?

金句摘录

1、如果你想获得不一样的结果,那你就必须有所行动:你必须改变自己的方式,首先需要改变的便是你的思维方式,你的思维方式铸了你今天的样子,这样的思维方式却并不能让你变成你想成为的样子。

2、你的思维方式铸就了你今天的样子,这样的思维方式却并不能让你变成你想成为的样子,如果你想获得不一样的结果,那你就必须有所行动:你必须改变自己的方式,首先需要改变的便是你的思维方式。

3、不管你相不相信,金钱确实改变了我生活中的许多东西,金钱不会解决你的所有问题,它也绝不是万能的,但是,缺钱却能使你的幸福蒙上一层阴影,有了金钱,你在处理问题的时候便能尝试许多方式,而且,你也将会有机会结识更多的人,参观风景优美的地方,得到更加有趣的工作,获得更多的自信,赢得更多的赞赏,获得更多的机会。

4、我们想要自己掌控将来事态的发展,还是让我们的错误及其后果掌握将来的事态?如果我们在事件发生时勇于承担责任,那么所有的负面情绪在这一刻都会失去掌控力。

5、只有你(绝非任何人)能对你7年内收获多少金钱负责,首先离开你的舒适环境;其次将困难当做成长的机会,并且问自己:我如何创造一种情况,是这个问题不再出现?接着提出正确的问题;最后扩展你的个人范畴,同时你的可控领域也得到了拓展,这世上有一些事你无能为力,但你可以决定,你打算如何判断以及如何对此作出反应,在这方面,你一直都是拥有权利的。

6、责任意味着:没有任何东西能改变你的态度或品格,因为你是按照自己的本性对事情做出反应,你可以决定自己以哪种方式去生活,这种态度是你能够生活幸福,并且成为最好的自己,如果你勇于承担责任,那么你就能在7年内变得富有。

7、随着年龄的增长,你只会对自己没有做过的事情感到后悔。

8、引发奇迹的4个法则,被我统称为持续不断地学习和成长,对我来说,持续不断地学习和成长已经成为我的人生信条,当我们不再成长,我们就与死亡无异了,成长就是生命,持续不断地学习和成长意味着,感受到自身源源不断的活力,也意味着你能成为最好的自己。

9、我们的自信心决定了我们是否敢于冒险,不去冒险便不可能获得成长。

10、大多数人高估了自己1年内能做到的事情,也低估了自己10年内能做到的事情。

11、我们不应该以他人来衡量自己,而应该与自己做纵向对比。

12、我们常常忘记,顺着我们目前为止走过的道路继续前行,同样有风险,这条路给我们一种熟悉感,但也不会更安全,生活就是一场比赛,不去冒险的人不可能获得成功,立即采取行动!因为你永远不能为成功做好完美的准备。

13、世上唯一从来不犯错的人,是从来不做任何事的人。

14、我们倾向于将自己无法理解的事物视为奇迹或好运,然而你也看到了,奇迹是可以创造的,就连好运也是多年准备的结果,最终还是在于我们的态度,态度决定我们是否将无法解释的奇迹定义为一种超自然的现象,决定我们是否将坏运气当作理由,答案如果是肯定的话,那么你就不再负有责任,你的坏运气也会成为一个很好的借口,然而,如果你主动承担责任,那么你的好运气就是可以期待的,每个人都会得到属于自己的机会,根本问题是:我们是否愿意对所有事情(包括奇迹和好运)承担责任?如果愿意承担责任,你将为自己创造的奇迹感到惊讶,如果拒绝承担责任,你就会声称“坏运气已经发生了”(但谁又能与之对抗呢),如果勇敢地承担责任,那么好运气就在你的安排计划之中。

15、知足常乐的人也是主动放弃的人,我们拥有什么样的期望,也就决定了我们能获得什么,渴望从生活中获取许多东西的人,生活会给予更多的回报,乔纳森.斯威夫特曾经说过一句讽刺的话:“不期望任何事的人是有福之人,因为他也不会失望”。

16、永远不让目标低于期望,永远不要劝自己说:你不“值得”拥有,你自己是可以决定你值得拥有什么的,你的期望决定你会获得什么,灯光下有你的一席之地。

17、永远不要用短期解决方案来应对长期问题。

18、每个人获得的东西都恰好是他值得获得的东西,虽然我总听到有人抱怨:“我远比我现在获得的东西有价值的多” 但这是错误的,正确的说法应该是:如果你“值得”更多,那么你早该获得了。

19、你必须了解市场法则,你收入的高低取决于此,你今天的收入就是你昨天所做之决定的结果,如果不理解这一点,你就不能说:“现在我要另做选择” 作为自己人生的设计师,你的收入或加薪是由你自己创造的,不是你被加薪,而是你主动获得加薪,如果别人能够决定你的收入高低,那么他们也就拥有了操纵你人生的权利,你(也只有你)能增加自己的收入,你个人对此负责,你个人就可以做出决定。

20、金钱和机遇并不会应需求而产生,而是应能力而产生,你不会因为自己需要更多金钱而获得加薪,你只会因为你所拥有的能力而获得加薪。

21、永远不要将你对自己的怀疑告知任何人,要展示自己的强项,人们不会追随一个自我怀疑的人,只会追随那些坚强不屈的、对目标坚定不移的人,你的强项往往会为你挣得更多的报酬。

22、如果说世上存在成功之终极秘密,那就是刻不容缓地去处理日常事务的能力,为自己确定一条指导原则:尽可能快地着手去做,不要害怕犯错误,IBM创始人说过:“在我的公司里,想要出成绩,就必须犯下双倍的错误” 犯错使人积累经验,经验帮助你快速做出正确决策。

23、如果想让事物想着利于你的方向发展,那么首先你自己必须变得更好。

24、如何分析你的收入之能力:你在自己的专业领域是一个什么水平?你有没有将自己定位为专家?你了解自己的专业领域吗?你是否已为自己建立起能提升你知识和能力的导师及专家团队?你在个人专业领域之外是否还继续深造?你的个人能力是否和你的专业技能共同成长?你了解成功的法则吗,是否运用了成功的法则?你具备领导资质吗?同你所在领域的优秀人士相比,你的能力如何?

25、如何分析你的收入之精力:你准备将多少精力投入专业技能的提升中去?你实际投入了多少精力?你还有多少能量?你能否将精力集中起来,全身心做某件事情?对于你现在所做的事情,你的热情和激情有多大?你热爱你的工作吗?从长远来看,你在将来需要更多的精力,你是否任然为自己的健康、运动、家庭和持续学习以及成长而投入时间?

26、如何分析你的收入之影响力/知名度:你是否知道这是你的收入板块中最重要的版块?影响力是最强大的乘数,你的产品/你的服务受众面有多广?知识、技术、产品在当今社会比比皆是,你能否利用你的产品挣到钱,取决于有多少人知道。

27、如何分析你的收入之自我评价:你是否知道感知即现实?你推销自己的技巧如何?你对自己的举止有多自信?你的自信有多强?你是否认为自己出类拔萃、鹤立鸡群?你是否能很好地表现自己?别人是否认为你是一名专家?是否有人因为你优秀而愿意无偿地为你服务?别人是否认为认识你是莫大的荣幸?你能准确定位自己吗?

28、如何分析你的收入之创意:你具有创造力吗?你是否易于接受新事物?你是否坚持自己的目标,同时准备不断尝试实现目标的新方法?你是否灵活变通?你会把自己的灵感马上写下来吗?你相信自己的灵感并付诸实施吗?你个人的创意工厂发展壮大了吗?你是否不断问自己,“这个创意如何才能切和我的情况”,“我如何才能快速采取行动”,你相信每一个你需要的信息和解决方法,都可以被找出来?你相信要得到这些信息,你就必须不断发展新的创意吗?

29、你的收入水平很大程度上取决于你能做那些别人不能做的事情,“尽快”这条法则同样适用于此,不要等到自己能力足够了才去做,尽快放权,放权那些别人都能做的事情,把空出来的时间集中用于收入丰厚的活动上,放权其他无用的事情,将时间用来定位自己,在同等时间内,只要你挣到的钱别付给助你做事的人要多,账单就永远不会来烦你。

30、借口是我们讲给自己听的谎言,我们应该自己对自己越来越诚实,承担越来越多的责任,许多人声称:“我没有时间去学习正确的投资方法” 是这样吗?这是真的吗?或者说这些人还可以对自己更诚实吗?“我还没有准备好为此投入更多的时间”,这样说会不会显得更诚实? 一位真正的人物不会将生活中的机会滥用在道歉和寻找借口上,借口使我们贫穷。

31、我们能否实现财务自由,对此起决定性作用的是,我们听从的是两个声音中的哪一个:受害者的声音还是勇于承担责任者的声音,永远不要让你内心中那个弱小的人战胜强大的那个人。

32、即使一直以来都没有什么意外发生,你还是应该坚持,因为有了这笔储备金,你才拥有安全感,安全感是每个人都需要的,当你面临困境时,有支持你的后盾,你才可以更好地解决困难,财务保障便是我们所说的后盾,没人能保证自己在意外和不幸来临时不受伤害,但是我们可以未雨绸缪,让我们在意外发生时,能够从容面对,如果意外来临时,我们还要面临财务危机,或者是因为财务问题而妥协,那才是真正的不幸。

33、财务保障是短期目标:这应当是你下一个目标,一个必须尽快完成的目标。

34、为你的强项找一位教练,为你的弱项找一个解决方案。

35、开始阶段简单的事情以后会变得困难,开始阶段困难的事情以后也会变得简单、储蓄并不难,但由于这对你而言是一项全新的开始,所以在开始阶段你应该会感到十分陌生,用储蓄明智地投资不是一件易事,但你会觉得相对简单,因为有很多容易上手的投资形式,因为你有投资顾问、内行的朋友和其他可供参考的例子。

36、第一项计划财务保障能给你带来很多的益处你可以安全地渡过一次危机,你会感到有安全感,可以时刻准备好面对一些未知的意外,但是财务保障有一个很大的缺点一旦意外情况发生,你会花光所有的积蓄,虽然你在财务上安然无恙地渡过了一次危机,但是你的积蓄都没了,真正保险的办法是,你需要积累足够的资金,使你可以靠利息生活。

37、7年后,你要么就根本没有改变你的财务状况,要么就至少部分实现了你的财务安全,7年之后开始的未来,正是你今天所准备的未来,有的人不去从事自己感兴趣的职业,主要原因就是缺钱,这令人感到惋惜,也是一种才能的浪费,这之所以尤其可悲,是因为我们只有在做自己喜爱的事情时才会真正感到幸福,一个从来没有长期做过使自己快乐的有意义的事情的人,从来不会知道,自身究竟蕴藏着多少潜力,如果有人无法走出关键性的一步,不能做自己感兴趣的工作,那么原因往往都在于金钱。

38、在你做出明确的决定之前,请不要继续读下去,这个决定应该包含你实际做的你所了解的必要之事,你知道,你首先必须着手改变信仰,你必须将巨大的痛苦与目标无法实现联系在一起,将巨大的快乐与目标得以实现联系在一起,你应该意识到为什么你一定要这样生活,记住你必须每天提升自己,全力以赴,你必须不断地学习和成长,你必须付出110%的努力,你必须全力以赴,成为最好的自己。你真的想要这样吗?你真的愿意为了财富和幸福去付出每个人都必须付出的代价吗?如果你决定好了这样做(我也想鼓励你这样做),你就负有责任了你应该向自己承诺,不到最好,永不满足。多年前,第一次听到这一理念时,我几乎不敢相信,但它确确实实是正确的,当你为自己定好一个目标,制订出书面的计划,你就已经成功50%了,以下也有4个足以证明这一点的重要原因:目标拓宽你的机会意识,目标为你指出解决问题的方向,目标是你为了“赢”而去比赛,你有了一个目标,一切都变得很重要。

39、他人可以短暂地阻止你,但只有你自己才可以永远地使自己停下来。

40、你自己决定受谁影响、向谁学习、学习什么:只模仿那些比你成功的人。

41、成功意味着获得你所爱的东西,而幸福意味着享受你获得的东西。

在耶鲁精进.jpg

对于我来说,这是一本信息量巨大的书。

我先试着整理出每个章节的摘录,然后找出关键词,再选择其中的某几个来展开,发现还有10多个,一时不知如何下笔。罢了,既然无从下笔,就写写最想说的吧。

首先是精进这个词,记得在罗振宇时间的朋友跨年晚会上也提过,大意是只有不停的迭代自己,成功的概率才能不断提高。这个惟一靠谱的人生策略,希望自己能铭记,不断的学习、总结、迭代、精进。

其次是通识教育,说实话之前一直没有通识教育的概念,以前在校读书是为了读而读,也没读出个什么花样来。知识结构过于片面,更不用谈触类旁通的智慧了。两年前自己有整理过专业方面的知识架构,目前也一直在完善它;但是通识这块可以说毫无准备,毫无积累,这可不是件好事。通识体系大致包括:文学、数学、科学、经济学、哲学、心理学等,多去了解,扩展自己的涉猎广度何乐而不为。

最后我想说说关于孩子的问题,吴军老师在如何给孩子最好的教育分享会中,讲到“如果有起跑线的话,父母的见识、格局,是孩子的第一起跑线”,父母是孩子的第一任老师,那么上面我们讲到的精进、迭代又或者是扩展自己的通识储备是不是等于在提高将来孩子的第一起跑线呢?此外,还有两点令我影响深刻,①小时候严格要求,长大尊重个人选择。顺序千万别反了。“你是愿意孩子小时候没有多少空间长大后有很多空间呢,还是小时候很多空间长大后没有空间?”什么使人幸福?有选择才有幸福。②让孩子知道生活不易,人生的错误要尽量犯在前面,犯在后面的错误你无法承受,少年则是最好的犯错时期,这时犯错的收益/风险比高到惊人,不犯错简直是虚度人生。将这种思想输送到孩子,不比什么十八线教育培训机构层次高太多?所以我特别特别认同“父母的见识、格局,是孩子的第一起跑线”。

为了利于重复翻阅,还是将摘录做成图片插入在下面:
序言

耶鲁故事

极简金融课

极简谈判课

在美国看美国

大学.问.答网友