JAVA

作者 | gexinzhao

地址 | http://www.jianshu.com/p/251b1bf72cd8

声明 | 本文是 gexinzhao 原创,已获授权发布,未经原作者允许请勿转载

如梦朦胧

九月份的时候有了换工作的躁动,然后投了某度的 Android 岗位,本以为像我这种非 211、985 没工作经验的渣渣只能被直接 pass,结果却意外的收到了电话,真是受宠若惊.经过电面,技术三面,然后就是等通知到最后拿到了 OFFER,如梦一般,真是挺激动的.

面试准备

当收到HR的面试的通知还是很懵逼的,因为感觉自己突然啥都不会了,迅速镇定下来,去网上找了一下某度的面试题,但是发现都只有提问了什么并没有对所提问题的解答,那只能自力更生,像做试卷一样,一遍总结一遍温顾.其实大多都是平时开发中用到的,只是我们没有总结过,被问起来的时候回答的难免会有点捉襟见肘,不能回答的很全面.下面为我个人总(bai)结(du)的,希望对你能有所帮助,但毕竟能力有限,有写的不对的地方,还望轻喷.虽然喷我我也不会改的.

因为本文篇幅较长建议收藏,在用到时候找出来看一眼.有一些知识点可能没涉及到,以后会加以补足.因为面试无非是考察你对技术的理解和总结,所以本篇的每个点总结的比较精简,只是让你大概的说出来,有的部分是需要能够画出原理图并进行解释说明,这个要在工作中多积累.

JAVA

一. 类的加载过程,Person person = new Person();为例进行说明。

1).因为new用到了Person.class,所以会先找到Person.class文件,并加载到内存中;

2).执行该类中的static代码块,如果有的话,给Person.class类进行初始化;

3).在堆内存中开辟空间分配内存地址;

4).在堆内存中建立对象的特有属性,并进行默认初始化;

5).对属性进行显示初始化;

6).对对象进行构造代码块初始化;

7).对对象进行与之对应的构造函数进行初始化;

8).将内存地址付给栈内存中的p变量


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


C++ 类对象的初始化顺序 ZZ

C++构造函数调用顺序

1.     创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);

2.    如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)

3.     基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;

4.     成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;

5.     派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)

综上可以得出,初始化顺序:

父类构造函数–>成员类对象构造函数–>自身构造函数

其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。
析构顺序和构造顺序相反

下面是最近笔试的某网络公司的题目:

#include <iostream>
using namespace std;

class A
{
      public:
            A(){ cout<<“A”<<endl;}
            virtual ~A(){ cout<<“~A”<<endl; }
};

class B: public A
{
      public:
             B(){ cout<<“B”<<endl;}
             ~B() {cout<<“~B”<<endl; }
      private:
              A a;
};

class C: public A, public B     //类在派生表中的继承列表
{
      public:
              C() {cout<<“C”<<endl;}
              ~C() {cout<<“~C”<<endl; }
      private:
              B b;
      public:
             A a;
};

int main()
{
    C * p = new C;
    delete p;
   
    system(“PAUSE”);
    return 0;
}

结果和分析:

A          //1 父类A的构造函数
A         //2 父类B中A的构造函数
A         //3 父类B中成员变量b初始化 (调用父类A的构造函数)
B         //4  父类B中成员变量b初始化 (调用父类B的构造函数)
A        //5   C中成员变量b 的构造
A
B
A         //6 C中成员变量a的构造
C         //7 C的构造函数最后调用 (finally ,-__-|||)
~C
~A
~B
~A
~A
~B
~A
~A
~A

====================================

综上可以看出,1~4 按照在派生表中的出现次序进行初始化,首先调用父类的构造函数

                          5, 6 调用 成员变量的构造函数

                         7 调用自身的构造函数

PS:更复杂的情况,可以试下虚继承。

在有虚继承和一般继承存在的情况下,优先虚继承

例如class C: public B, virtual public A

则先调用A的构造函数,再调用B的构造函数









+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

二. JVM相关知识,GC机制

JVM基本构成

从上图可知,JVM主要包括四个部分:

1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。(下图表示了从 java 源文件到 JVM 的整个过程,可配合理解。

2.执行引擎:负责执行class文件中包含的字节码指令;

3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域,如图:

  • 方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然 JVM 规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。

  • java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。

  • java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。

  • 程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。

  • 本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

4.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。

GC机制

垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?一般有以下几种方法:

引用计数法:

给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:

可达性分析算法:

以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象、本地方法中引用的对象等。

总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收。一般回收算法也有如下几种:

1).标记-清除(Mark-sweep)

2).复制(Copying

3).标记-整理(Mark-Compact)

4).分代收集算法

具体的解释可以参考本篇文章还不点我?


++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

本文是基于周志明的《深入理解Java虚拟机》

    堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象)

1、引用计数算法(Reference Counting)
    很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。
    引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。
    例如:
    在testGC()方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外这两个对象再无任何引用,实际上这两个对象都已经不能再被访问,但是它们因为相互引用着对象方,异常它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。
  1. /** 
  2.  * 执行后,objA和objB会不会被GC呢? 
  3.  */  
  4. public class ReferenceCountingGC {  
  5.     public Object instance = null;  
  6.   
  7.     private static final int _1MB = 1024 * 1024;  
  8.   
  9.     /** 
  10.      * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过 
  11.      */  
  12.     private byte[] bigSize = new byte[2 * _1MB];  
  13.   
  14.     public static void main(String[] args) {  
  15.         ReferenceCountingGC objA = new ReferenceCountingGC();  
  16.         ReferenceCountingGC objB = new ReferenceCountingGC();  
  17.         objA.instance = objB;  
  18.         objB.instance = objA;  
  19.   
  20.         objA = null;  
  21.         objB = null;  
  22.   
  23.         //假设在这行发生了GC,objA和ojbB是否被回收  
  24.         System.gc();  
  25.     }  
  26. }  
0.193: [GC 4418K->256K(61504K), 0.0046018 secs]
0.198: [Full GC 256K->160K(61504K), 0.0125962 secs]
    在运行结果中可以看到GC日志中包含"4418K->256K",老年代从4418K(大约4M,其实就是objA与objB)变为了141K,意味着虚拟并没有因为这两个对象相互引用就不回收它们,这也证明虚拟并不是通过通过引用计数算法来判断对象是否存活的。大家可以看到对象进入了老年代,但是大家都知道,对象刚创建的时候是分配在新生代中的,要进入老年代默认年龄要到了15才行,但这里objA与objB却进入了老年代。这是因为Java堆区会动态增长,刚开始时堆区较小,对象进入老年代还有一规则,当Survior空间中同一代的对象大小之和超过Survior空间的一半时,对象将直接进行老年代。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

了解java GC机制,必须先清楚在JVM内存区域的划分。

在java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:

java运行时内存划分

线程共享:堆区+方法区

线程私有:虚拟机栈+本地方法栈+程序计数器

一个线程就有一个程序计数器

虚拟机栈(JVM Stack):一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

本地方法栈(Native Method Statck):本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。



堆区(Heap):堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。







+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

---------------------------------------------------------------------------------------------------------

2、可达性分析算法(GC Roots Analysis):主流用这个判断

    在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

  在Java语言里,可作为GC Roots对象的包括如下几种:
    a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
    b.方法区中的类静态属性引用的对象
    c.方法区中的常量引用的对象
    d.本地方法栈中JNI的引用的对象
--------------------------------------------------------------------------------------------------------------

3、finalize()方法最终判定对象是否存活

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
    标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
  1).第一次标记并进行一次筛选。
    筛选的条件是此对象是否有必要执行finalize()方法。
    当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

  2).第二次标记
    如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
    Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
流程图如下:


  1. /** 
  2.  * 此代码演示了两点 
  3.  * 1、对象可以在被GC时自我拯救 
  4.  * 2、这种自救的机会只有一次,因为一个对象的finalize()方法最多只能被系统自动调用一次。 
  5.  */  
  6. public class FinalizeEscapeGC {  
  7.     public static FinalizeEscapeGC SAVE_HOOK = null;  
  8.   
  9.     public void isAlive() {  
  10.         System.out.println("yes, I am still alive");  
  11.     }  
  12.   
  13.     protected void finalize() throws Throwable {  
  14.         super.finalize();  
  15.         System.out.println("finalize method executed!");  
  16.         FinalizeEscapeGC.SAVE_HOOK = this;  
  17.     }  
  18.   
  19.     public static void main(String[] args) throws InterruptedException {  
  20.         SAVE_HOOK = new FinalizeEscapeGC();  
  21.   
  22.         //对象第一次成功拯救自己  
  23.         SAVE_HOOK = null;  
  24.         System.gc();  
  25.   
  26.         //因为finalize方法优先级很低,所有暂停0.5秒以等待它  
  27.         Thread.sleep(500);  
  28.         if (SAVE_HOOK != null) {  
  29.             SAVE_HOOK.isAlive();  
  30.         } else {  
  31.             System.out.println("no ,I am dead QAQ!");  
  32.         }  
  33.   
  34.         //-----------------------  
  35.         //以上代码与上面的完全相同,但这次自救却失败了!!!  
  36.         SAVE_HOOK = null;  
  37.         System.gc();  
  38.   
  39.         //因为finalize方法优先级很低,所有暂停0.5秒以等待它  
  40.         Thread.sleep(500);  
  41.         if (SAVE_HOOK != null) {  
  42.             SAVE_HOOK.isAlive();  
  43.         } else {  
  44.             System.out.println("no ,I am dead QAQ!");  
  45.         }  
  46.     }  
  47. }  
