美文网首页Android开发技术Android进阶Android开发
Android知识点再理解总结(一)

Android知识点再理解总结(一)

作者: 慕涵盛华 | 来源:发表于2017-10-13 15:51 被阅读89次

    目录

    1.Service
    2.广播
    3.如何判断Activity是否在运行
    4.自定义View
    5.理解Java内存
    6.线程安全问题理解
    7.布局问题

    前言

    本文主要从原理上深入理解Android中常用的一些知识点,我们做开发时可能直接调用相应的API,没有关心一些原理知识。功能也可以照样实现。但理解掌握其原理面对问题时才会更从蓉。

    一.Service

    • Service的onCreate回调函数是不可以做耗时操作的,因为是在主线程中执行的。要执行耗时操作的话可以开启一个线程,或者使用IntentService,它是Service的一个子类,可以处理异步请求。在需要的时候创建,在任务执行完毕之后自动关闭(不需要考虑在什么时候关闭了)

    源码分析:在onStartCommand中回调了onStart,onStart中通过mServiceHandler发送消息到该handler的handleMessage中去。最后handleMessage中回调onHandleIntent(intent)。回调完成后回调用 stopSelf(msg.arg1),注意这个msg.arg1是个int值,相当于一个请求的唯一标识。每发送一个请求,会生成一个唯一的标识,然后将请求放入队列,当全部执行完成(最后一个请求也就相当于getLastStartId == startId),或者当前发送的标识是最近发出的那一个(getLastStartId == startId),则会销毁我们的Service.如果传入的是-1则直接销毁。当任务完成销毁Service回调onDestory,可以看到在onDestroy中释放了我们的Looper:mServiceLooper.quit()。

    二.广播

    • 有序广播和无序广播,有序广播会优先发给优先级高的,并且优先级高的可以决定是否发送给下一个。除此之外还有一种广播sendStickyBroadcast,这种在发送广播时Reciever还没有被注册,但它注册后还是可以收到在它之前发送的那条广播。有时候基于数据安全考虑,我们想发送广播只有自己(本进程)能接收到,那么该如何去做呢?使用LocalBroadcastManager类,Support V4包里的一个类。LocalBroadcastManager源码分析:单例实现,在私有化构造函数中,基于主线程的 Looper 新建了一个 Handler,在handleMessage中会调用接收器对广播的消息进行处理。注册接收器方法中,有两个HashMap,mReceivers 存储广播和过滤器信息,以BroadcastReceiver作为 key,IntentFilter链表作为 value。mReceivers 是接收器和IntentFilter的对应表,主要作用是方便在unregisterReceiver(…)取消注册,mActions 以Action为 key,注册这个Action的BroadcastReceiver链表为 value。mActions 的主要作用是方便在广播发送后快速得到可以接收它的BroadcastReceiver。发送广播:先根据Action从mActions中取出ReceiverRecord列表,循环每个ReceiverRecord判断 filter 和 intent 中的action、type、scheme、data、categoried 是否 match,是的话则保存到receivers列表中,发送 what 为MSG_EXEC_PENDING_BROADCASTS的消息,

    用广播来更新界面是否合适?更新界面也分很多种情况,如果不是频繁地刷新,使用广播来做也是可以的。但对于较频繁地刷新动作,建议还是不要使用这种方式。广播的发送和接收是有一定的代价的,它的传输是通过Binder进程间通信机制来实现的(细心人会发现Intent是实现了Parcelable接口的),那么系统定会为了广播能顺利传递做一些进程间通信的准备。除此之外,还可能有其他的因素让广播发送和到达是不准时的,这种情况可能吗?很可能,而且很容易发生。我们要先了解Android的ActivityManagerService有一个专门的消息队列来接收发送出来的广播,sendBroadcast执行完后就立即返回,但这时发送来的广播只是被放入到队列,并不一定马上被处理。当处理到当前广播时,又会把这个广播分发给注册的广播接收分发器ReceiverDispatcher,ReceiverDispatcher最后又把广播交给接Receiver所在的线程的消息队列去处理(就是你熟悉的UI线程的Message Queue)。整个过程从发送--ActivityManagerService--ReceiverDispatcher进行了两次Binder进程间通信,最后还要交到UI的消息队列,如果基中有一个消息的处理阻塞了UI,当然也会延迟你的onReceive的执行。

    三.如何判断Activity是否在运行?

    很多人可能都用过isFinishing来判断,用多了就会发现好象不太准,因为该方法直接返回mFinished,而mFinished是在finish()中被赋值的,也就是说只有通过调用finish()结束的Activity,mFinished的值才会被置为true。所以有时候Activity的生命周期没有按我们预想的来走时(如内存紧张时),会出现判断出错的情况。看看Google工程师是怎么判断的(AsyncTask中的onPostExecute片段):if (activity == null || activity.isDestroyed() || activity.isFinishing()) { return; }
    多了一个isDestroyed()的判断。

    四.自定义View

    • 做过自定义View的人很容易遇到这个问题,因为Activity转屏,或Home键到后台很容易在被系统销毁,恢复时我们肯定是希望看到View保持之前状态。自定义View的状态是如何保存的?

    可以随便从一个Android自带的控件中看到,如TextView的源代码;BaseSavedState是View的一个内部静态类,从代码上我们也很容易看出是把控件的属性(如selStart)打包到Parcel容器,Activity的onSaveInstanceState、onRestoreInstanceState最终也会调用到控件的这两个同名方法。无法保证系统会在销毁Activity前一定调用onSaveInstanceState,例如用户使用“返回” 按退出 Activity 时,因为用户的行为是在显式关闭 Activity,所以不会调用onSaveInstanceState。(在onSop之前调用)Activity类的onSaveInstanceState默认实现会恢复Activity的状态,默认实现会为布局中的每个View调用相应的 onSaveInstanceState方法,让每个View都能保存自身的信息。
    注意:想要保存View的状态,需要在XML布局文件中提供一个唯一的ID(android:id),如果没有设置这个ID的话,View控件的onSaveInstanceState是不会被调用的。

    自定义View控件的状态被保存需要满足两个条件:
    View有唯一的ID;
    View的初始化时要调用setSaveEnabled(true) ;

    里面的SparseArray(完整的参数是:SparseArray<Parcelable> )是一个KEY-VALUE的Map,KEY当然就是View的ID了。所以细看一下源码的调用过程,你一下就理解为什么一定要给View调置一个唯一的ID了。如果是通过new出来的View,如果设置了id状态也是会被保存的。

    五.理解Java内存

    Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。

    我们可以把上图的“运行时数据区”分为线程私有和共享数据区两大类。其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含Java堆、方法区,在方法区内有一个常量池。

    • 程序计数器 :记录正在执行的虚拟机字节码的地址

    由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。线程私有的。如果正在执行的是Natvie 方法,这个计数器值则空。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

    • 虚拟机栈(JVM Stack)

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

    • 本地方法栈(Native Method Stack)

    本地方法栈则为虚拟机使用到的Native方法提供内存空间。

    堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
    堆是垃圾收集器管理的主要区域

    • 方法区(Method Area)

    主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。

    • 常量池(Runtime Constant Pool)

    存放编译器生成的各种字面量和符号引用,是方法区的一部分。

    常量池

    Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。

    • 谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
    • 运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

    看一下下面的例子,以下面的例子来说明:

    String s1 = "Hello";
    String s2 = "Hello";
    String s3 = "Hel" + "lo";
    String s4 = "Hel" + new String("lo");
    String s5 = new String("Hello");
    String s6 = s5.intern();
    String s7 = "H";
    String s8 = "ello"; 
    String s9 = s7 + s8;        
    
    System.out.println(s1 == s2);  // true
    System.out.println(s1 == s3);  // true
    System.out.println(s1 == s4);  // false
    System.out.println(s1 == s9);  // false
    System.out.println(s4 == s5);  // false
    System.out.println(s1 == s6);  // true
    

    首先说明一点,在java 中,对于引用类型直接使用==操作符,比较的是两个字符串的引用地址

    s1 == s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。

    s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello";,所以s1 == s3成立。

    s1 == s4当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理所以地址肯定不同。

    s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,所以不做优化,等到运行时,s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。

    s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。

    s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。

    我们可以得出三个非常重要的结论:
    • 必须要关注编译期的行为,才能更好的理解常量池。

    • 运行时常量池中的常量,基本来源于各个class文件中的常量池。

    • 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池

    Java内存模型

    java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。程序中的变量存储在主内存中,每个线程拥有自己的工作内存并存放变量的拷贝,线程读写自己的工作内存,通过主内存进行变量的交互。JMM就是规定了工作内存和主内存之间变量访问的细节,通过保障原子性、有序性、可见性来实现线程的有效协同和数据的安全。

    • JVM如何判断一个对象实例是否应该被回收?

    标准答案: 垃圾回收器会建立有向图的方式进行内存管理,通过GC Roots来往下遍历,当发现有对象处于不可达状态的时候,就会对其标记为不可达,以便于后续的GC回收。

    • 说说JVM的垃圾回收策略。

    标准答案: JVM采用分代垃圾回收。在JVM的内存空间中把堆空间分为年老代和年轻代。将大量创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。

    六.线程安全问题理解

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

    同理,线程拷贝变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。先在线程私有的内存中修改,然后在合适的时候写回主内存,这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 也就是多线程安全问题。

    并发编程中的三个概念

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

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

    • 有序性:即程序执行的顺序按照代码的先后顺序执行。

    指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

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

    volatile关键字

    使用volatile关键字会强制将修改的值立即写入主存;保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证原子性。

    七.布局问题

    优化布局的嵌套:

    • merge标签的作用是合并UI布局,使用该标签能降低UI布局的嵌套层次。

    merge标签可用于两种情况:

    • 布局顶结点是FrameLayout且不需要设置background或padding等属性,可以用merge代替,因为Activity内容试图的parent view就是个FrameLayout,所以可以用merge消除只剩一个。
    • 某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中。
    • ViewStub:延时加载

    ViewStub标签引入的布局默认不会inflate,既不会显示也不会占用位置。 ViewStub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如数据加载进度布局、出错提示布局等。

    • include : 引入子布局

    将可复用的组件抽取出来并通过include标签使用,但<include>标签不能减少布局的层次。include主要解决的是相同布局的复用问题。

    RelativeLayout和LinearLayout性能PK

    RelativeLayout需要对其子View进行两次measure过程。而LinearLayout则只需一次measure过程,所以显然会快于RelativeLayout,但是如果LinearLayout中有weight属性,则也需要进行两次measure,但即便如此,应该仍然会比RelativeLayout的情况好一点。

    RelativeLayout对View的measure方法里对绘制过程做了一个优化,如果我们或者我们的子View没有要求强制刷新,而父View给子View的传入值也没有变化(也就是说子View的位置没变化),就不会做无谓的measure。但是上面已经说了RelativeLayout要做两次measure,而在做横向的测量时,纵向的测量结果尚未完成,只好暂时使用myHeight传入子View系统,假如子View的Height不等于(设置了margin)myHeight的高度,那么measure中上面代码所做得优化将不起作用,这一过程将进一步影响RelativeLayout的绘制性能。而LinearLayout则无这方面的担忧。解决这个问题也很好办,如果可以,尽量使用padding代替margin。

    • 1.RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,也会调用子View2次onMeasure
    • 2.RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
    • 3.在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。为什么Google给开发者默认新建了个RelativeLayout,而自己却在DecorView中用了个LinearLayout。因为DecorView的层级深度是已知而且固定的,上面一个标题栏,下面一个内容栏。采用RelativeLayout并不会降低层级深度,所以此时在根节点上用LinearLayout是效率最高的。而之所以给开发者默认新建了个RelativeLayout是希望开发者能采用尽量少的View层级来表达布局以实现性能最优,因为复杂的View嵌套对性能的影响会更大一些。

    新式布局

    • ConstraintLayout

    ConstraintLayout即约束布局,在2016年由Google I/O推出。ConstraintLayout和RelativeLayout有点类似,控件之间根据依赖关系而存在,但比RelativeLayout更加灵活。创建大型复杂的布局仍然可以使用扁平的层级(不用嵌套View Group),说的简单些就是,再复杂的界面也可以只有2层层次。

    • FlexBoxLayout

    FlexBoxLayout可以理解成一种更高级的LinearLayout,不过比LinearLayout更加强大和灵活。如果我们使用LinearLayout布局的话,那么不同的分辨率,也许我们要重新调整布局,势必会需要跟多的布局文件放在不同的资源目录。而使用FlexBoxLayout来布局的话,它可以适应各种界面的改变(所以叫响应式布局)。

    关注微信公众号获取更多相关资源

    Android小先生

    相关文章

      网友评论

        本文标题:Android知识点再理解总结(一)

        本文链接:https://www.haomeiwen.com/subject/onmpyxtx.html