虚拟机类加载机制

在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成。虚拟机在运行期间会把描述类的数据从Class文件中加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机知己网使用的Java类型,这就是虚拟机的类加载机制。

1、类加载过程

类从被加载到内存开始,到卸载出内存为止,其生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。

1.1 加载

虚拟机规范没有对类加载的时机做规定,但要求五种情况必须触发类初始化,而在类初始化之前必定会先被加载:

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

虚拟机需要完成三件事:

  1. 通过类得全限定名来获取定义此类额二进制字节流(可以从zip包读取,如JAR,WAR,可以从网络中获取,可以动态生成JSP,可以从数据库中读取)
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口

加载阶段,既可以使用系统提供的引导类加载器来完成,又可以使用用户自定义的类加载器完成。

1.2 验证

验证是连接的第一步,目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

验证阶段包括4部分:

  1. 文件格式校验,验证字节流是否符合Class文件格式规范,版本号是否可以被虚拟机接受等等
  2. 元数据校验,对字节码描述信息进行语义分析,以保证描述信息符合Java语言规范
  3. 字节码校验,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,保证程序方法在运行期间不会做出危害虚拟机的事
  4. 符号引用校验,在虚拟机将符号引用转化为直接引用时(引用类方法字段等)需要进行符号引用校验,可以看做是对类自身以外(常量池的各种符号引用)的信息进行匹配性校验,目的是确保解析动作能正常执行,如果无法通过符号引用验证则会抛出类似java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError等。

1.3 准备

准备阶段为类变量(不包括实例变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

此处所设置的类变量初始值,并不是程序代码中所设置的初始值,而是为类变量赋0值,例如int类型为0,boolean类型为false。根据程序代码设置初始值是在初始化阶段发生。

1.4 解析

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

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量(类接口,字段方法常量等),只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存。

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。

1.5 初始化

初始化是类加载过程的最后一步,从这里开始执行Java程序代码。在准备阶段变量已经赋值过一次系统要求的初始值,初始化阶段则是根据程序代码去初始化类变量,同一个类加载器下一个类只会被初始化一次。

2、类加载器

类加载器功能就是通过一个类的全限定名来获取描述此类的二进制字节流。Java允许程序自己去实现类加载器。当前自定义类加载器可以实现OSGI,热部署、代码加密等诸多功能。

Java中有两种类加载器,一种是启动类加载器(Bootstrap ClassLoader,使用C++实现)是虚拟机的一部分,另一种是其他类加载器,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。

  • 启动类加载器,负责将存放在\lib目录中的类库加载到虚拟机内存中,无法被程序调用,用户编写自定义类加载器时如需把加载请求委派给引导类加载器直接使用null代替。

  • 扩展类加载器(Extension ClassLoader),负责加载\lib\ext目录中或者被java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

  • 应用程序类加载器(Application ClassLoader),系统默认类加载器getSystemClassLoader()方法的返回值,负责加载用户(ClassPath)路径上所指定的类库,开发者可以直接使用这个类加载器

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

双亲委派模型有一个好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如,java.lang.Object,无论哪个类加载器加载最终都会交给最顶层的类加载器加载,因此Object在各种加载器环境中都是一个类(判断类是否相同,需要同一个类加载器加载的同一个Class文件,缺一不可)。

双亲委派模型并不是必须,当前很多JNDI,代码热部署,模块热部署的应用并不符合双亲委派模型原则的行为。

Class类文件结构分析

Class文件中存储着Java虚拟机指令集和符号表以及若干辅助信息。它使用的是一种平台无关的字节码储存格式,不同的虚拟机实现都可以载入执行这种平台无关的字节码。Java虚拟机不与任何语言绑定,只与Class文件这种特定二进制文件格式关联,原则上任何语言都可以编译成Class文件在Java虚拟机上运行。

1、Class类文件结构

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

1.1 无符号数

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

1.2 表

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

2、Class字节码解析

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格的按照顺序紧凑的排列在Class文件中。当遇到占用8字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。Class内部不包含任何分隔符,数据存储顺序数量都被严格限定,不允许任何改动。下面看看具体数据项的含义:

2.1 魔数(Magic Number)

每个Class文件的头4个字节称为魔数(Magic Number),它唯一作用就是用来确定文件是否能被虚拟机接受。

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

2.2 版本号

接下来的4个字节存储着Class文件的版本号,第五第六个字节为次版本号(Minor Version),第七第八为主版本号(Major Version)。版本号主要用于版本控制,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。

2.3 常量池入口

紧接着版本号之后的就是常量池入口,常量池入口后面还必须有一个u2数据项作为常量池容量计数器(因为常量池数量不固定)。

常量池是一个表类型的数据项,相当于Class文件的资源仓库,与Class文件其他项目关联最多,占用Class空间最大的数据项之一,且是第一个出现的表类型数据项目。

常量池主要存储两大类常量:字面量(Literal)和符号引用(Symbolic References)

字面量相当于Java语言中的常量概念,比如字符串,声明为final的常量值。
符号引用则属于编译原理方面的概念包括三类常量:

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

Class文件不会保存各个方法字段的最终内存布局信息,因为这些字段、方法和符号引用不经过运行期转换(动态连接)的话无法得到真正内存入口地址,也就无法被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析翻译到具体的内存地址之中。

常量池中的每一项常量都是一个表(JDK1.7中有14种)。包括UTF-8编码的字符串表,整型字面量表,浮点型字面量表,长整型字面常量表,类和接口的符号引用表,字段符号引用表,类中的方法符号引用表,接口中方法符号引用表等等。这些表都会有各自不同的结构。

2.4 访问标志

常量池之后就是由两个字节代表的访问标识(access flags)这些标识用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口;是否定义为public;是否定义为abstract类型;是否被final修饰。

2.5 类索引、父类索引、接口索引

访问标志位之后就是u2类型的类索引,父类索引和接口索引集合。Class文件由这三项数据确定这个类的继承关系。这三项数据(u2类型的索引值)各指向类型为CONSTANT_Class_info的类描述符常量。

2.6 字段表集合

字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表中字段的各种描述信息(作用域比如public,private,是否被final,static修饰,是否可序列化等)均使用标志位表示,名称则引用常量池中的常量来描述。

2.7 方法表集合

在方法表中,方法的描述和字段的描述基本一致,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

方法中的代码经过编译器编译成字节码指令后存放在方法属性表集合中一个名为“Code”的属性里面。

如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。

2.8 属性表集合

Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

为了能正确解析Class文件,在Java SE 7中预定义了21项属性,虚拟机在运行时会忽略他不认识的属性。

3、字节码指令

Java虚拟机的指令是由一个字节长度的、代表着某种特定操作含义的数字(操作码,Opcode)以及跟随其后的零个至多个代表此操作所需要的参数(操作数,Operands)构成。

常用指令:

  • 加载存储指令,将数据在栈帧中的局部变量表和操作数栈之间来回传输
  • 运算指令,对两个操作数栈上的值进行某种特定运算,并把结果重新写入操作栈,包括加减乘除逻辑与或非
  • 类型转换指令,将两种不同的数值类型进行相互转换,这些转换一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令代码无法与数据类型一一对应的问题。另,类型转换指令永远不可能导致虚拟机运行异常。
  • 对象创建与访问指令,创建对象数组访问对象等
  • 操作数栈管理指令,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括入栈,出栈,栈顶端两个数据交换
  • 控制转移指令,可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,包括条件分支ifxxx,复合条件分支,无条件分支goto等。
  • 方法调用和返回指令,调用对象实例方法,调用接口方法,调用类方法,运行时动态解析出调用点限定符所引用的方法,返回指令根据返回值类型区分
  • 异常处理指令,Java程序中显式抛出异常的操作(throw)都是由athrow指令实现,异常处理不是字节码指令实现,而是采用异常表实现
  • 同步指令,方法级同步和方法内部一段指令序列的同步(通过管程Monitor支持),执行线程要求先持有管程,然后才能执行方法,当方法执行完成后释放管程;方法执行期间,执行线程持有管程,任何一个线程都无法再获取同一个管程

GC算法与内存回收

Java内存回收虽说是自动完成,但当需要排查各种内存溢出问题以及提高系统并发量时,仍然需要对Java的垃圾回收技术进行必要调节与监控。本文主要介绍垃圾收集器的GC算法与内存回收策略。

对象引用算法

GC在回收内存之前首先需要知道对象是否存活,只有那些不需要存活的对象才需要回收。常见判断对象是否存活的方法有两种,一种是引用计数器算法,一种是可达性分析算法,这两种算法在行业内都被广泛使用。

引用计数器算法
给对象添加一个引用计数器,每当有一个地方引用它时就+1;当引用失效时就-1;任何时刻计数器为0则表示对象不再被使用。

引用计数器算法实现简单,判定效率较高,在一些其他语言与游戏脚本中广泛使用。不过它很难解决对象之间相互循环引用的问题。

可达性分析算法
该算法基本思想是:基本通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论说就是从GCRoots到这个对象不可达),则证明此对象不可用。