finalize method executed!
yew, I am still alive
no ,I am dead QAQ!
    从结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。
注意:任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。 并且建议大家尽量避免使用它   

Eclipse设置GC日志输出:
1.右键项目,选择properties。
2.选择run/debug setting, 在要执行的类点Edit,并如下图设置

这个方法是测试main方法用的


1、在eclipse根目录下的eclipse.ini配置文件中添加以下参数:
-verbose:gc (开启打印垃圾回收日志)
-Xloggc:eclipse_gc.log (设置垃圾回收日志打印的文件,文件名称可以自定义)
-XX:+PrintGCTimeStamps (打印垃圾回收时间信息时的时间格式)
-XX:+PrintGCDetails (打印垃圾回收详情)
添加完以上参数后当启动Eclipse后就能在Eclipse根目录看到一个eclipse_gc.log的gc日志文件
2、设置eclipse初始堆、非堆内存大小以及年轻代
-Xms50m –Xmx200m -XX:PermSize=30m -XX:MaxPermSize=60m
3、添加JVM监控参数
-Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.port=6688 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

gc日志:

Java HotSpot(TM) Client VM (25.25-b02) for windows-x86 JRE (1.8.0_25-b18), built on Oct  7 2014 14:31:05 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8340720k(3545144k free), swap 8787248k(2501780k free)
CommandLine flags: -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=943718400 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:-UseLargePagesIndividualAllocation
4.458: [GC (Allocation Failure) 4.458: [DefNew: 69952K->8704K(78656K), 0.0681410 secs] 69952K->22643K(253440K), 0.0690407 secs] [Times: user=0.06 sys=0.02, real=0.08 secs]
5.741: [Full GC (Metadata GC Threshold) 5.741: [Tenured: 13939K->33208K(174784K), 0.1036054 secs] 48618K->33208K(253440K), [Metaspace: 11442K->11442K(12672K)], 0.1041130 secs] [Times: user=0.11 sys=0.00, real=0.11 secs]
12.427: [GC (Allocation Failure) 12.427: [DefNew: 70016K->8704K(78720K), 0.0821340 secs] 103224K->55838K(253504K), 0.0822684 secs] [Times: user=0.08 sys=0.00, real=0.09 secs]
12.787: [Full GC (Metadata GC Threshold) 12.787: [Tenured: 47134K->56307K(174784K), 0.1662610 secs] 59785K->56307K(253504K), [Metaspace: 19215K->19215K(20864K)], 0.1663899 secs] [Times: user=0.16 sys=0.00, real=0.16 secs]
17.018: [GC (Allocation Failure) 17.018: [DefNew: 70016K->8704K(78720K), 0.0475891 secs] 126323K->68567K(253504K), 0.0477196 secs] [Times: user=0.03 sys=0.02, real=0.06 secs]
21.047: [GC (Allocation Failure) 21.047: [DefNew: 78720K->8704K(78720K), 0.0752255 secs] 138583K->83739K(253504K), 0.0753766 secs] [Times: user=0.08 sys=0.00, real=0.06 secs]
21.320: [Full GC (Metadata GC Threshold) 21.320: [Tenured: 75035K->61015K(174784K), 0.2800589 secs] 87423K->61015K(253504K), [Metaspace: 31969K->31969K(34176K)], 0.2802057 secs] [Times: user=0.28 sys=0.00, real=0.29 secs]

GC日志说明:
GC打印时间: [垃圾回收类型回收时间: [收集器名称: 年轻代回收前占用大小->年轻代回收后占用大小(年轻代当前容量),
年轻代局部GC时JVM暂停处理的时间] 堆空间GC前占用的空间->堆空间GC后占用的空间(堆空间当前容量)
,GC过程中JVM暂停处理的时间]。
垃圾回收类型:分为GC和Full GC.
GC一般为堆空间某个区发生了垃圾回收,
Full GC基本都是整个堆空间及持久代发生了垃圾回收,通常优化的目标之一是尽量减少GC和Full GC的频率。
收集器名称:一般都为收集器的简称或别名,通过收集器名称基本都能判断出那个区发生了GC。
DefNew:年轻代(新生代)发生了GC (若为DefNew可知当前JVM年轻代使用的串行收集器)
ParNew:年轻代(新生代)发生了GC (若为ParNew可知当前JVM年轻代使用了并行收集器)
Tenured:老年代发生了GC
Perm:持久代发生了GC


四、引用

   无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
    在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。  
    强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
    软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
   弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
    虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。



++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

三. 类的加载器,双亲机制,Android的类加载器。

类的加载器

大家都知道,当我们写好一个 Java 程序之后,不是管是 CS 还是 BS应用,都是由若干个 .class 文件组织而成的一个完整的 Java 应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的 class 文件当中,所以经常要从这个 class 文件中要调用另外一个 class 文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有 class 文件,而是根据程序的需要,通过 Java 的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。

双亲机制

1、原理介绍

ClassLoader 使用的是双亲委托模型来搜索类的,每个 ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException 异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的 Class实例对象。

2、为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的 String,除非你改变 JDK 中 ClassLoader 搜索类的默认算法。

3、但是 JVM 在搜索类的时候,又是如何判定两个 class 是相同的呢?

JVM 在判定两个 class 是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM 才认为这两个 class 是相同的。就算两个 class 是同一份 class 字节码,如果被两个不同的 ClassLoader 实例所加载,JVM 也会认为它们是两个不同 class。比如网络上的一个 Java 类org.classloader.simple.NetClassLoaderSimple,javac 编译之后生成字节码文件 NetClassLoaderSimple.class,ClassLoaderA 和ClassLoaderB 这两个类加载器并读取了 NetClassLoaderSimple.class文件,并分别定义出了 java.lang.Class 实例来表示这个类,对于 JVM 来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个 Class 实例生成具体的对象进行转换时,就会抛运行时异常 java.lang.ClassCaseException,提示这是两个不同的类型。

Android类加载器

对于 Android 而言,最终的 apk 文件包含的是 dex 类型的文件,dex文件是将 class 文件重新打包,打包的规则又不是简单地压缩,而是完全对class 文件内部的各种函数表,变量表进行优化,产生一个新的文件,即dex文件。因此加载这种特殊的 Class 文件就需要特殊的类加载器DexClassLoader。

四. 集合框架,list,map,set都有哪些具体的实现类,区别都是什么?

1.List,Set都是继承自 Collection 接口,Map 则不是;

2.List特点:元素有放入顺序,元素可重复;

  Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注    意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的        HashCode决定的,其位置其实是固定的,加入Set 的Object必须定    义equals()方法;

  另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是 set只能用迭代,因为他无序,无法用下标来取得想要的值)。

3.Set和List对比:

  Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元  素位置改变。

  List:和数组类似,List可以动态增长,查找元素效率高,插入删除元 素效率低,因为会引起其他元素位置改变。

4.Map适合储存键值对的数据。

5.线程安全集合类与非线程安全集合类

LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;

HashMap是非线程安全的,HashTable是线程安全的;

StringBuilder是非线程安全的,StringBuffer是线程安全的。

下面是这些类具体的使用介绍:

ArrayList与LinkedList的区别和适用场景

Arraylist:

优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。

缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。

LinkedList:

优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。

缺点:因为LinkedList要移动指针,所以查询操作性能比较低。

适用场景分析:

当需要对数据进行对此访问的情况下选用 ArrayList,当需要对数据进行多次增加删除修改时采用 LinkedList。

ArrayList 与 Vector 的区别和适用场景

ArrayList有三个构造方法:

public ArrayList(int initialCapacity)//构造一个具有指定初始容量的空列表。    
public ArrayList()//构造一个初始容量为10的空列表。    
public ArrayList(Collection<? extends E> c)//构造一个包含指定 collection 的元素的列表

Vector有四个构造方法:

public Vector()//使用指定的初始容量和等于零的容量增量构造一个空向量。    
public Vector(int initialCapacity)//构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。    
public Vector(Collection<? extends E> c)//构造一个包含指定 collection 中的元素的向量    
public Vector(int initialCapacity,int capacityIncrement)//使用指定的初始容量和容量增量构造一个空的向量

ArrayList 和 Vector 都是用数组实现的,主要有这么三个区别:

1).Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比;

2).两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。

3).Vector可以设置增长因子,而ArrayList不可以。

4).Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

适用场景:

1.Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。

2.如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。

HashSet与Treeset的适用场景

1.TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset中的数据是自动排好序的,不允许放入null值。

2.HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。

3.HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例。

适用场景分析:

HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。

HashMap与TreeMap、HashTable的区别及适用场景

HashMap 非线程安全 

HashMap:基于哈希表(散列表)实现。使用 HashMap 要求添加的键类明确定义了 hashCode() 和 equals()[可以重写 hashCode() 和equals()],为了优化 HashMap 空间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主要分两种,一种是开放定址法,另一种是链表法。HashMap 的实现中采用的是链表法。

TreeMap:非线程安全基于红黑树实现。TreeMap 没有调优选项,因为该树总处于平衡状态。

适用场景分析:

HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。

HashMap:适用于Map中插入、删除和定位元素。

Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

(ps:其实我们工作的过程中对集合的使用是很频繁的,稍加注意并总结积累一下,在面试的时候应该会回答的很轻松)

