您的位置:首页 > 移动开发 > Android开发

一些底层基本知识(Android篇二)

2017-04-13 19:24 405 查看
这篇着重记录一些Android偏底层的基本知识

Activity的工作原理

《Android开发艺术探索》

Android instrumentation原理 http://blog.csdn.net/a19891024/article/details/54342799

Activity的启动过程

简述: 启动Activity涉及到Instrumentation,ActivityThread,ActivityManagerService(AMS).

启动Activity的请求会由Instrumentation来处理,然后它通过Binder向AMS发送请求,AMS内部维护着一个ActivityStack并负责栈内的Activity的状态同步,AMS通过ActivityThread的scheduleLaunchActivity方法去同步Activity的状态从而完成生命周期方法的调用。

Instrumentation:

官方描述:

instrumentation can load both a test package and the application under test into the same process. Since the application components and their tests are in the same process, the tests can invoke methods in the components, and modify and examine fields in the components.

Instrumentation可以把测试包和目标测试应用加载到同一个进程中运行。既然各个控件和测试代码都运行在同一个进程中了,测试代码当然就可以调用这些控件的方法了,同时修改和验证这些控件的一些数据

Android instrumentation是Android系统里面的一套控制方法或者”钩子“。这些钩子可以在正常的生命周期(正常是由操作系统控制的)之外控制Android控件的运行,其实指的就是Instrumentation类提供的各种流程控制方法

一般在开发Android程序的时候,需要写一个manifest文件,其结构是:

<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".TestApp" android:label="@string/app_name">
……
</activity>
</application>


  

 

这样,在启动程序的时候就会先启动一个Application,然后在此Application运行过程中根据情况加载相应的Activity,而Activity是需要一个界面的。

但是Instrumentation并不是这样的。可以将Instrumentation理解为一种没有图形界面的,具有启动能力的,用于监控其他类(用Target Package声明)的工具类。

IntentFilter的匹配规则

隐式调用中,需要Intent能够匹配目标组件中的IntentFilter中所设置的过滤信息,如果不匹配将无法启动目标Activity.

为了匹配过滤列表,需要同时匹配过滤列表中的action,category,data信息,否则匹配失败。另外,一个Activity中可以有多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity。

action匹配规则

action是一个区分大小写的字符串,系统预定义了一些,我们也可以自己定义一些。一个过滤规则可以有多个action,但是只要intent中的action能够匹配任何一个action即匹配成功。

总结一下即:action的匹配要求Intent中的action存在且必须和过滤规则中的其中一个action相同。

category匹配规则

和action一样,系统预定义了一些,我们也可以自定义一些。但匹配规则不同,它要求Intent中如果含有category,那么所有的category都必须和过滤规则中的其中一个category相同才匹配。

而且,和action不同的是,intent中的category可以为空,因为在intent-filter中系统自动加上了DEFAULT这个category用来匹配空值。

data匹配规则

IPC机制

《Android开发艺术探索》

IPC是Inter-Process-Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。

在这里简单复习一下进程线程,具体参考上篇博客JAVA篇二。

按照操作系统中的描述,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上值一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。在最简单的情况下,一个进程中只有一个线程即主线程,在Android中主线程也叫UI线程,在UI线程里才能操作界面元素。

很多时候,一个进程中需要执行大量耗时的任务,如果这些任务放在主线程中去执行就会造成界面无法响应,严重影响用户体验,在Android中叫做ANR(Application Not Responding),即应用无响应。解决这个问题就需要用到线程,把一些耗时的任务放在线程中即可。

Linux上可以通过管道(pipe),命名管道(FIFO),内存映射(mapped memeory),消息队列(message queue),共享内存(shared memory),信号量(semaphore),信号(signal),套接字(Socket)八种方式来进行进程间通信。

管道(pipe):管道允许一个进程和另一个与它有共同祖先的进程之间进行通信;

命名管道(FIFO):类似于管道,但是它可以用于任何两个进程之间的通信,命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建;

信号(signal):信号是比较复杂的通信方式,用于通知接收进程有某种事情发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持UNIX早期信号语义函数signal外,还支持语义符合POSIX.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD即能实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数的功能);

内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它;

消息队列(message queue):消息队列是消息的连接表,包括POSIX消息对和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能成该无格式字节流以及缓冲区大小受限等缺点;

