Android基本知识

/ Android / 没有评论 / 2268浏览

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 处理消息的过程是:

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()。六个步骤:

  1. 绘制视图的背景;
  2. 保存画布的图层(Layer);
  3. 绘制View的内容;
  4. 绘制View子视图,如果没有就不用;
  5. 还原图层(Layer);
  6. 绘制滚动条。

事件传递机制

  1. Android事件分发机制的本质是要解决:点击事件由哪个对象发出,经过哪些对象, 最终达到哪个对象并最终得到处理。这里的对象是指Activity、ViewGroup、View.
  2. Android中 事件分发顺序:Activity(Window) -> ViewGroup -> View.
  3. 事件分发过程由 dispatchTouchEvent() 、onInterceptTouchEvent() 和 onTouchEvent() 三个方法协助完成.
  4. 设置 Button 按钮来响应点击事件事件传递情况:(如下图) 布局如下: alt

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

按钮点击事件:

DOWN 事件被传递给 C 的 onTouchEvent 方法,该方法返回 true,表示处理这个事件;因为 C 正在处理这个事件,那么 DOWN事件将不再往上传递给 B 和 A 的onTouchEvent();该事件列的其他事件(Move、Up)也将传递给 C 的 onTouchEvent(); alt (记住这个图的传递顺序,面试的时候能够画出来,就很详细了)

Binder机制

了解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的组成解构: alt 图中的IPC就是进程间通信的意思。图中的 ServiceManager,负责把 Binder Server注册到一个容器中。有人把 ServiceManager 比喻成电话局,存储着每个住宅的座机电话,还是很恰当的。张三给李四打电话,拨打电话号码,会先转接到电话局, 电话局的接线员查到这个电话号码的地址,因为李四的电话号码之前在电话局注册过,所以就能拨通; 没注册,就会提示该号码不存在。对照着 Android Binder 机制,对着上面这图,张三就是 Binder Client,李四就是Binder Server,电话局就是ServiceManager,电话局的接线员在这个过程中做了很多事情,对应着图中的Binder驱动.
  3. 接下来我们看 Binder 通信的过程,还是摘自田维术博客的一张图: alt 注:图中的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。
  4. 以上这3步,Binder驱动出了很多力,但我们不需要知道Binder驱动的底层实现, 涉及到C++的代码了——把有限的时间去做更有意义的事情。 (ps:以上节选自包建强老师的文章点我点我).

为什么 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. 可见进程 一个进程不拥有运行于前台的组件,但是依然能影响用户所见。满足下列条件时,进程即为可见:
  1. 服务进程 一个可见进程被认为是极其重要的。并且,除非只有杀掉它才可以保证所有前台进程的运行,否则是不能动它的。这个进程拥有一个绑定到可见activity的Service。一个进程不在上述两种之内,但它运行着一个被startService()所启动的service。尽管一个服务进程不直接影响用户所见,但是它们通常做一些用户关心的事情(比如播放音乐或下载数据),所以系统不到前台进程和可见进程活不下去时不会杀它。
  2. 后台进程 一个进程拥有一个当前不可见的activity(activity的onStop()方法被调用)。这样的进程们不会直接影响到用户体验,所以系统可以在任意时刻杀了它们从而为前台、可见、以及服务进程们提供存储空间。通常有很多后台进程在运行。它们被保存在一个LRU(最近最少使用)列表中来确保拥有最近刚被看到的activity的进程最后被杀。如果一个activity正确的实现了它的生命周期方法,并保存了它的当前状态,那么杀死它的进程将不会对用户的可视化体验造成影响。因为当用户返回到这个activity时,这个activity会恢复它所有的可见状态。
  3. 空进程 一个进程不拥有入何active组件。保留这类进程的唯一理由是高速缓存,这样可以提高下一次一个组件要运行它时的启动速度。系统经常为了平衡在进程高速缓存和底层的内核高速缓存之间的整体系统资源而杀死它们。

线程池的相关知识。

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

  1. FixThreadPool 只有核心线程,并且数量固定的,也不会被回收,所有线程都活动时,因为队列没有限制大小,新任务会等待执行.
  1. SingleThreadPool 只有一个核心线程,确保所有的任务都在同一线程中按顺序完成.因此不需要处理线程同步的问题.
  2. CachedThreadPool 只有非核心线程,最大线程数非常大,所有线程都活动时,会为新任务创建新线程,否则会利用空闲线程(60s空闲时间,过了就会被回收,所以线程池中有0个线程的可能)处理任务.
  1. ScheduledThreadPool 核心线程数固定,非核心线程(闲着没活干会被立即回收)数没有限制.

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

内存泄露

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

产生的内存泄露

查找内存泄漏

查找内存泄漏可以使用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. 重用布局文件
  1. 仅在需要时才加载布局

高性能编码优化

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

  1. 避免创建不必要的对象 不必要的对象我们应该避免创建:
  1. 在没有特殊原因的情况下,尽量使用基本数据类型来代替封装数据类型,int比Integer要更加有效,其它数据类型也是一样。基本数据类型的数组也要优于对象数据类型的数组。另外两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效的多。
  2. 静态优于抽象 如果你并不需要访问一个对系那个中的某些字段,只是想调用它的某些方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,调用速度提升15%-20%,同时也不用为了调用这个方法去专门创建对象了,也不用担心调用这个方法后是否会改变对象的状态(静态方法无法访问非静态字段)。
  3. 对常量使用static final修饰符
static int intVal = 42;  
static String strVal = "Hello, world!";  

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

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

这样,定义类就不需要方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。当我们调用intVal时可以直接指向42的值,而调用strVal会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。这种优化方式只对基本数据类型以及String类型的常量有效,对于其他数据类型的常量是无效的。 5. 使用增强型for循环语法

static class Counter {  
    int mCount;  
}  

Counter[] mArray = new Counter[]{};  
public void zero() {  
    int sum = 0;  
    for (int i = 0; i < mArray.length; ++i) {  
        sum += mArray[i].mCount;  
    }
}  

public void one() {  
    int sum = 0;  
    Counter[] localArray = mArray;  
    int len = localArray.length;  
    for (int i = 0; i < len; ++i) {  
        sum += localArray[i].mCount;  
    }
}  

public void two() {  
    int sum = 0;  
    for (Counter a : mArray) {  
        sum += a.mCount;  
    }  
}

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

  1. 多使用系统封装好的API 系统提供不了的Api完成不了我们需要的功能才应该自己去写,因为使用系统的Api很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的。 举个例子,实现数组拷贝的功能,使用循环的方式来对数组中的每一个元素一一进行赋值当然可行,但是直接使用系统中提供的System.arraycopy()方法会让执行效率快9倍以上。
  2. 避免在内部调用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:让系统能及时回 收内存。
  1. 使用decodeStream代替其他方法。decodeResource,setImageResource,setImageBitmap等方法。

LRUCache算法是怎样实现的

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

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