五. concurrentHashmap原理,原子类。

ConcurrentHashMap 作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比 HashTable 的全表锁在性能上的提升非常之大.

六. volatile原理

在《Java并发编程:核心理论》一文中,我们已经提到过可见性、有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果对Synchronized原理有了解的话,应该知道Synchronized是一个比较重量级的操作,对系统的性能有比较大的影响,所以,如果有其他解决方案,我们通常都避免使用Synchronized来解决问题。而volatile关键字就是Java中提供的另一种解决可见性和有序性问题的方案。对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

参考文章 

https://link.jianshu.com/?t=https://www.cnblogs.com/paddix/p/5428507.html

七. 多线程的使用场景

使用多线程就一定效率高吗? 有时候使用多线程并不是为了提高效率,而是使得CPU能够同时处理多个事件。

1).为了不阻塞主线程,启动其他线程来做好事的事情,比如APP中耗时操作都不在UI中做.

2).实现更快的应用程序,即主线程专门监听用户请求,子线程用来处理用户请求,以获得大的吞吐量.感觉这种情况下,多线程的效率未必高。 这种情况下的多线程是为了不必等待, 可以并行处理多条数据。

比如JavaWeb的就是主线程专门监听用户的HTTP请求,然后启动子线程去处理用户的HTTP请求。

3).某种虽然优先级很低的服务,但是却要不定时去做。

比如Jvm的垃圾回收。

4.)某种任务,虽然耗时,但是不耗CPU的操作时,开启多个线程,效率会有显著提高。

比如读取文件,然后处理。 磁盘IO是个很耗费时间,但是不耗CPU计算的工作。 所以可以一个线程读取数据,一个线程处理数据。肯定比一个线程读取数据,然后处理效率高。 因为两个线程的时候充分利用了CPU等待磁盘IO的空闲时间。

八. JAVA常量池

Interger 中的128(-128~127)

a.当数值范围为-128~127时:如果两个 new 出来 Integer 对象,即使值相同,通过“==”比较结果为false,但两个对象直接赋值,则通过“==”比较结果为“true,这一点与String非常相似。

b.当数值不在-128~127时,无论通过哪种方式,即使两个对象的值相等,通过“==”比较,其结果为false;

c.当一个Integer对象直接与一个int基本数据类型通过“==”比较,其结果与第一点相同;

d.Integer对象的hash值为数值本身;

为什么是-128-127?

在 Integer 类中有一个静态内部类 IntegerCache,在 IntegerCache类中有一个 Integer 数组,用以缓存当数值范围为-128~127时的Integer 对象。

九. 简单介绍一下java中的泛型,泛型擦除以及相关的概念。

泛型是 Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。

在 Java SE 1.5 之前,没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。

2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

3、泛型的类型参数可以有多个。

4、泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上称为“有界类型”。

5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName("java.lang.String");

泛型擦除以及相关的概念

Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

类型擦除引起的问题及解决方法

1、先检查,在编译,以及检查编译的对象和引用传递的问题

2、自动类型转换

3、类型擦除与多态的冲突和解决方法

4、泛型类型变量不能是基本数据类型

5、运行时类型查询

6、异常中使用泛型的问题

7、数组(这个不属于类型擦除引起的问题)

9、类型擦除后的冲突

10、泛型在静态方法和静态类中的问题

Android

一. Handler机制

Android 的消息机制也就是 handler 机制,创建 handler 的时候会创建一个 looper ( 通过 looper.prepare() 来创建 ),looper 一般为主线程 looper.

handler 通过 send 发送消息 (sendMessage) ,当然 post 一系列方法最终也是通过 send 来实现的,在 send 方法中handler 会通过 enqueueMessage() 方法中的 enqueueMessage(msg,millis )向消息队列 MessageQueue 插入一条消息,同时会把本身的 handler 通过 msg.target = this 传入.

Looper 是一个死循环,不断的读取MessageQueue中的消息,loop 方法会调用 MessageQueue 的 next 方法来获取新的消息,next 操作是一个阻塞操作,当没有消息的时候 next 方法会一直阻塞,进而导致 loop 一直阻塞,当有消息的时候,Looper 就会处理消息 Looper 收到消息之后就开始处理消息: msg.target.dispatchMessage(msg),当然这里的 msg.target 就是上面传过来的发送这条消息的 handler 对象,这样 handler 发送的消息最终又交给他的dispatchMessage方法来处理了,这里不同的是,handler 的 dispatchMessage 方法是在创建 Handler时所使用的 Looper 中执行的,这样就成功的将代码逻辑切换到了主线程了.

Handler 处理消息的过程是:首先,检查Message 的 callback 是否为 null,不为 null 就通过 handleCallBack 来处理消息,Message 的 callback 是一个 Runnable 对象,实际上是 handler 的 post 方法所传递的 Runnable 参数.其次是检查 mCallback 是 否为 null,不为 null 就调用 mCallback 的handleMessage 方法来处理消息.

二. View的绘制流程

View 的绘制流程:OnMeasure()——>OnLayout()——>OnDraw()

各步骤的主要工作:

OnMeasure():

测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

OnLayout():

确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

OnDraw():

绘制视图:ViewRoot创建一个Canvas对象,然后调用OnDraw()。六个步骤:①、绘制视图的背景;②、保存画布的图层(Layer);③、绘制View的内容;④、绘制View子视图,如果没有就不用;⑤、还原图层(Layer);⑥、绘制滚动条。

三. 事件传递机制

1).Android事件分发机制的本质是要解决:点击事件由哪个对象发出,经过哪些对象,最终达到哪个对象并最终得到处理。这里的对象是指Activity、ViewGroup、View.

2).Android中 事件分发顺序:Activity(Window) -> ViewGroup -> View.

3).事件分发过程由 dispatchTouchEvent() 、onInterceptTouchEvent() 和 onTouchEvent() 三个方法协助完成

设置 Button 按钮来响应点击事件事件传递情况:(如下图)

布局如下:

最外层:Activiy A,包含两个子View:ViewGroup B、View C

中间层:ViewGroup B,包含一个子View:View C

最内层:View C

假设用户首先触摸到屏幕上 View C 上的某个点(如图中黄色区域),那么 Action_DOWN 事件就在该点产生,然后用户移动手指并最后离开屏幕。

按钮点击事件:

DOWN 事件被传递给 C 的 onTouchEvent 方法,该方法返回 true,表示处理这个事件;因为 C 正在处理这个事件,那么 DOWN事件将不再往上传递给 B 和 A 的onTouchEvent();该事件列的其他事件(Move、Up)也将传递给 C 的 onTouchEvent();

(记住这个图的传递顺序,面试的时候能够画出来,就很详细了)

四. Binder机制

1.了解Binder

在 Android 系统中,每一个应用程序都运行在独立的进程中,这也保证了当其中一个程序出现异常而不会影响另一个应用程序的正常运转。在许多情况下,我们activity都会与各种系统的service打交道,很显然,我们写的程序中activity与系统service肯定不是同一个进程,但是它们之间是怎样实现通信的呢?所以Binder是android中一种实现进程间通信(IPC)的方式之一。

1).首先,Binder分为Client和Server两个进程。

注意,Client和Server是相对的。谁发消息,谁就是Client,谁接收消息,谁就是Server。

举个例子,两个进程A和B之间使用Binder通信,进程A发消息给进程B,那么这时候A是Binder Client,B是Binder Server;进程B发消息给进程A,那么这时候B是Binder Client,A是Binder Server——其实这么说虽然简单了,但还是不太严谨,我们先这么理解着。

2).其次,我们看下面这个图(摘自田维术的博客),基本说明白了Binder的组成解构:

图中的IPC就是进程间通信的意思。

图中的 ServiceManager,负责把 Binder Server注册到一个容器中。

有人把 ServiceManager 比喻成电话局,存储着每个住宅的座机电话,还是很恰当的。张三给李四打电话,拨打电话号码,会先转接到电话局,电话局的接线员查到这个电话号码的地址,因为李四的电话号码之前在电话局注册过,所以就能拨通;没注册,就会提示该号码不存在。

对照着 Android Binder 机制,对着上面这图,张三就是 Binder Client,李四就是Binder Server,电话局就是ServiceManager,电话局的接线员在这个过程中做了很多事情,对应着图中的Binder驱动.

3).接下来我们看 Binder 通信的过程,还是摘自田维术博客的一张图:

注:图中的SM也就是ServiceManager。

我们看到,Client想要直接调用Server的add方法,是不可以的,因为它们在不同的进程中,这时候就需要Binder来帮忙了。

首先是Server在SM这个容器中注册。其次,Client想要调用Server的add方法,就需要先获取Server对象, 但是SM不会把真正的Server对象返回给Client,而是把Server的一个代理对象返回给Client,也就是Proxy。然后,Client调用Proxy的add方法,SM会帮他去调用Server的add方法,并把结果返回给Client。

以上这3步,Binder驱动出了很多力,但我们不需要知道Binder驱动的底层实现,涉及到C++的代码了——把有限的时间去做更有意义的事情。

(ps:以上节选自包建强老师的文章点我点我).

2.为什么 android 选用 Binder 来实现进程间通信?