可达性分析算法在Java,C#等语言的主流实现中用来判定对象是否可用。在Java语言中可作为GC Roots的对象包括:虚拟机栈中引用的对象,方法区中静态属性引用的对象,方法区中常量引用的对象。本地方法栈中JNI引用的对象。

Java中的四种引用

为了描述:一些对象在内存空间足够时则能保存在内存中,当内存空间紧张时则释放掉这些对象。Java将引用的概念扩充为4中,分别为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这四种引用强度依次减弱。

强引用
强引用指在代码中普遍存在的类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器就永远不会回收掉改引用的对象。

软引用
软引用用来描述一些还有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

弱引用
弱引用用来描述非必须的对象,但它的强度比软引用要更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集工作时,无论当前内存是否够用,都会回收掉只被弱引用关联的对象。

虚引用
虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收之前收到一个系统通知。

对象内存回收

一个对象即使被可达性分析算法标记为不可达也并非立即被回收,至少要标记两次才有可能被回收:

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么它就会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为没有必要执行。

如果这个对象有必要执行finalize()方法,那么这个对象会被放置到一个F-Queue的队列中,并在稍后由一个虚拟机自动创建的低优先级的Finalizer线程去执行它。不过这里的执行仅仅是虚拟机插法此方法,但并不承诺会等待它运行完成。因为如果finalize执行时间较长或发生死循环会导致F-Queue中的其他对象用于处于等待状态进而导致内存回收系统的崩溃。

稍后GC将对F-Queue中的对象进行第二次小范围的标记,如果对象在finilize中重新建立起引用链连接,那么在第二次标记中就会被移除即将回收的集合;如果对象这时候仍然没有引用链,那么基本上它就要被回收了。

注意:任何一个对象的finalize方法都只会被系统调用一次,如果下一次GC回收,它的finalize方法将不会被执行。

方法区内存回收

像程序计数器、虚拟机栈、本地方法栈都是随线程而生,随线程而亡,不需要进行内存回收。方法区术语HotSpot虚拟机的永久带,Java虚拟机规范规定可以不对方法区进行回收。而且对于永久带回收内存的效率比较低。

