Android开发随记

1、性能优化

性能优化能够在这几个方面下手, 流畅性稳定性包体积大小
  1. 流畅性优化
    1. 启动时间优化—在Application的onCreate的时候,会有不少SDK选择在这里进行初始化,在加上本身写的一些库也在这里初始化,这样主线程在初始化的时候将会不堪重负,致使启动好久白屏,因此在初始化的时候应当进行
      1. 根据库进行分步延迟加载
      2. 多线程加载
      3. 后台任务加载
    2. UI优化—UI层级过深,在进行测量和定位的时候将会占用更多的CPU资源,也会致使渲染周期加长,在Android的渲染机制中,每16ms将会发起一次垂直同步信号,进行渲染,若是在16ms之内还没法更新到surface,画面将会显示上一次的画面,这样看起来就会卡顿。解决措施:
      1. 减小布局层级
      2. 使用懒加载标签ViewStub
      3. 避免使用include,改为使用merge标签
      4. 尽可能避免使用复杂的矢量动画和矢量图形,绘制矢量图形的须要占用cpu资源,也会致使卡顿,复杂的矢量图形可使用位图,GPU会进行缓存。
    3. 避免大量的IO — 大文件IO是很是占用CPU的耗时操做,必要时能够进行分布,分片的IO操做,对于不须要操做数据库的数据应当使用文件保存,小文件读写比数据库更快,也避免数据库冗余。
    4. 避免频繁GC — 避免频繁大量的建立对象,当内存紧张时,会频繁GC,申请大内存的对象,也会有可能触发GC,GC时会占用CPU,致使画面卡顿
    5. 合理的使用线程 — 线程的切换是又开销的,频繁的切换线程是会使用性能下降,应当建立cpu核数至关的线程池,合理分配线程,和使用协程
    6. 避免过多的复杂计算 做为前端也不该该进行复杂的运算,又不是超算,密集的复杂计算也会占用更多的cpu资源。
  2. 稳定性优化
    影响App稳定性常见的有两个场景 Crash 和 ANR,它会致使App异常退出。因此解决App的稳定性应该列为最高优先级。如何避免异常的发生,能够从这几个方面入手
    1. 编码阶段。人非机器,即便是机器也会出错,因此应该使用更多的工具辅助,在编码的时候尽可能把异常状况排除掉。
      1. 空指针异常。最多见的异常就是空指针异常的,我建议使用kotlin,有空安全类型。
      2. 内存泄漏,发生内存泄漏的主要缘由的生命周期不一致的对象相互引用,好比在线程中,handler,静态单例里引用了Activity,Activity销毁后,没有被释放。要解决的这个除了改变编程习惯,也可使用一些协同Activity的生命周期的工具类来使用线程和handler,在Activity销毁的时候把Activity的引用释放,避免不规范的建立线程,handler,致使内存泄漏。
      3. OOM。在App中常见的是加载大图等内存的大户。因此图片要进行压缩,读取的时候不要直接将大图载入内存中,先获取图片信息,在设置压缩比例inSampleSize在加载,最好使用Glide,Picasso这些优秀的开源库加载,他们有对图片缓存管理。
      4. 至于其余的bug,若是时间容许能够编写单元测试,也可使用相似Android Lint,Findbugs的工具排查。多人团队开发的,能够互相审查代码,一来能够看出本身没有察觉的bug,二来也能熟悉他人的代码。
    2. Carsh信息监控上报。这个不少第三方平台都有,app的必需品,这里就不打广告了。若是要本身写的话,Java层,除了设置UncaughtExceptionHandler以外,还须要获取AMS.getProcessesInErrorState,native层的话须要设置sigaction 和使用libunwind这个库了。
  3. 包体积的大小的优化
    1. 只使用一套高分辨率的资源图,使用工具对图片进行压缩,图片使用webp格式。
    2. 对于so文件只使用v7a平台的。固然这是牺牲性能为代价的处理方式。下列内容转自:https://www.cnblogs.com/yingsong/p/6709322.html
      1. armeabiv-v7a: 第7代及以上的 ARM 处理器。2011年15月之后的生产的大部分Android设备都使用它.
      2. arm64-v8a: 第8代、64位ARM处理器,不多设备,三星 Galaxy S6是其中之一。
      3. armeabi: 第5代、第6代的ARM处理器,早期的手机用的比较多。
      4. x86: 平板、模拟器用得比较多。
      5. x86_64: 64位的平板。
    3. 使用7z打包。能够参考微信的AndResGuard