1).可靠性。在移动设备上,通常采用基于 Client-Server 的通信方式来实现互联网与设备间的内部通信。目前 linux 支持 IPC 包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及 socket 中只有 socket 支持 Client-Server 的通信方式。Android 系统为开发者提供了丰富进程间通信的功能接口,媒体播放,传感器,无线传输。这些功能都由不同的 server 来管理。开发都只关心将自己应用程序的client与 server 的通信建立起来便可以使用这个服务。毫无疑问,如若在底层架设一套协议来实现 Client-Server 通信,增加了系统的复杂性。在资源有限的手机 上来实现这种复杂的环境,可靠性难以保证。

2).传输性能。socket主要用于跨网络的进程间通信和本机上进程间的通信,但传输效率低,开销大。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的一块缓存区中,然后从内核缓存区拷贝到接收方缓存区,其过程至少有两次拷贝。虽然共享内存无需拷贝,但控制复杂。比较各种IPC方式的数据拷贝次数。共享内存:0次。Binder:1次。Socket/管道/消息队列:2次。

3).安全性。Android是一个开放式的平台,所以确保应用程序安全是很重要的。Android对每一个安装应用都分配了UID/PID,其中进程的UID是可用来鉴别进程身份。传统的只能由用户在数据包里填写UID/PID,这样不可靠,容易被恶意程序利用。而我们要求由内核来添加可靠的UID。

所以,出于可靠性、传输性、安全性。android建立了一套新的进程间通信方式。

五. Android中进程的级别,以及各自的区别。

1、前台进程

用户当前正在做的事情需要这个进程。如果满足下面的条件之一,一个进程就被认为是前台进程:

1).这个进程拥有一个正在与用户交互的Activity(这个Activity的onResume()方法被调用)。

2).这个进程拥有一个绑定到正在与用户交互的activity上的Service。

3).这个进程拥有一个前台运行的Service(service调用了方法startForeground()).

4).这个进程拥有一个正在执行其任何一个生命周期回调方法(onCreate(),onStart(),或onDestroy())的Service。

5).这个进程拥有正在执行其onReceive()方法的BroadcastReceiver。

通常,在任何时间点,只有很少的前台进程存在。它们只有在达到无法调合的矛盾时才会被杀--如内存太小而不能继续运行时。通常,到了这时,设备就达到了一个内存分页调度状态,所以需要杀一些前台进程来保证用户界面的反应.

2、可见进程

一个进程不拥有运行于前台的组件,但是依然能影响用户所见。满足下列条件时,进程即为可见:

这个进程拥有一个不在前台但仍可见的Activity(它的onPause()方法被调用)。当一个前台activity启动一个对话框时,就出了这种情况。

3、服务进程

一个可见进程被认为是极其重要的。并且,除非只有杀掉它才可以保证所有前台进程的运行,否则是不能动它的。这个进程拥有一个绑定到可见activity的Service。一个进程不在上述两种之内,但它运行着一个被startService()所启动的service。

尽管一个服务进程不直接影响用户所见,但是它们通常做一些用户关心的事情(比如播放音乐或下载数据),所以系统不到前台进程和可见进程活不下去时不会杀它。

4、后台进程

一个进程拥有一个当前不可见的activity(activity的onStop()方法被调用)。

这样的进程们不会直接影响到用户体验,所以系统可以在任意时刻杀了它们从而为前台、可见、以及服务进程们提供存储空间。通常有很多后台进程在运行。它们被保存在一个LRU(最近最少使用)列表中来确保拥有最近刚被看到的activity的进程最后被杀。如果一个activity正确的实现了它的生命周期方法,并保存了它的当前状态,那么杀死它的进程将不会对用户的可视化体验造成影响。因为当用户返回到这个activity时,这个activity会恢复它所有的可见状态。

5、空进程

一个进程不拥有入何active组件。保留这类进程的唯一理由是高速缓存,这样可以提高下一次一个组件要运行它时的启动速度。系统经常为了平衡在进程高速缓存和底层的内核高速缓存之间的整体系统资源而杀死它们。

六. 线程池的相关知识。

Android中的线程池都是之间或间接通过配置ThreadPoolExecutor来实现不同特性的线程池.Android中最常见的四类具有不同特性的线程池分别为FixThreadPool、CachedThreadPool、SingleThreadPool、ScheduleThreadExecutor.

1).FixThreadPool

只有核心线程,并且数量固定的,也不会被回收,所有线程都活动时,因为队列没有限制大小,新任务会等待执行.

优点:更快的响应外界请求.

2).SingleThreadPool

只有一个核心线程,确保所有的任务都在同一线程中按顺序完成.因此不需要处理线程同步的问题.

3).CachedThreadPool

只有非核心线程,最大线程数非常大,所有线程都活动时,会为新任务创建新线程,否则会利用空闲线程(60s空闲时间,过了就会被回收,所以线程池中有0个线程的可能)处理任务.

优点:任何任务都会被立即执行(任务队列SynchronousQueue相当于一个空集合);比较适合执行大量的耗时较少的任务.

4).ScheduledThreadPool

核心线程数固定,非核心线程(闲着没活干会被立即回收)数没有限制.

优点:执行定时任务以及有固定周期的重复任务

参考Android开发——Android中常见的4种线程池(保证你能看懂并理解)

七. 内存泄露,怎样查找,怎么产生的内存泄露。

产生的内存泄露

1).资源对象没关闭造成的内存泄漏

2).构造Adapter时,没有使用缓存的convertView

3).Bitmap对象不在使用时调用recycle()释放内存

4).试着使用关于application的context来替代和activity相关的context

5).注册没取消造成的内存泄漏

6).集合中对象没清理造成的内存泄漏

查找内存泄漏

查找内存泄漏可以使用Android Stdio 自带的Android Profiler工具,也可以使用Square产品的LeadCanary.

八. Android优化

性能优化

    1).节制的使用Service 如果应用程序需要使用Service来执行后台任务的话,只有当任务正在执行的时候才应该让Service运行起来。当启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,系统可以在LRUcache当中缓存的进程数量也会减少,导致切换程序的时候耗费更多性能。我们可以使用IntentService,当后台任务执行结束后会自动停止,避免了Service的内存泄漏。

    2).当界面不可见时释放内存 当用户打开了另外一个程序,我们的程序界面已经不可见的时候,我们应当将所有和界面相关的资源进行释放。重写Activity的onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发说明用户离开了程序,此时就可以进行资源释放操作了。

    3).当内存紧张时释放内存 onTrimMemory()方法还有很多种其他类型的回调,可以在手机内存降低的时候及时通知我们,我们应该根据回调中传入的级别来去决定如何释放应用程序的资源。

    4).避免在Bitmap上浪费内存 读取一个Bitmap图片的时候,千万不要去加载不需要的分辨率。可以压缩图片等操作。

    5).使用优化过的数据集合 Android提供了一系列优化过后的数据集合工具类,如SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。

布局优化

1).重用布局文件

标签可以允许在一个布局当中引入另一个布局,那么比如说我们程序的所有界面都有一个公共的部分,这个时候最好的做法就是将这个公共的部分提取到一个独立的布局中,然后每个界面的布局文件当中来引用这个公共的布局。

Tips:如果我们要在标签中覆写layout属性,必须要将layout_width和layout_height这两个属性也进行覆写,否则覆写效果将不会生效。

标签是作为标签的一种辅助扩展来使用的,它的主要作用是为了防止在引用布局文件时引用文件时产生多余的布局嵌套。布局嵌套越多,解析起来就越耗时,性能就越差。因此编写布局文件时应该让嵌套的层数越少越好。

举例:比如在LinearLayout里边使用一个布局。里边又有一个LinearLayout,那么其实就存在了多余的布局嵌套,使用merge可以解决这个问题。

2).仅在需要时才加载布局

某个布局当中的元素不是一起显示出来的,普通情况下只显示部分常用的元素,而那些不常用的元素只有在用户进行特定操作时才会显示出来。

举例:填信息时不是需要全部填的,有一个添加更多字段的选项,当用户需要添加其他信息的时候,才将另外的元素显示到界面上。用VISIBLE 性能表现一般,可以用 ViewStub。ViewStub 也是 View 的一种,但是没有大小,没有绘制功能,也不参与布局,资源消耗非常低,可以认为完全不影响性能。

tips:ViewStub 所加载的布局是不可以使用标签的,因此这有可能导致加载出来出来的布局存在着多余的嵌套结构。

高性能编码优化

都是一些微优化,在性能方面看不出有什么显著的提升的。使用合适的算法和数据结构是优化程序性能的最主要手段。

1).避免创建不必要的对象 不必要的对象我们应该避免创建:

如果有需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。

当一个方法的返回值是String的时候,通常需要去判断一下这个String的作用是什么,如果明确知道调用方会将返回的String再进行拼接操作的话,可以考虑返回一个StringBuffer对象来代替,因为这样可以将一个对象的引用进行返回,而返回String的话就是创建了一个短生命周期的临时对象。

尽可能地少创建临时对象,越少的对象意味着越少的GC操作。

2).在没有特殊原因的情况下,尽量使用基本数据类型来代替封装数据类型,int比Integer要更加有效,其它数据类型也是一样。

基本数据类型的数组也要优于对象数据类型的数组。另外两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效的多。

3).静态优于抽象

如果你并不需要访问一个对系那个中的某些字段,只是想调用它的某些方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,调用速度提升15%-20%,同时也不用为了调用这个方法去专门创建对象了,也不用担心调用这个方法后是否会改变对象的状态(静态方法无法访问非静态字段)。

4).对常量使用static final修饰符

static int intVal = 42;  
static String strVal = "Hello, world!";  