永久带垃圾收集主要包括两部分:废弃的常量、无用的类。常量池中的字符串,类、方法,字段的符号引用如果不在被使用则需要清理出常量池。

判定一个常量是否需要回收比较简单,判断一个类是否需要回收则条件比较苛刻,需要同时满足三个条件:

  • 该类所有实例都被回收
  • 加载该类的ClassLoader已被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过访问该类的方法

垃圾收集算法

标记-清除算法
首先标记出所需要回收的对象,在标记完成后统一回收所有被标记(前面介绍过)的对象。

该算法有两个不足,一是效率问题,标记和清除的效率都不高,二是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收。

复制算法
该算法将可用内存按容量大小分为大小相等的两块,每次只使用其中一块。当这一块内存用完了就将还存活的对象复制到另一块上面,这样内存分配时就不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可。

复制算法解决了标记-清除算法的效率问题,实现简单,运行效率高,但将内存缩小为一半代价过高。复制算法在对象存活率较高时会进行过多复制,效率会降低,而且如果不想浪费50%的内存空间就需要额外的空间进行分配担保,以应对被使用内存对象100%存活的极端情况。

新生代在每次垃圾回收时都有大量对象死亡所以特别适用复制算法,而且该算法的改进版不需要按照1:1的比例划分内存空间,只需要按经验值划分即可。

标记-整理算法
该算法首先标记出所需要回收的对象,在标记完成后统一后将所有存活的对象统一移动到一端,然后直接清理掉边界以外的内存。

老年代中的对象存活时间都比较久,适用于标记整理算法。

分代收集算法
该算法根据对象存活周期将内存划分为不同的几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点分别采用最合适的收集算法。当前商业虚拟机普遍采用分代收集算法。

Java对象创建与内存分布

本文主要讲述Java对象在虚拟机中创建,分配内存,初始化的过程,以及分配内存,引用对象的几种常见方式。

对象创建

对象创建分为三部分,首先是类加载,接着是为对象分配内存,最后是初始化。

创建
虚拟机遇到new指令时会去检查这个指令参数是否能在常量池中定位到一个符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有则先进性类加载过程。

分配内存
对象所需内存大小在类加载完成后即可确定,所以虚拟机只要从堆中划分出相应大小内存分配给新创建的对象即可。

常见的内存分配方式有两种,一种是“指针碰撞”,一种是“空闲列表”。不同的Java虚拟机实现会分别采用这两种内存分配方式。

“指针碰撞”假设Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放一个指针作为分界点指示器。当需要分配内存时只需要把指针向空闲内存方向移动对象大小相等的距离即可。

如果Java堆中的内存并不规整,那么虚拟机需要维护一个列表用来记录那些内存块可用。当需要分配内存时从列表中找出一个足够大的空间划分给对象实例,这就是“空闲列表”。

初始化
虚拟机在对象内存分配完成后首先会将不包括对象头的内存空间初始化为零值,即为对象的字段分配其数据类型所对应的初始值。这一步保证对象的实例字段在Java代码中可以不赋初始值就可使用。

接下来虚拟机向对象头空间写入实例所属类,类的元数据信息获取方式,对象的哈希码,对象GC分代年龄等信息。

然后执行方法按照程序员编写的程序代码将对象进行初始化。(这里就是所谓的对象初始化”两次”的问题)

分配内存的线程安全问题
对象创建在虚拟机中是非常频繁的过程,并发的情况下并不是线程安全的。解决问题有两种方案,一种是在分配内存时进行同步处理;另一种是为每一个线程在Java堆中预先分配一块内存(即本地线程分配缓冲TLAB),这样线程内存分配的动作分别在不同的内存空间中进行,只有缓冲区内存不足时才会为缓冲区同步分配内存。虚拟机分配内存时还会加上失败重试的方式。

对象内存分布

对象在内存中的分配包括三部分:对象头,实例数据和对齐填充。

对象头
对象头包括两部分信息,第一部分是用于存储对象自身的运行时数据,如哈希码,GC分代年龄、锁状态标识、线程持有的锁,偏向线程ID、偏向时间戳等,这部分数据的长度在32为虚拟机中为32bit在64为虚拟机中为64bit,所有数据均以标志位的形式存储。

对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。但这并不是必须有的,也就是查找对象元数据并不一定需要对象本身。如果对象是数组,那么对象头中还必须记录对象数组长度。

实例数据
示例数据部分存储着对象程序代码中定义的各种类型字段内容,包括从父类继承的和子类总定义的。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源代码中定义顺序的影响。虚拟机默认将相同长度的字段分配到一起,且父类定义的变量会出现在子类之前。通过配置虚拟机参数也可以使子类较窄的变量插到父类变量空隙中。

对象填充
对象填充仅仅是占位符,并不是必然存在。HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍,所以如果对象实例数据部分没有对齐需要对齐填充来补齐(对象头已经对齐)。

对象访问

当创建好对象后,我们需要通过引用reference来访问使用对象,常见的有两种方式,第一种是句柄,第二种是直接指针。

句柄
Java堆中需要专门划分一部分内存作为句柄池,Java栈中的引用存储的是对象的句柄地址,而句柄地址存储了对象实例数据与数据类型各自的具体地址信息。

使用句柄的好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是普遍的行为)时只会改变句柄中实例数据指针,而reference不需要更改。

直接指针
直接指针就是Java栈中的引用直接存储对象的内存地址。使用直接指针最大的好处就是访问速度快,它节省了一次指针定位的时间开销。Sun的Hot Spot虚拟机使用的直接指针访问对象。

Android包管理服务