2、内存模型

Linux的进程内存模型是由用户空间内核空间组成。html

  • 内核空间。在这里CPU能够访问任何外围设备,好比什么键盘,显示器,网卡,固然这些在CPU的眼里都是一段物理地址。换句话说,在内核空间CPU能够访问全部的物理地址。这个内核空间是全部进程共享的。
  • 用户空间。在这里CPU的访问是受限的,好比操做系统给它分配了2G的空间,它也就只能访问这2G的地址了。这个是进程独享的,其余的进程没法访问这个空间。

在应用程序中,若是直接操做外围设备,访问时也不知道其余程序有没有在访问,也不知道哪一段能够用的,你们你争我抢的,都乱套了,并且也不安全。因此须要一位管理者--操做系统。操做系统将真实的物理地址隔离起来,给每一个程序分配一段虚拟地址,经过mmap将真实地址和虚拟地址起来,好比虚拟地址是0x00,那么它真实的物理地址多是0x1c。在真实物理地址它可能不是一段连续的地址,可是在虚拟地址是连续的就能够了。前端

虚拟空间还能够进行细分:java

内核空间(进程管理,存储管理,文件管理,设备管理,网络系统等)
----------
栈
FileMapping
堆
BSS
Data
text
复制代码

  • 内核空间。这里主要是一些进程管理,存储管理,文件管理,设备管理,网络系统等。因为这部分是全部进程共享的,为了更高效率的通讯,在Android中设计了一块匿名共享内存,只要将数据从用户空间拷贝到这里其余进程就能够获取,这样就能够实现高效率的进程间通讯。具体能够看看微信的MMKV的原理,Binder也是这个原理。
  • 用户空间
    1. 。这一块不是很大,主要保存一些方法的地址,局部变量表,返回地址等。因此递归很容易就StackOverFlow。
    2. 文件地址映射块。这里记录了虚拟地址对实际文件物理地址的映射,包括动态连接库文件。内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,并且在对该文件进行操做以前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将没必要再对文件执行I/O操做,使得内存映射文件在处理大数据量的文件时能起到至关重要的做用。
    3. 。这个区间是咱们要重点关注的,由于它彻底由咱们程序员来控制。native申请的空间为native heap,Java申请的空间则为dalvik heap。在Android系统中,有对Java进程申请堆内存空间进行限制,这个阈值在不一样手机上不一样,好比48MB。超过了这个值就会发生OOM。若是想要突破这个限制,有两个方法
      • 申请大内存。android:largeHeap=”true”
      • 建立子进程。android:process
    4. BSS这个区间保存的是一些没有初始化的全局变量,好比 int a;没有映射实际的物理地址,只是记录一下所须要用到的内存空间,因此这样写的变量是不会有默认的赋值。
    5. Data。这个区间保存的是已经初始化的全局变量。好比int a=123。
    6. 代码。保存程序文本。这个区域是只读的,防止被修改。

3、JVM 内存模型

进程由n个线程组成,在JVM中,又对进程以线程为单位对内存进行划分。android