编译器会为上面的代码生成一个初始方法,称为方法,该方法会在定义类第一次被使用的时候调用。这个方法会将42的值赋值到intVal当中,从字符串常量表中提取一个引用赋值到strVal上。当赋值完成后,我们就可以通过字段搜寻的方式去访问具体的值了。

final进行优化:

static final int intVal = 42;  
static final String strVal = "Hello, world!";  

这样,定义类就不需要方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。当我们调用intVal时可以直接指向42的值,而调用strVal会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。

这种优化方式只对基本数据类型以及String类型的常量有效,对于其他数据类型的常量是无效的。

5).使用增强型for循环语法

static class Counter   

Counter[] mArray = ...  

public void zero()   }  

public void one()   
}  

public void two()   
}

zero()最慢,每次都要计算mArray的长度,one()相对快得多,two()fangfa在没有JIT(Just In Time Compiler)的设备上是运行最快的,而在有JIT的设备上运行效率和one()方法不相上下,需要注意这种写法需要JDK1.5之后才支持。

Tips:ArrayList手写的循环比增强型for循环更快,其他的集合没有这种情况。因此默认情况下使用增强型for循环,而遍历ArrayList使用传统的循环方式。

6).多使用系统封装好的API

系统提供不了的Api完成不了我们需要的功能才应该自己去写,因为使用系统的Api很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的。 举个例子,实现数组拷贝的功能,使用循环的方式来对数组中的每一个元素一一进行赋值当然可行,但是直接使用系统中提供的System.arraycopy()方法会让执行效率快9倍以上。

7).避免在内部调用Getters/Setters方法

面向对象中封装的思想是不要把类内部的字段暴露给外部,而是提供特定的方法来允许外部操作相应类的内部字段。但在Android中,字段搜寻比方法调用效率高得多,我们直接访问某个字段可能要比通过getters方法来去访问这个字段快3到7倍。但是编写代码还是要按照面向对象思维的,我们应该在能优化的地方进行优化,比如避免在内部调用getters/setters方法。

九. 插件化相关技术,热修补技术是怎样实现的,和插件化有什么区别

相同点:

都使用ClassLoader来实现的加载的新的功能类,都可以使用PathClassLoader与DexClassLoader

不同点:

热修复因为是为了修复Bug的,所以要将新的同名类替代同名的Bug类,要抢先加载新的类而不是Bug类,所以多做两件事:在原先的app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志,还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements,这样才能抢先代替Bug类,完成系统不加载旧的Bug类.

而插件化只是增肌新的功能类或者是资源文件,所以不涉及抢先加载旧的类这样的使命,就避过了阻止相关类去打上CLASS_ISPREVERIFIED标志和还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements.

所以插件化比热修复简单,热修复是在插件化的基础上在进行替旧的Bug类

十. 怎样计算一张图片的大小,加载bitmap过程(怎样保证不产生内存溢出),二级缓存,LRUCache算法。

计算一张图片的大小

图片占用内存的计算公式:图片高度 * 图片宽度 * 一个像素占用的内存大小.所以,计算图片占用内存大小的时候,要考虑图片所在的目录跟设备密度,这两个因素其实影响的是图片的高宽,android会对图片进行拉升跟压缩。

加载bitmap过程(怎样保证不产生内存溢出)

由于Android对图片使用内存有限制,若是加载几兆的大图片便内存溢出。Bitmap会将图片的所有像素(即长x宽)加载到内存中,如果图片分辨率过大,会直接导致内存OOM,只有在BitmapFactory加载图片时使用BitmapFactory.Options对相关参数进行配置来减少加载的像素。

BitmapFactory.Options相关参数详解

(1).Options.inPreferredConfig值来降低内存消耗。

比如:默认值ARGB_8888改为RGB_565,节约一半内存。

(2).设置Options.inSampleSize 缩放比例,对大图片进行压缩 。

(3).设置Options.inPurgeable和inInputShareable:让系统能及时回 收内存。

A:inPurgeable:设置为True时,表示系统内存不足时可以被回 收,设置为False时,表示不能被回收。

B:inInputShareable:设置是否深拷贝,与inPurgeable结合使用,inPurgeable为false时,该参数无意义。

(4).使用decodeStream代替其他方法。

decodeResource,setImageResource,setImageBitmap等方法

十一. LRUCache算法是怎样实现的。

内部存在一个LinkedHashMap和maxSize,把最近使用的对象用强引用存储在 LinkedHashMap中,给出来put和get方法,每次put图片时计算缓存中所有图片总大小,跟maxSize进行比较,大于maxSize,就将最久添加的图片移除;反之小于maxSize就添加进来。

之前,我们会使用内存缓存技术实现,也就是软引用或弱引用,在Android 2.3(APILevel 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。

算法

m * n的矩阵,能形成几个正方形(2 * 2能形成1个正方形,2 * 3 2个,3 * 3 6个)

计数的关键是要观察到任意一个倾斜的正方形必然唯一内接于一个非倾斜的正方形,而一个非倾斜的边长为k的非倾斜正方形,一条边上有k-1个内点,每个内点恰好确定一个内接于其中的倾斜正方形,加上非倾斜正方形本身,可知,将边长为k的非倾斜正方形数目乘以k,再按k求和即可得到所有正方形的数目。

设2≤n≤m,k≤n-1,则边长为k的非倾斜有

(n-k)(m-k)个,故所有正方形有

∑(m-k)(n-k)k个

例如m=n=4

正方形有331+222+113=20个

下面是面试过程中遇到的题目

大多数题目都可以在上面找到答案.

电话面试题

1.ArrayList 和 Hashmap 简单说一些,区别,底层的数据结构.

2.Handler 消息机制

3.引起内存泄漏的场景

4.多线程的使用场景?

5.常用的线程池有哪几种?

6.在公司做了什么?团队规模?为什么离职?

面试中实际涉及到的问题

第一轮

1.知道哪些单例模式,写一个线程安全的单例,并分析为什么是线程安全的?


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

C++的单例模式与线程安全单例模式(懒汉/饿汉)

1 教科书里的单例模式

  我们都很清楚一个简单的单例模式该怎样去实现:构造函数声明为private或protect防止被外部函数实例化,内部保存一个private static的类指针保存唯一的实例,实例的动作由一个public的类方法代劳,该方法也返回单例类唯一的实例。

  上代码:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class singleton
{
protected :
     singleton(){}
private :
     static singleton* p;
public :
     static singleton* instance();
};
singleton* singleton::p = NULL;
singleton* singleton::instance()
{
     if (p == NULL)
         p = new singleton();
     return p;
}

  这是一个很棒的实现,简单易懂。但这是一个完美的实现吗?不!该方法是线程不安全的,考虑两个线程同时首次调用instance方法且同时检测到p是NULL值,则两个线程会同时构造一个实例给p,这是严重的错误!同时,这也不是单例的唯一实现!

2 懒汉与饿汉

  单例大约有两种实现方法:懒汉与饿汉。

    • 懒汉:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化,所以上边的经典方法被归为懒汉实现;
    • 饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。

  特点与选择:

    • 由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。
    • 在访问量较小时,采用懒汉实现。这是以时间换空间。
3 线程安全的懒汉实现

  线程不安全,怎么办呢?最直观的方法:加锁。

  • 方法1:加锁的经典懒汉实现:
复制代码
class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);
    }
private:
    static singleton* p;
public:
    static pthread_mutex_t mutex;
    static singleton* initance();
};

pthread_mutex_t singleton::mutex;
singleton* singleton::p = NULL;
singleton* singleton::initance()
{
    if (p == NULL)
    {
        pthread_mutex_lock(&mutex);
        if (p == NULL)
            p = new singleton();
        pthread_mutex_unlock(&mutex);
    }
    return p;
}
复制代码
  • 方法2:内部静态变量的懒汉实现

  此方法也很容易实现,在instance函数里定义一个静态的实例,也可以保证拥有唯一实例,在返回时只需要返回其指针就可以了。推荐这种实现方法,真得非常简单。    

 

复制代码
class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);
    }
public:
    static pthread_mutex_t mutex;
    static singleton* initance();
    int a;
};

pthread_mutex_t singleton::mutex;
singleton* singleton::initance()
{
    pthread_mutex_lock(&mutex);
    static singleton obj;
    pthread_mutex_unlock(&mutex);
    return &obj;
}
复制代码

 

4 饿汉实现

  为什么我不讲“线程安全的饿汉实现”?因为饿汉实现本来就是线程安全的,不用加锁。为啥?自己想!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class singleton
{
protected :
     singleton()
     {}
private :
     static singleton* p;
public :
     static singleton* initance();
};
singleton* singleton::p = new singleton;
singleton* singleton::initance()
{
     return p;
}

  是不是特别简单呢?

  以空间换时间,你说简单不简单?

  面试的时候,线程安全的单例模式怎么写?肯定怎么简单怎么写呀!饿汉模式反而最懒[正经脸]! 




++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

2.Java中的集合有哪些?解释一下HashMap?底部的数据结构?散列表冲突的处理方法,散列表是一个什么样的数据结构?HashMap是采用什么方法处理冲突的?

3.解释一下什么是MVP架构,画出图解,一句话解释MVP和MVC的区别?

4.Handle消息机制?在使用Handler的时候要注意哪些东西,是否会引起内存泄漏?画一下Handler机制的图解?


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