PackageManagerService(PmS)包管理服务运行在SystemServer进程中,是一个安卓系统服务,主要用于实现应用安装卸载,组件查询匹配,权限管理等功能。

主要功能

  • 根据Intent匹配到具体的Activity,Provider,Service,即当应用程序调用startActivity(intent)方法时,能够把Intent转换成一个具体的包含程序名称及Component的信息,以便类加载器加载具体的Component。

  • 权限检查,当应用程序调用某个需要特定权限的接口时,判断调用者是否有该权限

  • 提供安装删除应用程序的接口

实现原理

应用安装时读取应用程序AndroidManifest.xml中的标签比如,request-feature、permission并将其保存在指定目录文件下,PmS在启动时会读取这些xml文件建立起一个包信息树,应用程序可以间接的从信息树种查到所需要的程序包信息。

两个目录

1./data/system/package.xml文件记录系统中所有应用程序包管理相关信息,比如程序包名称是什么,安装包路径在哪里,程序都使用了哪些权限,等等。
2./system/etc/permissions/文件夹下保存的xml文件用于应用程序权限管理。

PmS在启动时会读取这两个文件来构建应用程序包信息树。PmS读取的数据会存在其内部类变量中。

数据结构

PmS的内部类Settings基本上包含了包管理所需要的全部信息,该类主要包含几类变量:

  1. 包属性信息,包括packages.xml配置文件,配置文件备份,应用程序列表文件,包管理信息。
  2. 用户Id相关信息,所有用户Id共享Id等,
  3. 权限管理相关信息,保存所有的签名,所有的权限。
  4. 删除信息,应用程序被卸载后,如果该程序的数据保存在外部存储空间中,则其数据目录默认不被删除。

以上信息,均是从packages.xml配置文件中解析而来。

PmS类中还包括以下重要数据:

  • 扫描应用程序目录得到的程序包信息
  • 系统所有权限名称
  • 系统所依赖的共享Java库
  • 从应用程序AndroidManifest.xml中解析出来的Activity,Service,BroadcastReceiver列表

PmS在启动时会遍历应用程序目录下的所有程序,并从AndroidManifest.xml中提取出Intent-filter数据并将其保存在一个列表中,用于进行Intent-filter(startXXX(intent))匹配。

关键方法

1.readPermission()
/system/etc/permissions/目录下读取系统中定义的所有feature列表,给系统一些native分配权限信息。

2.mSettings.readLP()
packages.xml中读取所有安装包信息。其中会涉及到package-backup.xml文件的处理。packages.xml中使用各种各样的标签存储应用程序信息。

3.mSettings.writeLP()
应用程序包扫描已经应用程序安装。应用程序安装分为两步,第一是把原始APK复制到相应程序目录下,第二则是为应用程序创建相应的数据目录及提取dex文件,并修改系统包管理信息等。mSettings.writeLP()主要完成第二步。

4.scanPackageLI()
将mSettings.mPackages的数据写到packages.xml和packages.list文件中。

创建PmS

SystemServer启动时,PmS在从其静态main函数中创建,并将自己添加到系统服务中:

1
2
3
4
5
public static final IPackageManager main(Context context,boolean factoryTest){
PackageManagerService m = new PackageManagerService();
SystemManager.addService("package",m);
return m;
}

启动PmS

  1. 创建PmS.Settings数据对象并赋值
  2. 创建Installer对象,该对象主要用于辅佐应用程序安装
  3. 为几个静态数据文件路径变量赋值
  4. 调用readPermission()方法从/system/etc/permissions目录下读取并解析全部XML文件
  5. 调用mSettings.readLP()方法从/data/system/packages.xml文件中读取应用程序包管理相关信息。
  6. 提取或者转换Java系统库中的dex文件
  7. /system/framwworks,/system/app,/vendor/app目录添加FileObserver,FileObserver用于检测目录天剑删除的问价事件。
  8. 调用scanDieLP()扫描上述三个目录中的所有应用程序,并将扫描结果保存到PmS的mPackages变量中。
  9. 删除已经不存在的应用程序对应的数据记录
  10. 清除没有安装成功的数据记录
  11. /data/app添加FileObserver
  12. 检测是否系统升级,如果升过则重新为应用程序设置权限
  13. 将mSettings.mPackages中的数据从新写入packages.xml中

获取PmS

通过ContextImpl.getPackageManager()返回一个PackageManager对象,然后就可以调用改对象提供的各种API接口。

getPackageManager内部获取过程和getSystemService的过程基本相似,都是通过ServiceManager获取指定名称的IBinder对象进而获取PmS服务。(普通服务通过context.getSystemService("serviceName")获取)

React Native实现banner轮播图

轮播图也叫焦点图,就是几张图片不断的来回切换,很多应用和网站中都可以看到。

如果是前端开发,使用JavaScript+HTML实现,可以将几张图横向拼装在一起,开启setInterval定时任务不断改变拼装图片的left位置。

如果是Android,即可以使用ViewPage,又可以使用ScrollView。如果使用ScrollView实现,则与前端实现方法基本相同,只不过需要自己控制手势操作。

本文中使用React Native的ScrollView组件实现轮播图,废话不说了,先来看效果:

效果图

动画很流畅(原谅我录屏的帧率比较小,转成gif就…),没有出现卡顿,同时支持手势滑动、点击事件,没有出现滑动冲突。总体来说效果还可以吧。

实现原理很简单,开启个定时任务不断来回滚动几张图片,同Android使用ScrollView实现以及前端实现方法基本一致。代码全部使用的官方API,比较容易理解。

轮播图组件代码

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/**
* @Author: zhaoshuo
* @Description: 轮播图组件
*/