线程的内存分配:
  •  栈[私有] : 
    • Java虚拟机栈
      • 栈帧
        1. 局部变量表
        2. 操做数栈
        3. 动态连接
        4. 方法返回地址
        5. 附加信息
    • 本地方法栈
    • 程序计数器
  •  堆[共享]: 
    • Java堆: 
      • 新生代 
      • 老年代 
    • 方法区: 
      • class信息:
        1. 类和接口的全限定名 
        2. 属性名称和描述符 
        3. 方法名称和描述符
        4. 运行时常量池
      • 编译后的代码

在操做系统看来,JVM是一个程序,而Java程序只是运行在程序上的程序,因此JVM须要模拟程序运行的环境。程序员


(图片来源:csdn-骁兵
web

  • Java虚拟机栈。Java栈由不少个栈帧组成,每个栈帧表明一个方法,而栈帧由局部变量表,操做数栈,动态连接,返回值地址以及一些附加信息组成,栈是方法的生存之地,当方法被调用的时候:
          1. 将调用方的地址入栈,也就是方法返回地址
          2. 给方法开辟栈帧,具体这个栈帧的须要多大的空间,在class文件就能够获得。
          3. 初始化栈帧空间。
          4. 将参数压入局部变量表。
          5. 将参数和局部变量压入局部变量表。
          6. 操做栈和程序计数器工做。
          7. 执行到方法返回指令,回到调用点。
      • 局部变量表。方法的执行其实就是值的存取,运算。因此方法须要以栈为基,在局部变量表中,以slot为单位,一个萝卜一个坑,用来存放int,short,float,boolean,char,byte,引用地址和返回值地址等。long 和 double 这两个不同,一个萝卜两个坑,由于他们是64位的,前面的是32位的。若是时基本数据类型,值保存在栈中,其余引用类型存在堆中,引用地址则保存在栈中,好比int[]。至于初始化局部变量表时须要多少坑位,在方法编译成class以后就定下来了。为了节省空间,坑位也会复用,好比a变量出了做用域,后面定义的b变量就会复用。

        public class Test {
        
            public void test(int b, int a) {
        
                int x = 6;
                if (b > 0) {
                    String str = "VeCharm";
                }
                int y = a;
                int c = y + b;
            }
        }
        ----------------
        javac Test.java
        javap -v Test
        ----------------
        class信息:
          Last modified 2019-3-31; size 347 bytes
          MD5 checksum b0e2fc2ec7a2d576136a693c77213446
          Compiled from "Test.java"
        public class com.vecharm.lychee.sample.api.Test
          minor version: 0
          major version: 52
          flags: ACC_PUBLIC, ACC_SUPER
        Constant pool:
          ...
        {
          public com.vecharm.lychee.sample.api.Test();
            descriptor: ()V
            flags: ACC_PUBLIC
            Code:
              stack=1, locals=1, args_size=1
                 0: aload_0
                 1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                 4: return
              LineNumberTable:
                line 3: 0
        
          public void test(int, int);
            descriptor: (II)V
            flags: ACC_PUBLIC
            Code:
              stack=2, locals=6, args_size=3
                 0: bipush        6
                 2: istore_3
                 3: iload_1
                 4: ifle          11
                 7: ldc           #2                  // String VeCharm
                 9: astore        4
                11: iload_2
                12: istore        4
                14: iload         4
                16: iload_1
                17: iadd
                18: istore        5
                20: return
              LineNumberTable:
                line 7: 0
                line 8: 3
                line 9: 7
                line 11: 11
                line 12: 14
                line 13: 20
              StackMapTable: number_of_entries = 1
                frame_type = 252 /* append */
                  offset_delta = 11
                  locals = [ int ]
        }
        SourceFile: "Test.java"    
        复制代码

      • 看test方法,咱们来逐步分析这些JVM指令
          1. bipush 6。将 6 push操做栈,当int取值-1~5采用iconst指令,取值-128~127采用bipush指令,取值-32768~32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令。
          2. istore_3。将6这个值从操做栈弹出,存入局部变量表3号坑,为啥是3号坑而不是1和2,由于这两个坑被参数b,和参数a栈了。
          3. iload_1。将局部变量表中的1号坑的值push操做栈,1号坑的是b的值。
          4. ifle 11。将操做栈弹出b的值,ifle这条指令的意思当栈顶int型数值小于等于0时跳转,跳转到11偏移地址。
          5. ldc #2。将int、float或String型常量值从常量池中推送至操做栈栈顶。
          6. astore 4。将操做栈栈顶的值弹出存入局部变量表4号坑,istore就是存int值和布尔值,fstore就是存float值,astore是存引用地址的。
          7. iload_2。取出2号坑的值push操做栈。
          8. istore 4。将操做栈顶的值存入4号坑,4号坑以前str已经用过了,可是出了做用域已经无用,因此能够复用。
          9. iload 4。取出4号坑的值push操做栈。
          10. iload_1。将局部变量表中的1号坑的值push操做栈,如今操做栈有两个值了,
          11. iadd。将操做栈的值相加。
          12. istore 5。将结果存入5号坑。
      • 看到这想必已经明白局部变量表的做用了。
      • 操做栈。JVM须要模拟cpu那样执行指令,但并没有法像cpu那样方便调用寄存器保存临时值。因此想了一个法子,在栈中划一块区域做为相似寄存器那样的功能。
      • 动态连接。Java做为一门多态的语言,确定少不了继承。有一Son类继承了Father类,重写了say()方法。当方法执行的时候,这个方法是属于Son这个版本仍是Father这个版本呢。因此就不能写死方法是谁的,而是搞一个符号,等到运行时才替换成真正的版本,这被称为动态分配。但也有某些方法签名是肯定永不变的,好比静态方法,私有方法等这些不可重写的方法的称为非虚方法,它们的分配称为静态分配,反之可重写的为虚方法。因为方法使用频繁,因此每个类配备一个虚方法表方便索引。在Java虚拟机提供了几条方法执行的指令。
        • invokestatic:调用static方法。
        • invokespecial:只能调用三类方法:<init>方法;final方法;private方法;super.method()。由于这三类方法的调用对象在编译时就能够肯定。
        • invokevirtual:调用虚方法。
        • invokeInterface:调用接口方法,会在运行时再肯定一个实现此接口的对象。
        • invokeDynamic:执行动态方法,它容许应用级别的代码来肯定执行哪个方法调用,先在运行时动态解析出调用点限定符所引用的方法,而后再执行该方法。
        • 方法信息保存在方法区的类信息里面。
      • 方法返回地址。调用点的地址。
  • 本地方法栈。执行native方法的栈。虚拟机能够自由实现它,在HotSopt虚拟机把本地方法栈和Java栈融合在一块儿。
  • 程序计数器。做为一个JVM虚拟机,它执行class字节码指令,须要记录代码执行到哪一条指令,换句话说也就是行号。JVM有200多条指令,最多不超过0xff条。若是感兴趣能够访问这个blog.csdn.net/lm2302293/a…
  • 堆。Java堆用来存储数据,类实例对象,全部线程共享。虽然不用关系释放,由垃圾处理器处理,但处理不慎仍是会有内存泄漏的问题。
  • 方法区。Java中很是重要的一个区域,因此它和堆同样,是被线程共享的,常量嘛,确定是共享的了。在方法区中,存储了每一个类的信息。在每一个类中存放:
      • 运行时常量池
        • 字面量
        • 字段符号引用/直接引用
        • 方法符号引用/直接引用
        • 属性
      • 字段数据。存放名称,类型,修饰符,属性。
      • 方法数据。存放名称,返回类型,参数类型,修饰符,属性。
      • 方法代码。
        • 签名和标志位
        • 字节码
        • 操做栈大小,本地变量表大小,本地变量表
        • 行号
        • 异常表。
          • 开始点
          • 终结点
          • 异常处理代码的位置
          • 异常类在常量池的索引
      • Classloader。
  • public class Test {
    
        public void test(int b, int a) {
    
            int x = 6;
            if (b > 0) {
                String str = "VeCharm";
            }
            int y = a;
            int c = y + b;
        }
    }
    ----------------
    javac Test.java
    javap -v Test
    ----------------
    class信息:
    ...
    Constant pool:
       #1 = Methodref #4.#14 // java/lang/Object."<init>":()V
       #2 = String #15 // VeCharm
       #3 = Class #16 // com/vecharm/lychee/sample/api/Test
       #4 = Class #17 // java/lang/Object
       #5 = Utf8 <init>
       #6 = Utf8 ()V
       #7 = Utf8 Code
       #8 = Utf8 LineNumberTable
       #9 = Utf8 test
      #10 = Utf8 (II)V
      #11 = Utf8 StackMapTable
      #12 = Utf8 SourceFile
      #13 = Utf8 Test.java
      #14 = NameAndType #5:#6 // "<init>":()V
      #15 = Utf8 VeCharm
      #16 = Utf8 com/vecharm/lychee/sample/api/Test
      #17 = Utf8 java/lang/Object
    ...
    SourceFile: "Test.java"    复制代码

    • 运行时常量池。每个类都分配一个运行时常量池,用来保存类的一些数据,按照类型分类。
      • 常见的常量池的数据项类型:
        CONSTANT_Utf8
        UTF-8编码的Unicode字符串
        CONSTANT_Integer
        int类型字面值
        CONSTANT_Float
        float类型字面值
        CONSTANT_Long
        long类型字面值
        CONSTANT_Double
        double类型字面值
        CONSTANT_Class
        对一个类或接口的符号引用
        CONSTANT_String
        String类型字面值
        CONSTANT_Fieldref
        对一个字段的符号引用
        CONSTANT_Methodref
        对一个类中声明的方法的符号引用
        CONSTANT_InterfaceMethodref
        对一个接口中声明的方法的符号引用
        CONSTANT_NameAndType
        对一个字段或方法的部分符号引用
    • 编译后的代码。一个Java类被编译成class代码,编译的时候并不能肯定类的地址,只能用符号代替,编译后的class文件,在ClassLoad而以后将会被提取分类保存在方法区,方法区保存的是类的信息,堆中保存的是类的对象,obj.getClass获取的信息就是在方法区的。方法区也会溢出,当方法区的信息超过了阈值也会OOM,好比使用动态代理MethodInterceptor。

看到这想必就已经知道了一个从一个Java文件到内存是如何运做的了。类从加载到虚拟机内存中开始到卸载内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,和卸载7个阶段,其中验证,准备,解析3个部分被称为链接。数据库

加载,验证,准备,初始化和卸载这5个阶段是肯定的,类的加载过程是必须按照顺序来,而解析阶段这个能够在初始化以后开始,这是为了支持运行时绑定(动态绑定)。编程

  1. 遇到new,getStatic,putStatic,invokeStatic这4条指令时,若是没有初始化,则须要先触发器初始化。
  2. 反射类的时候,会去常量池查查,若是没有就会加载,初始化。
  3. 当初始化一个类的,做为一个它的父类,若是没有初始化就会先进行初始化。
  4. 当虚拟机启动时,会先初始化用户指定的主类。
  5. 使用MethodHandle。

说到底,编程就是编的只是数据和指令,来总结一下流程。api

  1. 经过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生存一个表明这个类的java.lang.Class对象,做为方法区这个类各类数据的访问入口。这个对象比较特殊,它存在方法区,不在堆区。并设置加载此class的ClassLoader引用。
  4. 验证。验证代码的安全性。这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击。
  5. 准备。正式为类变量分配内存并设置类变量的初始值的阶段,这些变量所使用的内存都将在方法区进行分配,这时候分配的变量都是静态变量,不是实例变量,实例变量会在对象实例化时随着对象一块儿分配在Java堆中。
  6. 解析。解析阶段时虚拟将常量池内的符号引用替换为直接引用的过程。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定加载到内存中,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同。
  7. 初始化。静态方法使用<cinit>,实例对象使用<init>,对象存进Java堆。
  8. 寻找main方法执行,以后就是一个方法堆着一个方法的用了。

4、垃圾回收机制

在内存模型中,咱们须要重点关注的就是Heap。由于它是由咱们来控制的,处理不当容易发生OOM。内存处理的步骤无非也就三个: 申请,整理,清除。管理内存打个比方就和管理卖戏票的,观众台也就几十个座位,都是宝贵的资源。vip大户,里边走,直接坐贵席。其余的买计时票看,每隔必定时间把到时的人清出去,但常常有人到时赖着不走,隔一段时间催他才走。有时候座满了,只能把到时的赖皮清出去,不想走能够交钱。有时候人家成群结队的买票,天然要调配一下,清理出一些连座的给客户对吧。若是是一大帮人来看,更是欢迎,vip里面请。缓存

在Java的堆模型中划分为三个区。

  1. 新生代。这个区域的对象活动频繁,朝生暮死的。能活下来的对象最终会被转移到老年代,为了管理这些对象,新生代还进行更细的划分。
    • Eden
    • From Survivor
    • To Survivor
  2. 老年代。这个区域的对象存活比较久。通常能在GC下躲过15次的对象都会保存到这里。若是申请大内存空间的对象,也是直接分配到这里。分配担保,最坏的状况,新生代没有足够的内存分配,则会分配到老年代,固然也会分析老年代剩余空间,判断是否要进行一次Full GC。

管理对象的生命周期

生存仍是毁灭,是经过这个对象到GC Root的可到达性来决定的。能做为GC Root的对象有四种。

  • 虚拟机栈引用的对象
  • 方法区中常量引用的对象
  • 方法区中静态属性引用的变量
  • 本地方法栈中native方法引用的对象

引用类型有四种,强引用,软引用,弱引用,虚引用。

  1. 新生代对象的整理--复制整理法。这个区域因为活动频繁,容易更快的产生内存碎片,整理的时候还不能有大动做,因此这里使用复制法,对cpu停顿小,代价是占用必定的空间。
    • 若是发生Minor GC的时候,将Eden 存活下来的对象复制到 From Survivor ,对象在From Survivor每躲过一次GC 年龄就会+1,达到必定的程度,就会被移动到老年代,不然还没死的话,就会移动到To Survivor ,若是To Survivor放不下了,这个对象会被移动到老年代。最后清空Eden 和 From Survivor,接着将To 和 From 交换,当To Survivor满了就会将这些移动到老年代。
    • 如何保证新生代对象被老年代引用的时候不被gc?有些新生代对象会被老年代对象引用,然而老年代空间很大,若是每次Minor GC 都扫描一遍老年代,效率将大大下降,全部在老年代会划分一个小区域来管理卡表,这写卡表记录了老年代和新生代的引用,也就是说这些老年代被当成新生代的GC roots。
  2. 老年代对象的整理--标记整理法。这个区域整理的时间间隔比较长,由于它们都是比较长久的数据,因此可使用标记法来处理,但对cpu停顿大。
    • 初始化标记。这时候会暂停“全世界(stop-the-world)”,开始进行标记。仅仅标记GC Roots能直接关联到的对象。
    • 并发标记。从GC Roots开始进行可达性分析,找出存活对象,耗时长,就是进行追踪引用链的过程,可与用户线程并发执行。
    • 从新标记。修正并发标记阶段因用户线程继续运行而致使标记发生变化的那部分对象的标记记录。这个阶段也会再次暂停全部事件。
    • 并行清理。最后执行清理,这个阶段也是并行的。


结语,限于篇幅,只是初略的整理了一下大体的流程,参考《深刻Java虚拟机》等。
相关文章
相关标签/搜索