一、Android消息机制一

         Android 有一种叫消息队列的说法,这里我们可以这样理解:假如一个隧道就是一个消息队列,那么里面的每一部汽车就是一个一个消息,这里我们先忽略掉超车等种种因素,只那么先进隧道的车将会先出,这个机制跟我们android 的消息机制是一样的。

角色描述

1. Looper:(相当于隧道) 一个线程可以产生一个Looper 对象,由它来管理此线程里的Message Queue( 车队,消息隧道) 。

2. Handler: 你可以构造Handler 对象来与Looper 沟通,以便push 新消息到Message Queue 里;或者接收Looper( 从Message Queue 取出) 所送来的消息。

3. Message Queue( 消息队列): 用来存放线程放入的消息。

4. 线程:UI thread 通常就是main thread ,而Android 启动程序时会替它建立一个Message Queue 。

每一个线程里可含有一个Looper 对象以及


一个MessageQueue 数据结构。在你的应用


程序里,可以定义Handler 的子类别来接收


Looper 所送出的消息。

 

在你的Android 程序里,新诞生一个线程,或执行 (Thread) 时,并不会自动建立其Message Loop 。

Android 里并没有Global 的Message Queue 数据结构,例如,不同APK 里的对象不能透过Massage Queue来交换讯息(Message) 。

例如:线程A 的Handler 对象可以传递消息给别的线程,让别的线程B 或C 等能送消息来给线程A( 存于AMessage Queue 里) 。

线程A 的Message Queue 里的讯息,只有线程A 所属的对象可以处理。

使用Looper.myLooper 可以取得当前线程的Looper 对象。

使用mHandler = new EevntHandler(Looper.myLooper()); 可用来构造当前线程的Handler 对象;其中,EevntHandler 是自已实现的Handler 的子类别。

使用mHandler = new EevntHandler(Looper.getMainLooper()); 可诞生用来处理main 线程的Handler 对象;其中,EevntHandler 是自已实现的Handler 的子类别。

1.  同线程内不同组件间的消息传递

Looper 类用来管理特定线程内对象之间的消息交换(Message Exchange) 。你的应用程序可以产生许多个线程。而一个线程可以有许多个组件,这些组件之间常常需要互相交换讯息。如果有这种需要,您可以替线程构造一个Looper 对象,来担任讯息交换的管理工作。Looper 对象会建立一个MessageQueue 数据结构来存放各对象传来的消息( 包括UI 事件或System 事件等) 。

 

每一个线程里可含有一个Looper 对象以及一个MessageQueue 数据结构。在你的应用程序里,可以定义Handler 的子类别来接收Looper 所送出的消息。

同线程不同组件之间的消息传递:

举例:

  1. public class Activity1 extends Activity implements OnClickListener{  
  2.        Button button = null ;  
  3.        TextView text = null ;  
  4.        @Override  
  5.        protected void onCreate(Bundle savedInstanceState) {  
  6.               super .onCreate(savedInstanceState);  
  7.               setContentView(R.layout.activity1 );          
  8.               button = (Button)findViewById(R.id.btn );  
  9.               button.setOnClickListener(this );  
  10.               text = (TextView)findViewById(R.id.content );  
  11.        }  
  12.        public void onClick(View v) {  
  13.               switch (v.getId()) {  
  14.               case R.id.btn :  
  15.                      Looper looper = Looper.myLooper();//取得当前线程里的looper  
  16.                      MyHandler mHandler = new MyHandler(looper);//构造一个handler使之可与looper通信  
  17.                      //buton等组件可以由mHandler将消息传给looper后,再放入messageQueue中,同时mHandler也可以接受来自looper消息  
  18.                      mHandler.removeMessages(0);  
  19.                      String msgStr = "主线程不同组件通信:消息来自button";  
  20.                      Message m = mHandler.obtainMessage(111, msgStr);//构造要传递的消息  
  21.                      mHandler.sendMessage(m);//发送消息:系统会自动调用handleMessage方法来处理消息  
  22.                      break ;  
  23.                }              
  24.        }       
  25.        private class MyHandler extends Handler{               
  26.               public MyHandler(Looper looper){  
  27.                      super (looper);  
  28.               }  
  29.               @Override  
  30.               public void handleMessage(Message msg) {//处理消息  
  31.                      text.setText(msg.obj.toString());  
  32.               }              
  33.        }  
  34. }  
  1.   

   说明:

此程序启动时,当前线程( 即主线程, main thread) 已诞生了一个Looper 对象,并且有了一个MessageQueue数据结构。

  1. looper = Looper.myLooper ();   

调用Looper 类别的静态myLooper() 函数,以取得目前线程里的Looper 对象.

  1. mHandler = new MyHandler (looper);  

构造一个MyHandler 对象来与Looper 沟通。Activity 等对象可以藉由MyHandler 对象来将消息传给Looper ,然后放入MessageQueue 里;MyHandler 对象也扮演Listener 的角色,可接收Looper 对象所送来的消息。

  1. Message m = mHandler.obtainMessage(111, obj);  

先构造一个Message 对象,并将数据存入对象里。

  1. mHandler.sendMessage(m);  

就透过mHandler 对象而将消息m 传给Looper ,然后放入MessageQueue 里。

此时,Looper 对象看到MessageQueue 里有消息m ,就将它广播出去,mHandler 对象接到此讯息时,会呼叫其handleMessage() 函数来处理,于是输出"This my message!" 于画面上

  1. public class Activity1 extends Activity implements OnClickListener{  
  2.        Button button = null ;  
  3.        TextView text = null ;  
  4.        @Override  
  5.        protected void onCreate(Bundle savedInstanceState) {  
  6.               super .onCreate(savedInstanceState);  
  7.               setContentView(R.layout.activity1 );          
  8.               button = (Button)findViewById(R.id.btn );  
  9.               button.setOnClickListener(this );  
  10.               text = (TextView)findViewById(R.id.content );  
  11.        }  
  12.        public void onClick(View v) {  
  13.               switch (v.getId()) {  
  14.               case R.id.btn :  
  15.                      Looper looper = Looper.myLooper();//取得当前线程里的looper  
  16.                      MyHandler mHandler = new MyHandler(looper);//构造一个handler使之可与looper通信  
  17.                      //buton等组件可以由mHandler将消息传给looper后,再放入messageQueue中,  
  18.              //同时mHandler也可以接受来自looper消息  
  19.                      mHandler.removeMessages(0);  
  20.                      String msgStr = "主线程不同组件通信:消息来自button";  
  21.                      Message m = mHandler.obtainMessage(111, msgStr);//构造要传递的消息  
  22.                      mHandler.sendMessage(m);//发送消息:系统会自动调用handleMessage方法来处理消息  
  23.                      break ;  
  24.                }              
  25.        }       
  26.        private class MyHandler extends Handler{               
  27.               public MyHandler(Looper looper){  
  28.                      super (looper);  
  29.               }  
  30.               @Override  
  31.               public void handleMessage(Message msg) {//处理消息  
  32.                      text.setText(msg.obj.toString());  
  33.               }              
  34.        }  
  35. }  

二、Android消息机制二


角色综述(回顾)

   

1. UI thread 通常就是main thread ,而Android 启动程序时会替它建立一个MessageQueue 。

2. 当然需要一个Looper 对象,来管理该MessageQueue 。

3. 我们可以构造Handler 对象来push 新消息到Message Queue 里;或者接收Looper( 从Message Queue取出) 所送来的消息。

4. 线程A 的Handler 对象可以传递给别的线程,让别的线程B 或C 等能送讯息来给线程A( 存于A 的Message Queue 里) 。

5. 线程A 的Message Queue 里的消息,只有线程A 所属的对象可以处理。

 

子线程传递消息给主线程

举例

  1. public class Activity2 extends Activity implements OnClickListener{  
  2.        Button button = null ;  
  3.        TextView text = null ;  
  4.        MyHandler mHandler = null ;  
  5.        Thread thread ;  
  6.        @Override  
  7.        protected void onCreate(Bundle savedInstanceState) {  
  8.               super .onCreate(savedInstanceState);  
  9.               setContentView(R.layout.activity1 );          
  10.               button = (Button)findViewById(R.id.btn );  
  11.               button.setOnClickListener(this );  
  12.               text = (TextView)findViewById(R.id.content );  
  13.        }  
  14.        public void onClick(View v) {  
  15.               switch (v.getId()) {  
  16.               case R.id.btn :  
  17.                      thread = new MyThread();  
  18.                      thread.start();  
  19.                      break ;  
  20.               }              
  21.        }       
  22.        private class MyHandler extends Handler{               
  23.               public MyHandler(Looper looper){  
  24.                      super (looper);  
  25.               }  
  26.               @Override  
  27.               public void handleMessage(Message msg) {//处理消息  
  28.                      text.setText(msg.obj.toString());  
  29.               }              
  30.        }  
  31.        private class MyThread extends Thread{  
  32.               @Override  
  33.               public void run() {  
  34.                      Looper curLooper = Looper.myLooper();  
  35.                      Looper mainLooper = Looper.getMainLooper();  
  36.                      String msg ;  
  37.                      if (curLooper==null ){  
  38.                             mHandler = new MyHandler(mainLooper);  
  39.                             msg = "curLooper is null";  
  40.                      }else {  
  41.                             mHandler = new MyHandler(curLooper);  
  42.                             msg = "This is curLooper";  
  43.                      }  
  44.                      mHandler.removeMessages(0);  
  45.                      Message m = mHandler.obtainMessage(111, msg);  
  46.                      mHandler.sendMessage(m);  
  47.               }              
  48.        }  
  49. }  