'use strict';

import React, {
PropTypes,
TouchableWithoutFeedback,
ScrollView,
Animated,
View,
Component,
} from 'react-native';

import Dimensions from 'Dimensions';

// 屏幕宽度
var screenWidth = Dimensions.get('window').width;

class FocusImage extends Component{
constructor(props) {
super(props);
this.state = {
images : ['#dfe24a','#68eaf9','#ef9af9'],// 使用颜色代替图片
selectedImageIndex: 0,
isNeedRun: true,
};

this._index = 0;// 当前正在显示的图片
this._max = this.state.images.length;// 图片总数
}

render(){

// 图片列表
let images = this.state.images.map((value,i) => {
return (
<TouchableWithoutFeedback onPress={()=>this._showLog(i)}>
<View style={{width:screenWidth,height:130,backgroundColor:value}}/>
</TouchableWithoutFeedback>);
});

// 小圆点指示器
let circles = this.state.images.map((value,i) => {
return (<View key={i} style={ (i == this.state.selectedImageIndex) ? styles.circleSelected : styles.circle}/>);
});

// 小圆点位置居中显示
let imageLength = this.state.images.length;
let circleLength = 6 * imageLength + 5 * 2 * imageLength;
let center = (screenWidth - circleLength) / 2;

return (
<View style={styles.container}>
<ScrollView horizontal={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
onTouchStart={()=>this._onTouchStart()}
onTouchMove={()=>console.log('onTouchMove')}
onTouchEnd={()=>this._onTouchEnd()}
onScroll={()=>this._onScroll()}
ref={(scrollView) => { this._scrollView = scrollView;}}>

<Animated.View style={{flexDirection:'row'}}>{images}</Animated.View>
</ScrollView>
<View style={{flexDirection:'row',position:'absolute',top:115,left:center}}>{circles}</View>
</View>
);
}

_onTouchStart(){
// 当手指按到scrollview时停止定时任务
clearInterval(this._timer);
}

_onTouchEnd(){
// 先滑动到指定index位置,再开启定时任务
this._scrollView.scrollTo({x:this._index * screenWidth},true);
// 重置小圆点指示器
this._refreshFocusIndicator();
this._runFocusImage();
}

_onScroll(){
this._contentOffsetX = this._scrollView.contentOffset.x;
this._index = Math.round(this._contentOffsetX / screenWidth);
}

_runFocusImage(){
if(this._max <= 1){ // 只有一个则不启动定时任务
return;
}
this._timer = setInterval(function () {
this._index++;
if(this._index >= this._max){
this._index = 0;
}
this._scrollView.scrollTo({x:this._index * screenWidth},true);
// 重置小圆点指示器
this._refreshFocusIndicator();
}.bind(this), 4000);
}

_stopFocusImage(){
clearInterval(this._timer);
}

_refreshFocusIndicator(){
this.setState({selectedImageIndex:this._index});
}

_showToast(i) {
//显示的内容
var message = '点击: ' + i;
console.log(message);
}

// 组件装载完成
componentDidMount(){
this._runFocusImage();
}

// 组件即将卸载
componentWillUnmount(){
clearInterval(this._timer);
}

// 组件接收到新属性
componentWillReceiveProps(nextProps) {
}
}

const styles = {
container: {
flex:1,
flexDirection:'row',
},
circleContainer: {
position:'absolute',
left:0,
top:120,
},
circle: {
width:6,
height:6,
borderRadius:6,
backgroundColor:'#f4797e',
marginHorizontal:5,
},
circleSelected: {
width:6,
height:6,
borderRadius:6,
backgroundColor:'#ffffff',
marginHorizontal:5,
}
};

FocusImage.defaultProps = {
isNeedRun : true,
};

FocusImage.propTypes = {
isNeedRun : PropTypes.bool,
onItemClick : PropTypes.func,
};

module.exports = FocusImage;

功能逻辑比较简单,代码里有注释,我就不再做多余的介绍了。

使用

1
2
3
4
5
6
7
8
9
import FocusImage from './../components/FocusImage';

...
render(
<View>
<FocusImage />
</View>
);

...

Git版本管理策略

本文主要介绍使用Git这种有主干分支概念的工具开发项目时,进行版本管理的方法。每种方法都对应一些特定情况,使用时可以按需选取。

主干开发主干发布

在一个开发周期迭代时所有开发测试都在主干分支上进行,待开发完成在主干上打tag发布。

主干开发分支发布

在主干上进行开发任务,发布时从主干上拉一个分支并进行测试与bug修复工作,待测试修复完成后在分支上发布,并将发布代码合并到主干上。

分支开发分支发布

每次开发周期迭代时都重新拉一个开发分支,在这个周期内所有开发任务都在这个分支上进行。待开发测试完成再将这个分支合并到主干分支上。

项目延期

由于种种原因没能完成开发任务,这时候需要在主干上单独拉一个分支进行延期项目开发,已完成的任务按原计划发布。在新分支上完成开发测试后再合并到主干分支上。

Bug

发现重大Bug需要专门拉取一个分支进行修复,并在修复完成后合并到主干分支上。

对于客户端,可能需要修复某一个版本的bug,在此版本tag上拉取分支,修复完成后在此分支上发布。

定制任务

对于客户端,可能需要为一些渠道定制开发,所以在主干分支上专门拉取一个分支进行开发,并在开发完成后发布。此次定制功能并不是主干代码需要的功能,不必合到主干分支上。

举例

在实际开发工作中,项目发布可能会在master分支上进行,而开发则会在develop分支上进行。

大家都在develop分支上开发,但总有一些情况导致某一两个项目无法在发布之前完成,然而所有代码无论开发完成的还是未开发完成的都在develop分支上。

这时候可以在develop分支上再开一个分支,develop上进行一些处理并进行一系列测试与bug修复工作,待测试完成将develop合并到master上发布。而新分支则继续进行延期项目开发,待开发完成合并到develop上,在下一个版本时发布。

Java内存区域与异常

Java虚拟机在运行时会把其管理的内存划分为若干不同的数据区域。《Java虚拟机规范》规定的数据区域通常包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、运行时常量池以及直接内存。这些区域都会有各自不同的生存周期以及各自不同的用途,本文主要介绍这些内存区域以及各个内存区域可能抛出的异常。

程序计数器

程序计数器相当于当前线程所执行字节码的行号指示器。字节码解释器通过改变这个计数器来选取下一条需要执行的字节码指令,程序的循环、跳转、异常处理、线程切换都需要依赖这个计数器来完成。

一个Java虚拟机内部可以有多个线程,每个线程都会有单独的程序计数器,程序计数器属于线程私有内存,各个计数器之间互不影响。

程序计数器记录的只能是Java方法编译出的字节码指令地址,对于Native方法,则计数器为空。程序计数器不会出现OutOfMemoryError异常。

Java虚拟机栈

Java虚拟机栈就是我们经常说的堆栈中的栈内存。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

虚拟机栈中有一个局部变量表,局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和指向一条字节码指令地址的returnAddress类型。

虚拟机栈是线程私有的,其生命周期与线程相同。此区域可能会出现两种异常:如果请求栈深度大于虚拟机最大允许栈深度则抛出StackOverflowError异常;如果虚拟机栈在动态扩展时无法申请到足够的内存则会抛出OutOfMemoryError异常。

本地方法栈

同Java虚拟机栈相似,本地方法栈是Native方法执行的内存模型,其内部也会抛出StackOverflowError和OutOfMemory异常。

Java 堆

Java堆用来存放虚拟机在运行时创建的Java对象实例。Java虚拟机规范规定:所有的对象实例以及数组都要在堆上分配。不过现在很多技术比如Just InTime(及时编译),允许在栈中分配对象内存。

Java堆内存被所有线程共享,任何线程都可以在上面创建对象。内存回收(GC)主要在Java堆上进行。

Java堆内存也是虚拟机所管理的内存最大的一块。其内存只要逻辑上连续即可,允许物理上不连续。如果在创建对象时堆内存没有足够的内存分配会抛出OutOfMemoryError异常。

方法区

方法区用来存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据,被所有线程共享。

当方法区无法满足内存分配时,会抛出OutOfMemoryError异常。

运行时常量池

运行时常量池是方法区的一部分,Class文件中的常量池(Constant Pool Table)在类加载后会放到运行时常量池中。

Class常量池用于存放编译期生成的各种字面常量和符号引用。运行时常量池与Class常量池的区别就是动态性。运行时常量池不仅仅允许编译期放入常量池,也允许运行时将新的常量放入常量池。而Class常量池只能在编译期生成。Java虚拟机规范对Class常量池要求严格,对运行时常量池的要求则比较宽松。

当常量池无法再申请到内存空间时会抛出OutOfMemoryError异常。

直接内存

直接内存也就是我们本机可用的物理内存空间,不属于任何Java虚拟机,但任何虚拟机都可以在上面操作。例如,Java虚拟机可以通过NIO包中提供的方法直接在物理内存中分配。

当Java虚拟机要求分配的内存大于本机物理内存时就会抛出OutOfMemoryError异常。

内存溢出与内存泄露的区别

内存溢出
内存溢出是指分配对象的内存超过虚拟机所允许的最大内存,此时所有的对象实例均有用。优化方案就是尝试减少程序运行时内存消耗。

内存泄露
某些对象不再有用,但由于不正确的引用关系造成对象内存无法释放,最终导致所有对象的内存超过虚拟机所允许的最大值。所以要检查每个对象的生命周期,确保长生命周期对象引用短生命周期时释放内存。

React Native数据存储

RN使用AsyncStore将数据存储到本地,AsyncStorage是一个基于key-value键值对的异步持久化存储系统,对于应用来说其存储的内容全局生效。

AsyncStorage使用异步Promise模式存储数据,例如调用存储方法存储一个字符串setItem('I_AM_KEY','i_am_value')setItem会异步执行,等setItem执行完成后会返回一个Promise对象。

举个例子:

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
'use strict';

import React,{AsyncStorage,Component,TouchableOpacity,View,Text,AppRegistry} from 'react-native';

// 数据对应的key
var STORAGE_KEY = 'I_AM_KEY';

class Demo extends Component{

// 获取
async _get() {
console.log('Demo._get()');
try {// try catch 捕获异步执行的异常
var value = await AsyncStorage.getItem(STORAGE_KEY);
if (value !== null){
console.log('_get() success: ' ,value);
} else {
console.log('_get() no data');
}
} catch (error) {
console.log('_get() error: ',error.message);
}
}

// 保存
async _save(value) {
console.log('Demo._save()');
try {
await AsyncStorage.setItem(STORAGE_KEY, value);
console.log('_save success: ',value);
} catch (error) {
console.log('_save error: ',error.message);
}
}

// 删除
async _remove() {
console.log('Demo._remove()');
try {
await AsyncStorage.removeItem(STORAGE_KEY);
console.log('_remove() success');
} catch (error) {
console.log('_remove() error: ', error.message);
}
}

render(){
return(
<View style={{flexDirection:'column',flex:1,marginTop:50,}}>

<TouchableOpacity style={{padding:10,flex:1,flexDirection:'row',}} onPress={()=>this._save('haha').then(()=>console.log('you can do something here when the setItem is starting')).done(()=>console.log('you can do something here when the setItem is done'));}>
<Text style={{fontSize:16,color:'#333333'}}>保存数据</Text>
</TouchableOpacity>
<TouchableOpacity style={{padding:10,flex:1,flexDirection:'row',}} onPress={()=>this._get().done()}>
<Text style={{fontSize:16,color:'#333333'}}>获取数据</Text>
</TouchableOpacity>
<TouchableOpacity style={{padding:10,flex:1,flexDirection:'row',}} onPress={()=>this._remove()}>
<Text style={{fontSize:16,color:'#333333'}}>删除数据</Text>
</TouchableOpacity>
</View>);
}
}

AppRegistry.registerComponent('Demo', () => Demo);

代码很简单,点击三个按钮就可以看到console控制台的输出数据。

ES6中promise提供了几个回调方法then,done,finally,如下所示:

1
this._save('haha').then(()=>console.log('you can do something here when the setItem is starting')).done(()=>console.log('you can do something here when the setItem is done'));