信号量(semaphore):信号量主要作为进程间以及同进程不同线程之间的同步手段;

共享内存 (shared memory):它使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。这是针对其他通信机制运行效率较低而设计的。它往往与其他通信机制,如信号量结合使用,以达到进程间的同步及互斥;

套接字(Socket):它是更为通用的进程间通信机制,可用于不同机器之间的进程间通信。起初是由UNIX系统的BSD分支开发出来的,但现在一般可以移植到其他类UNIX系统上:Linux和System V的变种都支持套接字。

而Android,它是一种基于Linux内核的移动操作系统,它并不是完全继承自Linux,它有自己特色的通信方式:Binder,通过Binder可以轻松地实现进程间通信。

在Android中,为每一个应用都分配了一个独立的虚拟机,或者说为每一个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同虚拟机中访问同一个类的对象会产生多分副本。

所以使用多进程会造成以下几方面的问题:

1. 静态成员和单例模式完全失效

2. 线程同步机制完全失效

3. SharedPreferences的可靠性下降

4. Application会多次创建

Serializable 和Parcelable

Serializable, 简单易用

serializable的迷人之处在于你只需要对某个类以及它的属性实现Serializable 接口即可。Serializable 接口是一种标识接口(marker interface),这意味着无需实现方法,Java便会对这个对象进行高效的序列化操作。

这种方法的缺点是使用了反射,序列化的过程较慢。这种机制会在序列化的时候创建许多的临时对象,容易触发垃圾回收。

Parcelable, 速度至上

Parcelable内部包装了可序列化的数据,可以在Binder中自由传输。序列化功能由writeToParcel完成,反序列化是由CREATOR来完成。

二者如何取舍?

Serializable是JAVA中的序列化接口,虽然使用起来简单但是开销很大,序列化和反序列化过程都要大量的I/O操作。

而Parcelable是Android中的序列化方式,因此更适合使用在Android平台上。它的缺点就是使用起来稍微麻烦一点,但是效率高。

Parcelable主要用在内存序列化上,Serializable主要用于将对象序列化到存储设备中或者通过网络传输。

Binder

http://www.cnblogs.com/innost/archive/2011/01/09/1931456.html

http://blog.csdn.net/luoshengyang/article/details/6618363/

直观来说,Binder是Android中的一个类,他实现了IBinder接口。这个接口是能进行远程操作对象的一个基接口。定义了为在提供进程间和跨进程间的调用时提供高性能的轻量级远程调用的核心部分。该接口描述了与远程对象进行交互的抽象协议。IBinder是远程对象的基本接口,是为高性能而设计的轻量级远程调用机制的核心部分。但它不仅用于远程调用,也用于进程内调用。这个接口定义了与远程对象交互的协议。不要直接实现这个接口,而应该从Binder派生。

从IPC角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;

从Android Framework角度来说,Binder是ServiceManager连接各种Manager和相应ManagerService的桥梁。

从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

Android开发中,Binder主要用在Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及进程间通信。

在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。Binder就是一种把这四个组件粘合在一起的粘结剂了,其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Service Manager和Binder驱动已经在Android平台中实现好,开发者只要按照规范实现自己的Client和Server组件就可以了。

Android中的IPC方式

1、使用Bundle

四大组件中的三大组件(Activity,Service,Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以他可以方便地在不同的进程间传输。基于这一点,我们在一个进程中启动了另一个进程的时候,就可以在Bundle中附加我们需要传输的信息,并通过Intent传送出去。但是,传输的数据必须能够被序列化,比如基本类型、实现了Serializable/Parcelable的对象以及一些Android支持的特殊对象。

2、使用文件共享

两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。

文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。

3、使用Messenger

Messenger可以翻译为信使,通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递。

Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。它的使用方法和简单,它对AIDL做了封装,使得我们可以更简便的进行进程间通信。同时,它一次处理一个请求,因此在服务端不用考虑线程同步的问题。

(1)服务端进程

首先,在服务端创建一个Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,

然后在Service的onBind中返回这个Messenger对象底层的Binder即可。

(2)客户端进程

客户端进程中,首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,发消息类型为Message对象。

4、使用AIDL

Messenger是以串行的方式处理客户端发来的消息,在处理大量同时发送消息就不行了。我们可以使用AIDL来实现跨进程的方法调用。

(1)服务端

服务端首先创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中申明,最后在Service中实现这个AIDL接口即可。

(2)客户端

首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。

5、使用ContentProvider

6、使用Socket

View的工作原理

深入理解 Android 之 View 的绘制流程 http://www.cnblogs.com/jycboy/p/6219915.html

有关Android View 绘制流程 & 自定义View http://www.jianshu.com/p/f0bc39dbfa26

通过自定义View可以实现各种五花八门的效果。为了更好的自定义View,还需要掌握View的底层工作原理,比如View的测量流程、布局流程以及绘制流程。

View的工作流程主要是指measure、layout、drow这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。

在进行实际分析前,先看下面这张图:



DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。

ViewRoot

在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。

那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。

View绘制的起点

当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查发起布局请求的线程是否为主线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}