说明:

Android 会自动替主线程建立Message Queue 。在这个子线程里并没有建立Message Queue 。所以,myLooper 值为null ,而mainLooper 则指向主线程里的Looper 。于是,执行到:

  1. mHandler = new MyHandler (mainLooper);  

mHandler 属于主线程。

  1. mHandler.sendMessage(m);  

就将m 消息存入到主线程的Message Queue 里。mainLooper 看到Message Queue 里有讯息,就会作出处理,于是由主线程执行到mHandler 的handleMessage() 来处理消息。

三、用Android线程间通信的Message机制

在Android 下面也有多线程 的概念,在C/C++中,子线程可以是一个函数 ,一般都是一个带有循环的函数,来处理某些数据 ,优先线程只是一个复杂的运算过程,所以可能不需要while循环,运算完成,函数结束,线程就销毁。对于那些需要控制的线程,一般我们都是和互斥锁相互关联,从而来控制线程的进度,一般我们创建子线程,一种线程是很常见的,那就是带有消息循环的线程。
消息循环是一个很有用的线程方式,曾经自己用C在Linux下面实现一个消息循环的机制 ,往消息队列里添加数据,然后异步的等待消息的返回。当消息队列为空的时候就会挂起线程,等待新的消息的加入。这是一个很通用的机制。
在 Android,这里的线程分为有消息循环的线程和没有消息循环的线程,有消息循环的线程一般都会有一个Looper,这个事android的新概念。我 们的主线程(UI线程)就是一个消息循环的线程。针对这种消息循环的机制,我们引入一个新的机制Handle,我们有消息循环,就要往消息循环里面发送相 应的消息,自定义 消息一般都会有自己对应的处理,消息的发送和清除,消息的的处理,把这些都封装在Handle里面,注意Handle只是针对那些有Looper的线程,不管是UI线程还是子线程,只要你有Looper,我就可以往你的消息队列里面添加东西,并做相应的处理。
但是这里还有一点,就是只要是关于UI相关的东西,就不能放在子线程中,因为子线程是不能操作UI的,只能进行数据、系统 等其他非UI的操作。
那么什么情况下面我们的子线程才能看做是一个有Looper的线程呢?我们如何得到它Looper的句柄呢?
Looper.myLooper();            //获得当前的Looper
Looper.getMainLooper () ;  //获得UI线程的Lopper

我们看看Handle的初始化函数,如果没有参数,那么他就默认使用的是当前的Looper,如果有Looper参数,就是用对应的线程的Looper。
如 果一个线程中调用Looper.prepare(),那么系统就会自动的为该线程建立一个消息队列,然后调用 Looper.loop();之后就进入了消息循环,这个之后就可以发消息、取消息、和处理消息。这个如何发送消息和如何处理消息可以再其他的线程中通过 Handle来做,但前提是我们的Hanle知道这个子线程的Looper,但是你如果不是在子线程运行 Looper.myLooper(),一般是得不到子线程的looper的。

  1. public void run() {  
  2. <span style="white-space:pre;"> </span>synchronized (mLock) {  
  3.    <span style="white-space:pre;">      </span>Looper.prepare();  
  4.       <span style="white-space:pre;">       </span>//do something  
  5.         }  
  6.         Looper.loop();  
  7. }  
所以很多人都是这样做的:我直接在子线程中新建handle,然后在子线程中发送消息,这样的话就失去了我们多线程的意义了。
  1. class myThread extends Thread{  
  2.              private EHandler mHandler ;  
  3.              public void run() {  
  4.                  Looper myLooper, mainLooper;  
  5.                  myLooper = Looper.myLooper ();  
  6.                 mainLooper = Looper.getMainLooper ();  
  7.                 String obj;  
  8.                 if (myLooper == null ){  
  9.                          mHandler = new EHandler(mainLooper);  
  10.                          obj = "current thread has no looper!" ;  
  11.                 }  
  12.                 else {  
  13.                      mHandler = new EHandler(myLooper);  
  14.                      obj = "This is from current thread." ;  
  15.                 }  
  16.                 mHandler .removeMessages(0);  
  17.                 Message m = mHandler .obtainMessage(111, obj);  
  18.                 mHandler .sendMessage(m);  
  19.              }  
  20.   }  

可以让其他的线程来控制我们的handle,可以把 private EHandler mHandler ;放在外面,这样我们的发消息和处理消息都可以在外面来定义,这样增加程序 代码 的美观,结构更加清晰。
对如任何的Handle,里面必须要重载一个函数
public void handleMessage(Message msg)
这个函数就是我们的消息处理,如何处理,这里完全取决于你,然后通过 obtainMessage和 sendMessage等来生成和发送消息, removeMessages(0)来清除消息队列。Google 真是太智慧了,这种框架 的产生,我们写代码更加轻松了。
有的时候,我们的子线程想去改变UI了,这个时候千万不要再子线程中去修改,获得UI线程的Looper,然后发送消息即可。
我们看看Goole Music App的源代码 。
在MediaPlaybackActivity.java 中,我们可以看一下再OnCreate中的有这样的两句:
  1. mAlbumArtWorker = new Worker("album art worker");  
  2. mAlbumArtHandler = new AlbumArtHandler(mAlbumArtWorker.getLooper());  
很 明显这两句,是构建了一个子线程。并且这个子线程还是Looper的子线程,这里很牛逼的使用了 mAlbumArtWorker.getLooper()这个函数,因为我们知道,我们能够得到子线程的Looper的途径只有一个:就是在子线程中调用 Looper.myLooper (),并且这个函数还要在我们perpare之后调用才能得到正确的Looper,但是他这里用了一个这样的什么东东 getLooper,不知道它是如何实现的?
这里有一个大概的思路,我们在子线程的的prepare之后调用 myLooper ()这个方法,然后保存在一个成员变量中,这个getLooper就返回这个东西,但是这里会碰到多线程的一个很突出的问题,同步。我们在父线程中调用 mAlbumArtWorker.getLooper(),但是想要这个返回正确的looper就必须要求我们的子线程运行了prepare,但是这个东 西实在子线程运行的,我们如何保证呢?
我们看Google是如何实现的?
  1. private class Worker implements Runnable {  
  2.         private final Object mLock = new Object();  
  3.         private Looper mLooper;  
  4.           
  5.         /** 
  6.          * Creates a worker thread with the given name. The thread 
  7.          * then runs a [email=%7B@link]{@link [/email] android.os.Looper}. 
  8.          * @param name A name for the new thread 
  9.          */  
  10.         Worker(String name) {  
  11.             Thread t = new Thread(nullthis, name);  
  12.             t.setPriority(Thread.MIN_PRIORITY);  
  13.             t.start();  
  14.             synchronized (mLock) {  
  15.                 while (mLooper == null) {  
  16.                     try {  
  17.                         mLock.wait();  
  18.                     } catch (InterruptedException ex) {  
  19.                     }  
  20.                 }  
  21.             }  
  22.         }  
  23.           
  24.         public Looper getLooper() {  
  25.             return mLooper;  
  26.         }  
  27.           
  28.         public void run() {  
  29.             synchronized (mLock) {  
  30.                 Looper.prepare();  
  31.                 mLooper = Looper.myLooper();  
  32.                 mLock.notifyAll();  
  33.             }  
  34.             Looper.loop();  
  35.         }  
  36.           
  37.         public void quit() {  
  38.             mLooper.quit();  
  39.         }  
  40.     }  
我 们知道,一个线程类的构造函数是在主线程中完成的,所以在我们的 Worker的构造函数中我们创佳一个线程,然后让这个线程运行,这一这个线程的创建是指定一个 Runnable,这里就是我们的Worker本身,在主线程调用 t.start();,这后,我们子线程已经创建,并且开始执行work的run方法。然后下面的代码很艺术:
  1. synchronized (mLock) {  
  2.                 while (mLooper == null) {  
  3.                     try {  
  4.                         mLock.wait();  
  5.                     } catch (InterruptedException ex) {  
  6.                     }  
  7.                 }  
  8.             }  

我们开始等待我们的子线程给mLooper赋值,如果不赋值我们就继续等,然后我们的子线程在运行run方法之后,在给 mLooper赋值之后,通知worker够着函数中的wait,然后我们的构造函数才能完成,所以我们说:
  1. mAlbumArtWorker = new Worker("album art worker");  
这句本身就是阻塞的,它创建了一个子线程,开启了子线程,并且等待子线程给mLooper赋值,赋值完成之后,这个函数才返回,这样才能保证我们的子线程的Looper的获取 绝对是正确的,这个构思很有创意。值得借鉴。

四、Android中Handler的使用方法——在子线程中更新界面

本文主要介绍Android的Handler的使用方法。Handler可以发送Messsage和Runnable对象到与其相关联的线程的消息队列。每个Handler对象与创建它的线程相关联,并且每个Handler对象只能与一个线程相关联。

1.    Handler一般有两种用途:

1)执行计划任务,你可以再预定的实现执行某些任务,可以模拟定时器。

2)线程间通信。在Android的应用启动时,会 创建一个主线程,主线程会创建一个消息队列来处理各种消息。

当你创建子线程时,你可以再你的子线程中拿到父线程中创建的Handler对象,就可以通过该 对象向父线程的消息队列发送消息了。由于Android要求在UI线程中更新界面,因此,可以通过该方法在其它线程中更新界面。 