  • then()方法会在setItem开始执行后执行
  • done()方法会在setItem执行完成后调用,done
    都会捕捉到任何可能出现的错误,并向全局抛出
  • finally则是回调链执行的最后一个方法

AsyncStore全部方法列表参请参考官方文档,或者在你的工程项目中搜索AsyncStore.js查看源码。


参考资料:

官方AsyncStore
React Native Storage第三方组件
Promise的回调方法
JavaScript ES7 中使用 async/await 解决回调函数嵌套问题

React Native语法指南

React Native真的是越来越流行,没使用React Native开发项目都不好意思说自己是搞客户端开发的。对于纯Native开发者来说,刚上手React Native有一定的适应期,如果JavaScript也不熟练的话那就更悲催了。React Native涉及ES6,React语法,JSX,前端调试,Native客户端等知识,本文简单总结了React Native开发中一些知识点。算是在学习中的积累。

Component

Component:组件,使用React.createClass或者extends React.Component创建的类为组件。
Element:元素或者可以说是组件的实例,使用<Label />或者let label = new Label()创建的为实例。

对于定义组件,React以前版本的写法(ES5):

1
2
3
4
5
6
var Lable  = React.createClass({

render(){

}
});

React最新的写法(ES6):

1
2
3
4
class Label extends React.Component{
render(){
}
}

props与state

props属性:组件可以定义初始值,自己不可更改props属性值,只允许从父组件中传递过来:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 父组件
class MainComponent extends React.Component{
render(){
return(<Label name="标题栏">);
}
}

// 子组件
class Label extends React.Component{
render(){
return(<Text>{this.props.name}</Text>);
}
}

父组件向Label传递name=”标题栏”的props属性,在Label中使用this.props.name引用此属性。

state属性:组件用来改变自己状态的属性,通常使用setState({key:value})来改变属性值,不能使用this.state.xxx来直接改变,setState({key:value})方法会触发界面刷新。

对于经常改变的数据且需要刷新界面显示,可以使用state。对于不需要改变的属性值可以使用props。React Native建议由顶层的父组件定义state值,并将state值作为子组件的props属性值传递给子组件,这样可以保持单一的数据传递。

在以前版本的React中定义state,props可以使用生命周期方法 getInitialState()getInitialState():

1
2
3
4
5
6
7
8
9
10
11
12
13
var Label = React.createClass({
getInitialState(){
key:value,
...
},
getInitialProps(){
key:value,
...
},// 这种写法需要有,不要使用;
render:funation(){

}
});

在最新版本的React可以使用构造函数替代getInitialState(),getInitialState()方法定义初始值:

1
2
3
4
5
6
7
8
9
10
11
12
class Label extends React.Component{
constructor(props) {
super(props);
this.state = {
time: '2016',
city: '上海',
};
this.props = {
name:'标题',
};
}
}

默认props与props校验

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
class Label extends React.Component{
constructor(props) {
super(props);
}

// 默认props
static defaultProps = {
city: '南京',
index: 12,
}

// propTypes用于验证转入的props,当向 props 传入无效数据时,JavaScript 控制台会抛出警告
static propTypes = {
city: React.PropTypes.string.isRequired,
index: React.PropTypes.number.isRequired,
}

state = {
city: this.props.city,
index:this.props.index,
}
}

// or

class Label extends React.Component{
constructor(props) {
super(props);
}
}

// 默认props
Label.defaultProps = {
city: '南京',
index: 12,
}

// propTypes用于验证转入的props,当向 props 传入无效数据时,JavaScript 控制台会抛出警告
Label.propTypes = {
city: React.PropTypes.string.isRequired,
index: React.PropTypes.number.isRequired,
}

生命周期

我们把组件从装载,到渲染,再到卸载当做一次生命周期,也就是组件的生存状态从装载开始到卸载为止,期间可以根据属性的变化进行多次渲染。

生命周期的三种状态:

  • Mounting:装载,
  • Updating:渲染
  • Unmounting:卸载
1
2
3
4
5
componentWillMount(),组件开始装载之前调用,在一次生命周期中只会执行一次。
componentDidMount(),组件完成装载之后调用,在一次生命周期中只会执行一次,从这里开始就可以对组件进行各种操作了,比如在组件装载完成后要显示的时候执行动画。
componentWillUpdate(object nextProps, object nextState),组件属性更新之前调用,每一次属性更新都会调用
componentDidUpdate(object prevProps, object prevState),组件属性更新之后调用,每次属性更新都会调用
componentWillUnmount(),组件卸载之前调用

组件属性更改时会调用以下方法,在一次生命周期中可以执行多次:

1
2
componentWillReceiveProps(object nextProps),已加载组件收到新的参数时调用
shouldComponentUpdate(object nextProps, object nextState),组件判断是否重新渲染时调用

页面跳转

初始化第一个页面:

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
import SeatPageComponent from './SeatPageComponent';
import MainPageComponent from './MainPageComponent';
import TrainListComponent from './TrainListComponent';

class MainPage extends React.Component {
render() {
let defaultName = 'MainPageComponent';
let defaultComponent = MainPageComponent;
return (
<Navigator
// 指定默认页面
initialRoute={{ name: defaultName, component: defaultComponent }}
// 配置页面间跳转动画
configureScene={(route) => {
return Navigator.SceneConfigs.VerticalDownSwipeJump;
}}
// 初始化默认页面
renderScene={(route, navigator) => {
let Component = route.component;
// 将navigator作为props传递到下一个页面
return <Component {...route.params} navigator={navigator} />
}} />
);
}
}

跳转到下一页面:

1
2
3
4
5
6
7
8
9
jumpToNext(){
const { navigator } = this.props;// 由上一个页面传递过来
if(navigator) {
navigator.push({
name: 'SeatPageComponent',
component: SeatPageComponent,// 下一个页面
});
}
}

返回上一个页面:

1
2
3
4
5
6
_back(){
const { navigator } = this.props;
if(navigator) {
navigator.pop();
}
}

页面间通信

例如:从A页面打开B页面
A通过route.params将参数传递给B:

1
2
3
4
5
6
7
8
9
10
11
12
13
jumpToNext(){ 
const { navigator } = this.props;// 由上一个页面传递过来
if(navigator) {
navigator.push({
name: 'SeatPageComponent',
component: SeatPageComponent,// 下一个页面
params: { // 需要传递个下一个页面的参数,第二个页面使用this.props.xxx获取参数
id: 123,
title: this.state.title,
},
});
}
}

A通过route.params传递回调方法或者A的引用来让B将数据传回给A:

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

// A页面
jumpToNext(){
const { navigator } = this.props;// 由上一个页面传递过来
if(navigator) {
let that = this;// this作用域,参见下文函数绑定
navigator.push({
name: 'SeatPageComponent',
component: SeatPageComponent,// 下一个页面
params: { // 需要传递个下一个页面的参数,第二个页面使用this.props.xxx获取参数
title: '测试',
getName: function(name) {that.setState({ name: name })}
},
});
}
}

// B页面
_back(){
const { navigator } = this.props;
if(this.props.getName){
this.props.getName('测试');
}
if(navigator) {
navigator.pop();
}
}

组件间通信

父组件–>子组件, 使用props,父组件向子组件传递props

1
2
3
4
5
6
7
8
9
10
11
12
13
// 父组件
class MainComponent extends React.Component{
render(){
return(<Label name="标题栏">);
}
}

// 子组件
class Label extends React.Component{
render(){
return(<Text>{this.props.name}</Text>);
}
}

子组件–>父组件, 父组件在创建子组件时传递回调方法

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
// 父组件
class MainComponent extends React.Component{
constructor(props) {
super(props);
this.state = {
name: '测试',
};
}

// 回调方法
getName(str){
this.setState({name:str});
}

render(){
return(<Label name="标题栏" getName={getName}/>);
}
}

// 子组件
class Label extends React.Component{
render(){
return(
<View>
<TouchableOpacity onPress={()=>this._onPress()}>
<Text>点我,{this.props.name}</Text>
</TouchableOpacity>
</View>);

}

_onPress(){
if(this.props.getName){
this.props.getName('测试')
}
}
}

非父子关系的组件,即没有任何嵌套关系的组件, 可以引入订阅源(js-signals, PubSubJS),监听订阅事件。例如,在生命周期方法中addEventListener(),removeEventListener(),在合适时机setState()。

ECMAScript

ES6中函数的写法:

1
2
3
4
5
class Label extends React.Component{
doSomething(){
//...
}// 不要使用逗号或者分号作为结尾
}

key:value形式定义函数的写法:

1
2
3
4
5
6
7
8
var Label = React.createClass({
doSomething:funation(){
//......
},// 需要使用逗号作为结尾,不能使用分号
doSomething2:function(){
//......
},
});

函数绑定

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
class Label extends React.Component{

// 有函数
sayHello(str){
console.log(str)
}

// 在onPress中使用箭头函数调用
// onPress={() => this.sayHello('Hello')}

// 等同于
//onPress={sayHello('hello').bind(this)}

// 等同于
// onPress={print('hello',this)}

render(){
return (
<View>
<TouchableOpacity onPress={() => this.sayHello('Hello')}>
<Text>点我</Text>
</TouchableOpacity>
</View>
)

}

function print(str,this){
let that = this;// 注意这里this的生命周期
function say(str){
that.sayHello(str)// 此处不能再使用this
}
say(str);
}
}

Tips

require,import:javascript的模块管理工具,管理各个模块之间的引用,解决javascript异步加载的问题,解决js写成多个文件后浏览器加载缓慢的问题。

JavaScript中没有private,public的概念
使用_开头的方法代表private方法,不适用则表示public方法

1
2
3
4
5
6
7
8
9
10
11
class Label extends Component{
// private 函数
_doSomething(){
//......
}

// public 函数
doSomething(){
//......
}
}


参考资料
Reactjs中文教程
极客学院React教程
ECMAScript语法
JavaScript模块系统
require.js
Navigator
结合ES6+开发React
React组件通信