上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

三个阶段

View的整个绘制流程可以分为以下三个阶段:

- measure: 判断是否需要重新计算View的大小,需要则计算

- layout:判断是否需要重新计算View的位置,需要则计算

- draw:判断是否需要重新绘制View,需要则重绘

这三个子阶段可以用下图来描述:



measure阶段

此阶段的目的是计算出控件树中的各个控件要显示其内容的话,需要多大尺寸。

measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程。如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

从measure()方法的源码中我们可以知道,只有以下两种情况之一,才会进行实际的测量工作:

forceLayout为true:这表示强制重新布局,可以通过View.requestLayout()来实现;

needsLayout为true,这需要specChanged为true(表示本次传入的MeasureSpec与上次传入的不同)

layout阶段

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定为后,它在onLayout中会遍历所有的子元素并调用其layout方法。

layout方法的大致流程如下:首先通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mTop、mRight和mBottom四个参数,这四个参数描述了View相对其父View的位置。在setFrame()方法中会判断View的位置是否发生了改变,若发生了改变,则需要对子View进行重新布局,对子View的局部是通过onLayout()方法实现了。

draw阶段

draw过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:

绘制背景

绘制自己

绘制children

绘制装饰

public void draw(Canvas canvas) {
. . .
// 绘制背景,只有dirtyOpaque为false时才进行绘制,下同
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}

. . .

// 绘制自身内容
if (!dirtyOpaque) onDraw(canvas);

// 绘制子View
dispatchDraw(canvas);

. . .
// 绘制滚动条等
onDrawForeground(canvas);

}


如何对自定义View进行控制

如果想控制View在屏幕上的渲染效果,就在重写onDraw()方法,在里面进行相应的处理。

如果想要控制用户同View之间的交互操作,则在onTouchEvent()方法中对手势进行控制处理。

如果想要控制View中内容在屏幕上显示的尺寸大小,就重写onMeasure()方法中进行处理。

在 XML文件中设置自定义View的XML属性。

如果想避免失去View的相关状态参数的话,就在onSaveInstanceState() 和 onRestoreInstanceState()方法中保存有关View的状态信息。

Android线程和线程池

在Android中,扮演线程角色的有AsyncTask,IntentService,HandlerThread.

AsyncTask

AsyncTask封装了线程池和Handler,主要是为了方便开发者在子线程中更新UI。

HandlerThread

HandlerThread是一种具有消息循环的线程,在它的内部可以使用Handler。

IntentService

IntentService是一个服务,系统对其进行了封装使其可以更方便地执行后台任务,内部采用HandlerThread来执行任务,当任务执行完毕后IntentService会自动退出。

从任务执行的角度来看,IntentService的作用很像一个后台线程,但是它是一种服务,不容易被系统杀死从而可以保证任务的执行。这就是其优点。

线程池

在操作系统中,线程是操作系统调度的最小单元,同时线程又是一种受限的系统资源,即线程不可能无限制的产生,并且线程的创建和销毁都会有相应的开销。

正确的做法是采用线程池,一个线程池中会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。

Bitmap的加载和Cache

《Android 开发艺术探索》

Bitmap是Android系统中的图像处理的最重要类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。BitmapFactory类提供了四类方法来加载一个图片:decodeFile、decodeResource、decodeStream、decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载一个Bitmap对象。

怎样有效且高效的加载一个Bitmap是一个很有意义的话题,由于Bitmap的特殊性以及Android对单个应用所施加的内存限制,比如16MB,这导致加载Bitmap的时候容易出现内存溢出。

bitmap的高效加载