◆ 通过Runnable在子线程中更新界面

1)在onCreate中创建Handler 

  1. public class HandlerTestApp extends Activity {   
  2.         Handler mHandler;   
  3.         TextView mText;   
  4.         /** Called when the activity is first created. */   
  5.        @Override   
  6.        public void onCreate(Bundle savedInstanceState) {   
  7.            super.onCreate(savedInstanceState);   
  8.            setContentView(R.layout.main);   
  9.            mHandler = new Handler();//创建Handler   
  10.            mText = (TextView) findViewById(R.id.text0);//一个TextView   
  11.        }   
2) 构建Runnable对象,在runnable中更新界面,此处,我们修改了TextView的文字.此处需要说明的是,Runnable对象可以再主线程中创建,也可以再子线程中创建。我们此处是在子线程中创建的。    

  1. Runnable mRunnable0 = new Runnable()   
  2.     {   
  3.                 @Override   
  4.                 public void run() {   
  5.                         mText.setText("This is Update from ohter thread, Mouse DOWN");   
  6.                 }   
  7.     };   
3)   创建子线程,在线程的run函数中,我们向主线程的消息队列发送了一个runnable来更新界面。

  1. private void updateUIByRunnable(){   
  2.           new Thread()    
  3.          {    
  4.                //Message msg = mHandler.obtainMessage();    
  5.               public void run()    
  6.              {   
  7.   
  8.                    //mText.setText("This is Update from ohter thread, Mouse DOWN");//这句将抛出异常   
  9.                    mHandler.post(mRunnable0);    
  10.              }    
  11.          }.start();  
  12.   
  13.      }  
◆ 用Message在子线程中来更新界面

用Message更新界面与Runnable更新界面类似,只是需要修改几个地方。

1) 实现自己的Handler,对消息进行处理

  1. private class MyHandler extends Handler   
  2.     {   
  3.   
  4.         @Override   
  5.         public void handleMessage(Message msg) {   
  6.             super.handleMessage(msg);   
  7.             switch(msg.what)   
  8.             {   
  9.             case UPDATE ://在收到消息时,对界面进行更新   
  10.                 mText.setText("This update by message");   
  11.                 break;   
  12.             }   
  13.         }   
  14.     }  
2)  在新的线程中发送消息  

  1. private void updateByMessage()   
  2.     {   
  3.         //匿名对象   
  4.          new Thread()   
  5.          {   
  6.                 public void run()   
  7.                 {   
  8.                     //mText.setText("This is Update from ohter thread, Mouse DOWN");  
  9.   
  10.                     //UPDATE是一个自己定义的整数,代表了消息ID   
  11.                     Message msg = mHandler.obtainMessage(UPDATE);   
  12.                     mHandler.sendMessage(msg);   
  13.                 }   
  14.          }.start();   
  15.     }  

五、AsyncTask与handler

         AsyncTask实际上就是一个线程池,AsyncTask在代码上比handler要轻量级别,而实际上要比handler更耗资源,因为AsyncTask底层是一个线程池!而Handler仅仅就是发送了一个消息队列,连线程都没有开。
但是,如果异步任务的数据特别庞大,AsyncTask这种线程池结构的优势就体现出来了

         android的ui线程操作并不是安全的,并且和用户直接进行界面交互的操作都必须在ui线程中进行才可以。这种模式叫做单线程模式。

我们在单线程模式下编程一定要注意:不要阻塞ui线程、确保只在ui线程中访问ui组件

当我们要执行一个复杂耗时的算法并且最终要将计算结果反映到ui上时,我们会发现,我们根本没办法同时保证上面的两点要求;我们肯定会想到开启一个新的线程,让这个复杂耗时的任务到后台去执行,但是执行完毕了呢?我们发现,我们无法再与ui进行交互了。

为了解决这种情况,android为我们提供了很多办法。

1)handler和message机制:通过显示的抛出、捕获消息与ui进行交互;

2)Activity.runOnUiThread(Runnable):如果当前线程为ui线程,则立即执行;否则将参数中的线程操作放入到ui线程的事件队列中,等待执行。

3)View.post(Runnable):将操作放入到message队列中,如果放入成功,该操作将会在ui线程中执行,并返回true,否则返回false

4)View.postDelayed(Runnable, long)跟第三条基本一样,只不过添加了一个延迟时间。

5)android1.5以后为我们提供了一个工具类来搞定这个问题AsyncTask.

AsyncTask是抽象类,定义了三种泛型类型 Params,Progress,Result。

Params :启动任务执行的输入参数,比如HTTP请求的URL

Progress: 后台任务执行的百分比。

Result :后台执行任务最终返回的结果,比如String

用程序调用,开发者需要做的就是实现这些方法。

1) 子类化AsyncTask

2) 实现AsyncTask中定义的下面一个或几个方法

onPreExecute(),该方法将在执行实际的后台操作前被UI thread调用。可以在该方法中做一些准备工作,如在界面上显示一个进度条。

doInBackground(Params…),将在onPreExecute 方法执行后马上执行,该方法运行在后台线程中。这里将主要负责执行那些很耗时的后台计算工作。可以调用 publishProgress方法来更新实时的任务进度。该方法是抽象方法,子类必须实现。

onProgressUpdate(Progress…),在publishProgress方法被调用后,UI thread将调用这个方法从而在界面上展示任务的进展情况,例如通过一个进度条进行展示。

onPostExecute(Result),在doInBackground 执行完成后,onPostExecute 方法将被UI thread调用,后台的计算结果将通过该方法传递到UI thread.

为了正确的使用AsyncTask类,以下是几条必须遵守的准则:

1) Task的实例必须在UI thread中创建

2) execute方法必须在UI thread中调用

3) 不要手动的调用onPreExecute(), onPostExecute(Result),doInBackground(Params…), onProgressUpdate(Progress…)这几个方法

4) 该task只能被执行一次,否则多次调用时将会出现异常

  1. <span style="font-size: 14px;">    package cn.com.chenzheng_java;   
  2.           
  3.     import android.os.AsyncTask;   
  4.     /** 
  5.      *  
  6.      * @author chenzheng_java 
  7.      * @description 异步任务AcyncTask示例 
  8.      *     
  9.      */   
  10.     public class MyAsyncTask extends AsyncTask<String, Integer, Object> {   
  11.           
  12.      /** 
  13.      * 该方法由ui线程进行调用,用户可以在这里尽情的访问ui组件。 
  14.      * 很多时候,我们会在这里显示一个进度条啥的,以示后台正在 
  15.      * 执行某项功能。 
  16.      */   
  17.      @Override   
  18.      protected void onPreExecute() {   
  19.      super.onPreExecute();   
  20.      }   
  21.           
  22.      /** 
  23.      * 该方法由后台进程进行调用,进行主要的耗时的那些计算。 
  24.      * 该方法在onPreExecute方法之后进行调用。当然在执行过程中 
  25.      * 我们可以每隔多少秒就调用一次publishProgress方法,更新 
  26.      * 进度信息 
  27.      */   
  28.      @Override   
  29.      protected Object doInBackground(String... params) {   
  30.      return null;   
  31.      }   
  32.           
  33.           
  34.      /** 
  35.      * doInBackground中调用了publishProgress之后,ui线程就会 
  36.      * 调用该方法。你可以在这里动态的改变进度条的进度,让用户知道 
  37.      * 当前的进度。 
  38.      */   
  39.      @Override   
  40.      protected void onProgressUpdate(Integer... values) {   
  41.      super.onProgressUpdate(values);   
  42.      }   
  43.           
  44.      /** 
  45.      * 当doInBackground执行完毕之后,由ui线程调用。可以在这里 
  46.      * 返回我们计算的最终结果给用户。 
  47.      */   
  48.      @Override   
  49.      protected void onPostExecute(Object result) {   
  50.      super.onPostExecute(result);   
  51.      }   
  52.     }   
  53. </span>  




+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

5.是否做过性能优化?已经采取了哪些措施进行优化?

6.引起内存泄漏的原因是什么?以及你是怎么解决的?

这些问题应该都是比较基础的问题,每个开发者都应该是非常熟悉并能详细叙述的.这一轮的面试官问的技术都是平时用到的.

第二轮

1.关于并发理解多少?说几个并发的集合?

2.Handler 消息机制图解?

3.在项目中做了哪些东西?

4.画图说明View 事件传递机制?并举一个例子阐述

5.类加载机制,如何换肤,换肤插件中存在的问题?hotfix是否用过,原理是否了解?

6.说说项目中用到了哪些设计模式,说了一下策略模式和观察者模式?

7.会JS么?有Hybid开发经验么?

8.说一下快排的思想?手写代码

9.堆有哪些数据结构?

对于这轮米那是明显感觉到压力,知识的纵向了解也比较深,应该是个leader.

第三轮

1.介绍一下在项目中的角色?

2.遇到困难是怎么解决的?

3.如何与人相处,与别人意见相左的时候是怎么解决的,并举生活中的一个例子.

4.有没有压力特别大的时候?

这个应该是项目经理了,问的问题偏向于生活性格方面.


以上面试中问到的题目基本上都可以在上面找到答案,所以做准备是很重要的,但技术是一点点积累的,就算你全会背了,面试过了,真正等到工作的时候还是会捉襟见肘的,所以踏实点吧骚年
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值