核心思想就是采用BitmapFactory.Options来加载所需尺寸的图片。

举个例子,假设通过ImageView来显示图片,很多时候ImageView并没有原始图片那么大尺寸,这个时候就可以通过BitmapFactory.Options按一定的采样率来加载缩小后的图片。

通过BitmapFactory.Options来缩放图片,主要用到了它的inSampleSize参数,即采样率。当其为1时不缩放,为2时,即采样后的图片其宽、高均为原图的1/2,而像素为原图的1/4,所以占的内存也为原图的1/4.(采样率小于1没效果)

inSampleSize的取值应该总是为2的指数,如果不为2的指数,系统会向下取整并选择一个最近的2的指数来代替。比如3,系统会使用2来代替。

获取采样率的过程:

1. 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图像

2. 从BitmapFactory.Options中取出图片的原始宽高信息,他们对应于outWidth和outHeight参数

3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize.

4. 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。

注释:inJustDecodeBounds为true时,BitmapFactory只会解析图片的原始宽高信息,并不会真正去加载图片。

Android中的缓存策略

当程序第一次从网上加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不会再从网上下载了。

很多时候为了提高应用的用户体验,往往还会把图片在内存中也缓存一份,这样当应用打算从网络上请求一张图片时,程序会首先从内存中取获取,然后再从存储中获取,如果都没有最后才从网络下载。这样既提高了程序的效率又节约了不必要的流量开销。

在使用缓存时,要为其指定一个最大容量。当容量满了以后,采用LRU(Least Recently Used)近期最少使用算法来移除缓存内容。

优化列表的卡顿现象

核心思想:不要在主线程中做太耗时的操作即可。

首先,不要再getView中执行耗时操作。必须使用异步的方式来处理。

其次,控制异步任务的执行频率。以照片墙为例,如果用户频繁的上下滑动,必定带来大量UI更新,所以解决思路就是:在滑动时停止加载图片,等列表停下来以后再加载图片。

最后,通过开启硬件加速解决卡顿。

Android性能优化

Android程序不可能无限制地使用内存和CPU资源,过多地使用内存会导致程序内存溢出,即OOM。而过多地使用CPU资源,一般是指做大量的耗时任务,会导致手机变得卡顿甚至出现程序无法响应的情况,即ANR.

一些有效的性能优化方法:布局优化、绘制优化、内存泄漏优化、响应速度优化、ListView优化、Bitmap优化、线程优化。

布局优化

思想:尽量减少布局文件的层级。

首先删除布局中无用的控件和层级,其次有选择地使用性能较低的ViewGroup,比如RelativeLayout。尽量使用性能较高的ViewGroup如LinearLayout。

另一种手段就是采用标签、标签和ViewStub.标签主要用于布局重用,标签和标签配合使用,降低减少布局的层级。而ViewStub则提供了按需加载功能,提高了程序的初始化效率。

绘制优化

绘制优化是指View的onDraw方法要避免执行大量的操作。主要体现在两个方面:

1. onDraw中不要创建新的局部对象。

2. onDraw中不要做耗时的任务。

内存泄露优化

场景1、静态变量导致的内存泄露

场景2、单例模式导致的内存泄露:比如一个Activity实现了某注册监听了某单例模式的类,但是缺少解注册的操作所以会引起内存泄露,原因是Activity的对象被单例模式所持有的,而单例模式的特点是其生命周期和Application保持一致,因此Activity对象无法被及时释放。

场景3、属性动画导致的内存泄漏。

ListView和Bitmap优化

ListView的优化主要分三个方面:

1、采用ViewHolder,并避免在getView中执行耗时操作。

2、根据列表的滑动状态来控制任务的执行频率。

3、尝试开启硬件加速

Bitmap的优化:

主要通过BitmapFactory.Options来根据需要对图片进行采样。

线程优化

采用线程池,避免程序中存在大量的Thread。

线程池可以重用内部线程,从而避免了线程的创建和销毁所带来的性能开销,同时线程池还能有效地控制线程池的最大并发数,避免大量的线程因为相互抢占系统资源从而导致阻塞现象的发生。

一些性能优化建议:

避免创建过多地对象

不要过多使用枚举,枚举占用的内存空间要比整形大

常量请使用static final来修饰

使用一些Android特有的数据结构

适当使用软引用和弱引用

采用内存缓存和磁盘缓存

尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄漏
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息