Android 进程通信原理

Android系统提供了一些通用服务,比如音乐打电话发短信,WIFI,定位,输入法,传感器等。应用程序与这些通用服务运行在不同的进程中,如果应用程序想要与这些通用服务交互就要涉及到进程间通信,Binder就是为了Android进程间通信而设计的。

Binder框架

Binder是一种架构,这种架构提供了服务端接口、Binder驱动、客户端接口三个模块。

服务端
Binder服务端相当于一个Binder类对象。当创建该对象时,其内部会启动一个线程不断接收Binder驱动发送的消息。收到消息后会执行Binder.onTransact()方法。所以要实现一个Binder服务就必须重载onTransact()方法。

onTransact()方法通常用来做数据格式转换,按约定的顺序取出Binder客户端发送来的数据并转换成服务端识别的数据格式。

Binder驱动
Binder驱动运行在内核态,其所有操作都是基于内存而非硬件,客户端与服务端通信时需要Binder驱动进行中转。

当一个服务端Binder被创建时其在Binder驱动中同时会创建一个mRemote引用指向服务端。客户端要访问服务端时首先要获取服务端在Binder中的引用mRemote,获取引用后就可以通过mRemote.transact()方法向服务端发送消息。

transact()方法实现了以下几个功能:

  • 以线程间消息通信的模式向服务端发送客户端传递过来的参数。
  • 挂起当前客户端线程,等待服务端线程执行完毕后的通知(notify)
  • 接收服务端线程通知,继续执行客户端线程,并返回执行结果

客户端
这里就是指我们需要和系统服务交互的应用程序。应用程序使用startService()与应用程序Service建立连接,使用getSystemService()与系统Service建立连接,从而进行通信。

数据格式
在进行IPC通信时需要约定客户端与服务端通信的数据格式。Android使用AIDL(Android Interface Definition Language)约定数据格式。

Android提供了一个AIDL工具,可以把AIDL文件转换成一个Java类,在该Java类中同时重载了onTransact()方法和transact()方法,统一了存入和读取参数。

实现Binder服务端

1
2
3
4
5
6
7
8
9
10
11
public class MusicService extends Binder{
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException{
return super.onTransact(code,data,reply, flags);
}

public void start(String filePath){

}
public void stop(){}
}

以上代码基于Binder创建了一个Service,使用startService()方法启动后就可以看到DDMS中多了一个线程。

重载onTransact(int code, Parcel data, Parcel reply, int flags)方法:

1
2
3
4
5
6
7
8
switch(code){
code 1000:
data.enforceInterface("MusicService");// 数据校验与客户端writeInterfaceToken()对应
String filePath = data.readString();//读取一个字符串
start(filePath);
// reply.writeXXX();//如果需要返回结果则写入reply中
break;
}

data中保存着客户端传递过来的参数,onTransact()方法内部需要从data中读取客户端传递的参数。参数的位置格式需要在AIDL文件中约束。
reply中保存服务端返回客户端的结果,如果需要返回则调用Parcel提供的相关方法写入结果。参数的位置格式需要在AIDL文件中约束。
code变量标识客户端希望服务端执行哪个方法,这里假定1000执行start()方法。
flags表示IPC调用模式:0代表”双向模式”,服务端在执行完指定服务后会返回数据;1代表”单向模式”,服务端在执行完指定服务后不反回任何数据。

实现Binder客户端

首先在startService(),getSystemService()中获取服务端Binder的引用,然后将参数按AIDL定义的格式写入data中并调用mRemote.transact()方法传入参数,服务端会在onTransact()方法中取出这里出入的参数:

1
2
3
4
5
6
7
8
9
10
11
IBinder mRemote = null;
String filePath = "/sdcard/music/xxx.mp3";
int code = 1000;
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken("MusicService");
data.writeString(filePath);
mRemote.transact(code,data,reply,0);
IBinder binder = reply.readStrongBinder();
reply.recycle();
data.recycle();

调用mRemote.transact()方法后,Binder驱动会挂起当前客户端线程,并向服务端发送一个消息,这个消息就包括客户端传递的参数。服务端接收到消息,解析数据执行完相关任务后会把执行结果写入reply中,然后向Binder驱动发送一个notify消息,Binder驱动从挂起处唤醒客户端线程继续执行。

系统Service与应用程序Service

Service也就是服务分为两种,一种是系统创建的Service,另一种是应用程序自定义的Service。这两种Service都是Binder服务端。

系统Service
系统Service在系统系统初始化时从SystemServer进程中启动。常见的有PowerManagerService(电源管理服务),VibratorService(振动传感器服务),WindowManagerService(窗口管理服务)NotificationManagerService等等,每个系统服务都是一个Binder,都运行在一个独立的线程中。

系统提供了一个ServiceManager类来管理系统服务ServiceManager本身也是一个Binder,运行在一个独立的进程中,其提供了一个全局Binder供应用程序使用,应用程序获取其他系统服务都是通过ServiceManager的全局Binder来获取的。

这设计的好处仅仅暴露一个全局Binder引用,其他系统服务则隐藏起来,从而有助于系统扩展,以及调用系统服务的安全检查。

系统Service在启动时首先向ServiceManager注册Service(注册自己),当调用getSystemService(serviceName)获取系统服务时,会间接调用到ServiceManager.getService(String name)方法。getService()实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public static IBinder getService(String name){
try{
IBinder service = sCache.get(name);
if(service != null){
return service;
}else{
return getIServiceManager().getService(name);
}
} catch(RemoteException e){
Log.e(TAG,"error in get Service", e);
}
}

首先从缓存中获取,没获取到则从getIServiceManager()中获取,getIServiceManager()返回的是系统唯一ServiceManager的Binder。

应用程序Service
系统在ActivityManagerService中提供了startService()方法来启动应用程序自定义Service。客户端使用以下方法和应用程序Service建立连接:

1
2
public ComponentName startService(Intent intent)
public boolean bindService(Intent service, ServiceConnection conn,int flags)

startService用于启动一个服务,bindService用于绑定一个服务,bindService的第二个参数是获取服务端Binder的关键所在:

1
2
3
4
public interface ServiceConnection{
public void onServiceConnected(ComponentName name,IBinder service);
public void onServiceDisconnected(ComponentName name);
}

onServiceConnected()方法的第二个变量就是我们需要的服务端Binder的引用,当客户端请求AmS启动某个Service,如果该Service正常启动,那么AmS就会远程调用ActivityThread类中的ApplicationThread对象,调用的参数包括Service的Binder引用,然后在ApplicationThread中会回调bindService()方法中的conn接口。因此,在客户端中,可以在onServiceConnected()方法中将其参数Service保存为一个全局变量,从而在客户端任何地方都可以随时调用该服务。

Android 内存管理

Android是一个基于Linux实现的操作系统。但对于Linux内核来说,Android也仅仅只是一个运行在内核之上的应用程序,与其他运行在内核之上的应用程序没有任何区别。所以Android需要一套机制管理运行在Linux进程中的APK应用程序。Android内存管理包含两部分,一部分是Framework对内存的管理,一部分是Linux内核对内存管理,这两部分共同决定应用程序的生命周期。本文主要阐述Android内存管理机制的实现原理,以及在应用开发中需要注意的一些事项,最后本文总结了如何实现杀不死进程的一种方法。

Linux 进程回收

在Android中,大部分应用程序都运行在一个独立的Linux进程中,每个进程都有独立的内存空间。随着各种应用程序启动,系统内存不断下降,为了保证新应用能够运行,Android需要一套机制杀死暂时闲置的进程。

Android Framework并不能直接回收内存,其管理进程的服务(ActivityManagerService,以下简称AmS)也同应用程序一样运行在Java虚拟机环境里。Java虚拟机都运行在各自独立的内存空间,所以ActivityManagerService没有办法感知应用程序是否OOM。

Android系统中还运行了一个OOM进程。该进程启动时首先会在Linux内核中把自己注册为一个OOM Killer。AmS需要把每一个应用程序的oom_adj值告知OOM Killer,这个值的范围在-16到15之间,值越低,说明越重要,这个值类似于Linux中的nice值,只在标准的Linux中,有其自己的OOM Killer。Android中的OOM Killer进程仅仅适用于Android应用程序。

当内核的内存管理模块检测到系统内存不足时就会通知OOM Killer,然后OOM Killer根据AmS所告知的优先级强制退出优先级低的应用程序。

应用程序在内存中的状态

Android官方声称,Activity退出后,其所在进程并不会被立即杀死,从而在下次启动Activity时,能够提高启动速度。这些Activity只有在内存紧张时才会被系统杀死。所以对于应用程序来说,关闭并不意味着释放内存。

Activity在内存中的状态
系统只有一个Activity处于与用户交互的状态,对于非交互状态的Activity,AmS会在内部暂时缓存起来而不是立即杀死,但如果后台Activity数目超过一定阈值,AmS则会强制杀死一些优先级低的Activity。以下是Activity在内存或者说在AmS中的状态:

  • AmS会记录最近启动的20个Activity,如果超过20则舍弃最早记录的Activity。
  • AmS会将所有正在运行的Activity保存在一个列表中,对于使用back返回的Activity则从列表中清除。
  • AmS使用Lru算法保存所有最近使用过的Activity。
  • AmS使用一个列表(mStoppingActivities)保存需要停止的Activity,这种情况
    发生在启动一个Activity时,AmS遵循先启动后停止的策略,将需要停止的Activity保存在此列表中,等AmS闲置下来后再停止Activity。
  • AmS使用一个列表保存处于finish状态(onDestory())的Activity,当一个Activity处于finish状态时(onDestory()执行后)不会被立即杀死,而是保存到该列表中直到超过系统设定的警戒线才会回收该列表中的Activity。

应用进程在内存中的状态
每个应用程序都对应着一个ActivityThread类,该类初始化后就进入Looper.loop()函数中无限循环。

1
2
3
4
5
6
Looper.prepareMainLooper();
...
ActivityThread thread = new ActivityThread();
thread.attach(false);
...
Looper.loop();

以后则依靠消息机制运行,既当有消息时处理消息,没有消息则应用进程进入sleep状态。loop()方法内部代码如下所示:

1
2
3
4
5
6
7
8
public static final loop() {
Looper me = myLooper();
MessageQueue queue = me.mQueue;
while(true){
Message msg = queue.next();// might block
...
}
}

在Linux内核调度中,如果一个线程的状态为sleep,则除了占用调度本身的时间,不会占用CPU时间片。

有三种情况会唤醒应用线程,一种是定时器中断(比如我们设置的闹钟,在程序中可以设置定时任务),第二种是用户按键消息,第三种是Binder消息(Binder用于进程间通信,其在应用程序中会自动创建一个线程,Binder在接收到消息后会想UI主线程发送一个消息从而使queue.next()继续执行)这就是所谓的消息驱动模式。

所以设计良好的应用程序当处于后台时不会占用任何CPU时间,更不会拖慢系统运行速度。其所占用的仅仅是内存,即使释放所占用的内存也不会提高系统运行速度。当然这里说的是设计良好的应用程序,目前国内很多应用在处于后台状态时依然会偷偷干很多事情,这无疑就拖慢了系统运行速度。

Android 内存回收

Activity所占内存在一般情况下不会被回收,只有在系统内存不够用时才会回收,并且回收会遵循一定规则。大致可以概括为前台Activity最后回收,其次是包含前台的Service或者Provider,再其次是后台Activity,最后是空进程。

内存释放的三个地方

  • 第一个是在ActivityManagerService中运行,即Android所声称的当系统内存低时,优先释放没有任何Activity的进程,然后释放非前台Activity对应的进程。
  • 第二个是在OOM Killer中,此时AmS只要告诉OOM各个应用的优先级,然后OOM就会调用Linux内部的进程管理方法杀死优先级较低的进程。
  • 第三个是在应用进程本身之中,当AmS认为目标进程需要被杀死时,首先会通知目标进程进程内存释放。这包括调用目标进程的scheduleLowMemory()方法和processInBackground()方法。

关闭Activity的三种情况

  • 第一种,从调用startActivity()开始,一般情况下,当前都有正在运行的Activity,所以需要先暂停当前的Activity,而暂停完毕后,AmS会收到一个Binder消息,并开始从completePaused()处执行。在该函数中,由于上一个Activity并没有finishing,仅仅是stop,所以这里会把上一个Activity添加到mStoppingActivity列表中。当目标Activity启动后,会向Ams发送一个请求进行内存回收的消息,这会导致AmS在内部调用activityIdleInternal()方法,该方法中首先会处理mStoppingActivities列表中的Activity,这就会调用stopActivityLocked()方法。这又会通过IPC调用,通知应用进程stop指定的Activity,当stop完毕后,再报告给AmS,于是AmS再从activityStopped()出开始执行,而这会调用trimApplication()方法,该方法会执行内存相关的操作。
  • 第二种,当按Back键后,会调用finishActivityLocked(),然后把该Activity的finishing标识设为true,然后再调用startPausingLocked(),当目标Activity完成暂停后,就会报告AmS,此时AmS又会从completePaused()处开始执行。与第一种情况不同,由于此时暂停的Activity的finishing状态已经设置为true,所以会执行finishingActivityLocked(),而不是像第一种情况中仅仅把该Activity添加到mStoppingActivities列表。
  • 第三种,当Activity启动后,会向AmS发送一个Idle消息,这会导致AmS开始执行activityIdleInternal()方法。该方法会首先处理mStoppingActivities列表中的对象,接着处理mFinishingActivities列表,最后再调用trimApplication()方法。

以上就是关闭Activity的三种情况,包括stop和destory,客户进程中与之对应的就是onStop()和onDestory()的调用。

如果使用OOM还有AmS机制杀死后台进程后,此时运行的Activity数量依然超过MAX_ACTIVITIES(20),则需要继续销毁满足以下三个条件的Activity:

  1. Activity必须已经stop,但却没有finishing
  2. 必须是不可见的,既该Activity窗口上面有其他全屏的窗口,如果不是全屏,则后面的Activity是可见的。
  3. 不能是persistent类型,既常驻进程不能被杀死。

进程优先级

Android系统试图尽可能长时间地保持应用程序进程,但为了新建或者运行更加重要的进程,总是需要清除过时进程来回收内存。为了决定保留或终止哪个进程,根据进程内运行的组件及这些组件的状态,系统把每个进程都划入一个“重要性层次结构”中。重要性最低的进程首先会被清除,然后是下一个最低的,依此类推。

重要性层次结构共有5级,以下列表按照重要程度列出了各类进程(第一类进程是最重要的,将最后一个被终止):

1)前台进程

用户当前操作所必须的进程。满足以下任一条件时,进程被视作处于前台:
其中运行着正与用户交互的Activity(Activity对象的onResume()方法已被调用)。
其中运行着与用户交互的activity绑定的Service。
其中运行着前台Service,既该Service以startForeground()方式被调用。
其中运行着正在执行生命周期回调方法(onCreate()、onStart()或onDestory())的Service。
其中运行着正在执行onReceive()方法的BroadcastReceiver。

一般而言,任何时刻前台进程的数量都为数不多,只有当内存不足以维持它们同时运行时才会被终止。通常,设备这时候已经到了使用虚拟内存的地步,终止一些前台进程是为了保证用户界面的及时响应。

2) 可见进程

没有前台组件、但仍会影响用户在屏幕上所见内容的进程。满足以下任一条件时,进程被认为是可见的:
其中运行着非前台Activity,但用户仍然可见到此activity(onPause()方法被调用)。例如,打开了一个对话框,而activity还允许显示在对话框后面,对用户依然可见。
其中运行着被可见(或前台)activity绑定的Service。

可见进程被认为是非常重要的进程,除非无法维持所有前台进程同时运行了,否则它们是不会被终止的。

3) 服务进程

此进程运行着由startService()方法启动的服务,它不会升级为前台进程或可见进程。尽管服务进程不直接和用户所见内容关联,但他们通常在执行一些用户关心的操作(比如在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台、可见进程同时运行,系统会保持服务进程的运行。

4) 后台进程

包含用户不可见activity(Activity对象的onStop()方法已被调用)的进程。这些进程对用户体验没有直接的影响,系统可能在任意时间终止它们,以回收内存供前台进程、可见进程及服务进程使用。

通常系统会有很多后台进程在运行,所以它们被保存在一个LRU(最近最少使用)列表中,以确保最近被用户使用的activity最后一个被终止。如果一个activity正确实现了生命周期方法,并保存了当前的状态,则终止此类进程不会对用户体验产生可见的影响。因为在用户返回时,activity会恢复所有可见的状态。关于保存和恢复状态的详细信息,请参阅Activity文档。

5) 空进程

不含任何活动应用程序组件的进程。保留这种进程的唯一目的就是用作缓存,以改善下次在此进程中运行组件的启动时间。为了在进程缓存和内核缓存间平衡系统整体资源,系统经常会终止这种进程。

依据进程中目前活跃组件的重要程度,Android会给进程评估一个尽可能高的级别。例如,如果一个进程中运行着一个服务和一个用户可见的activity,则此进程会被评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会由于其它进程的依赖而被提高——为其它进程提供服务的进程级别永远不会低于使用此服务的进程。比如:如果A进程中的content provider为进程B中的客户端提供服务,或进程A中的服务被进程B中的组件所调用,则A进程至少被视为与进程B同样重要。

因为运行服务的进程级别是高于后台activity进程的,所以,如果activity需要启动一个长时间运行的操作,则为其启动一个服务会比简单地创建一个工作线程更好些——尤其是该操作时间比activity的生存期还要长的情况下。比如,一个activity要把图片上传至Web网站,就应该创建一个服务来执行之,即使用户离开了此activity,上传还是会在后台继续运行。不论activity发生什么情况,使用服务可以保证操作至少拥有“服务进程”的优先级。同理,广播接收器broadcast receiver也是使用服务来处理耗时任务的,而不是简单地把它放入线程中。

杀不死的Service

如何让应用在手机中存活更长时间?网上各种方法可谓是千奇百怪,有些简直异想天开。

  • 系统广播唤醒应用,比如手机开机,网络切换等
  • 接入第三方SDK唤醒应用,比如接入微信SDK会唤醒微信
  • 免杀白名单,比如360免杀白名单,MIUI系统免杀白名单
  • 全家桶,应用之间互相唤醒,比如百度系,阿里系应用
  • 两个Service互相唤醒(这个就别想了,不靠谱)
  • 使用Timer定时器(一样不靠谱)

设计良好的应用不应该在用户不使用的时候依然保持运行。一直在后台运行不光费电费流量,还是造成系统卡顿的主要原因之一(参见上文分析)。正常的做法是优化你的应用程序,减少不合理场景的情况,除一些必要服务应用外,大部分应用不需要一直在后台保存运行状态。

有正常的做法就有不正常的做法,让应用长时间停留在用户手机中无外乎就是增加所谓的活跃用户数等一些产品指标。这对于很多公司还是很有吸引力的。

如上文所说,无论应用怎么挣扎,当处于不可见进程的情况下随时都有可能被杀死。所以使用前台进程是最有效的方法。但前台进程必须有一个Notifcation显示在通知栏中,有没有办法让应用以前台进程的方式启动同时又不显示Notifcation?方法当然有,就是利用系统漏洞:

  • API<18,启动前台Service时直接传入new Notifcation();
  • API>=18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理

目前,QQ,微信,支付宝等知名应用都使用此方案。不过如果应用占用太多内存即使是前台进程也依然会被干掉。

这些所谓的实现进程杀不死的方案并不都是一劳永逸的方法,以牺牲用户体验为代价很有可能会激怒用户卸载你的应用,所以最好的方式还是遵循Android规范开发性能更优更合理的应用程序。

Android 资源加载机制详解

Android提供了一种非常灵活的资源系统,可以根据不同的条件提供可替代资源。因此,系统基于很少的改造就能支持新特性,比如Android N中的分屏模式。这也是Android强大部分之一。本文主要讲述Android资源系统的实现原理,以及在应用开发中需要注意的事项。

1.定义资源

Android使用XML文件描述各种资源,包括字符串、颜色、尺寸、主题、布局、甚至是图片(selector,layer-list)。

资源可分为两部分,一部分是属性,另一部分是值。对于android:text="hello,world"text就是属性,hello,world就是值。

1.1 属性的定义

在APK程序中,属性定义在res/values/attrs.xml中,在系统中属性位于framework/base/core/res/res/values/attrs.xml文件中。具体定义如下所示:

1
2
3
4
5
6
<declare-styleable name="Window">
<attr name="windowBackground" format="reference"/>
<attr name="windowContentOverlaly" />
<attr name="windowFrame" />
<attr name="windowTitle" />
</declare-styleable>

styleable相当于一个属性集合,其在R.java文件中对应一个int[]数组,aapt为styleable中的每个attr(属性)分配一个id值,int[]中的每个id对应着styleable中的每一个attr。

对于<declare-styleable name="Window">,Window相当于属性集合的名称。
对于<attr name="windowBackground">,windowBackground相当于属性的名称;属性名称在应用程序范围内必须唯一,既无论定义几个资源文件,无论定义几个styleable,windowBackground必须唯一。

在Java代码中,变量在一个作用域内只能声明一次,但可以多次使用。attr也是一样,只能声明一次,但可以多处引用。如上代码所示,在Window中声明了一个名为windowBackground的attr,在Window中引用了一个名为windowTitle的attr

如果一个attr后面仅仅有一个name,那么这就是引用;如果不光有name还有format那就是声明。windowBackground是属性的声明,其不能在其他styleable中再次声明;windowTitle则是属性的引用,其声明是在别的styleable中。

1.2 值的定义

常见的值一般有以下几种:

  • String,Color,boolean,int类型:在res/values/xxx.xml文件中指定
  • Drawable类型:在res/drawable/xxx中指定
  • layout(布局):在res/layout/xxx.xml中指定
  • style(样式):在res/values/xxx.xml中指定

值的类型大致分为两类,一类是基本类型,一类是引用类型;对于int,boolean等类型在声明属性时使用如下方式:
<attr name="width" format="integer"/>
<attr name="text" format="string" />
<attr name="centerInParent"="boolean"/>
对于Drawable,layout等类型在声明属性时:
<attr name="background" format="reference"/>

2.解析资源

资源解析主要涉及到两个类,一个是AttributeSet,另一个是TypedArray。

2.1 AttributeSet

该类位于android.util.AttributeSet,纯粹是一个辅助类,当从XML文件解析时会返回AttributeSet对象,该对象包含了解析元素的所有属性及属性值。并且在解析的属性名称与attrs.xml中定义的属性名称之间建立联系。AttributeSet还提供了一组API接口从而可以方便的根据attrs.xml中已有的名称获取相应的值。

如果使用一般的XML解析工具,则可以通过类似getElementById()等方法获取属性的名称和属性值,然而这样并没有在获取的属性名称与attrs.xml定义的属性名称之间建立联系。

Attribute对象一般作为View的构造函数的参数传递过来,例如:

1
publlic TextView(Context context,AttributeSet attrs,int defStyle)

AttributeSet中的API可按功能分为以下几类,假定TextView定义如下所示:

1
2
3
4
5
6
<TextView
android:id="@+id/tv"
android:layout_width="@dimen/width"
android:layout_height="wrap_content"
style="@stylel/text"
/>

第一类,操作特定属性:

  • public String getIdAttribute(),获取id属性对应的字符串,此处返回”@+id/tv”
  • public String getStyleAttribute(),获取style属性对应的字符串,返回”@style/text”
  • public int getIdAttributeResourceValue(int defaultValue),返回id属性对应的int值,此处对应R.id.tv。

第二类,操作通用属性:

  • public int getAttributeCount(),获取属性的数目,本例中返回4
  • public String getAttributeName(int index),根据属性所在位置返回相应的属性名称。例如,id=0,layout_width=1,layout_height=2,style=3,如果getAttributeName(2),则返回android:layout_height
  • public String getAttributeValue(int index),根据位置返回值。本例中,getAttributeValue(2)则返回”wrap_content”。
  • public String getAttributeValue(String namespace,String name),返回指定命名空间,指定名称的属性值,该方法说明AttributeSet允许给一个XML Element的属性增加多个命名空间的属性值。
  • public int getAttributeResource(int index),返回指定位置的属性id值。本例中,getAttributeResource(2)返回R.attr.layout_width。前面也说过,系统会为每一个attr分配一个唯一的id。

第三类,获取特定类型的值:

  • public XXXType getAttributeXXXType(int index,XXXType defaultValue),其中XXXType包括int、unsigned int、boolean、float类型。使用该方法时,必须明确知道某个位置(index)对应的数据类型,否则会返回错误。而且该方法仅适用于特定的类型,如果某个属性值为一个style类型,或者为一个layout类型,那么返回值都将无效。

2.2 TypedArray

程序员在开发应用程序时,在XML文件中引用某个变量通常是android:background=”@drawable/background”,该引用对应的元素一般为某个View/ViewGroup,而View/ViewGroup的构造函数中会通过obatinStyledAttributes方法返回一个TypedArray对象,然后再调用对象中的getDrawable()方法获取背景图片。

TypedArray是对AttributeSet数据类的某种抽象。对于andorid:layout_width="@dimen/width",如果使用AttributeSet的方法,仅仅能获取”@dimen/width”字符串。而实际上该字符串对应了一个dimen类型的数据。TypedArray可以将某个AttributeSet作为参数构造TypedArray对象,并提供更方便的方法直接获取该dimen的值。

1
TypedArray a = context.obtainStyledAttributes(attrs,com.android.internal.R.styleable.XXX,defStyle,0);

方法obtainStyledAttributes()的第一个参数是一个AttributeSet对象,它包含了一个XML元素中定义的所有属性。第二个参数是前面定义的styleable,appt会把一个styleable编译成一个int[]数组,该数组的内部实现正是通过遍历AttributeSet中的每一个属性,找到用户感兴趣的属性,然后把值和属性经过重定位,返回一个TypedArray对象。想要获取某个属性的值则调用相关的方法即可,比如TypedArray.getDrawbale(),TypedArray.getString()等。getDrawable(),getString()方法内部均通过Resources获取属性值。

3.加载资源

在使用资源时首先要把资源加载到内存。Resources的作用主要就是加载资源,应用程序需要的所有资源(包括系统资源)都是通过此对象获取。一般情况下每个应用都会仅有一个Resources对象。

要访问资源首先要获取Resources对象。获取Resources对象有两种方法,一种是通过Context,一种是通过PackageManager。

3.1 使用Context获取Resources

抽象类Context内部个有getResources()方法,一般是在Activity对象或者Service对象中调用,因为Activity或者Service的本质是一个Context,而真正实现Context接口的是ContextImpl类。

获取Resources对象流程图

ContextImpl对象是在ActivityThread类中创建,所以getResources()方法实际上是调用ContextImpl.getResources()方法。在ContextImpl类中,该方法仅仅是返回内部的mResources变量,而对该变量赋值是在init()方法中。在创建ContextImpl对象后,一般会调用init()方法对ContextImpl对象内部变量初始化,其中就包括mResources变量,如以下代码所示:

1
2
3
4
final void init(ActivityThread.PackageInfo packageInfo, IBinder activityToken, ActivityThread mainThread, Resources container){
mPackageInfo = packageInfo;
mResources = mPackageInfo.getResources(mainThread);
}

从以上代码可以看出,mResources又是调用mPackageInfo的getResources()方法进行赋值。一个应用程序中可以有多个ContextImpl,但多个ContextImpl对象共享一个PackageInfo对象。所以多个ContextImpl对象中的mResources变量实际上是同一个Resources对象。

PackageInfo.getResources()方法如下所示:

1
2
3
4
5
public Resources getResources(ActivityThread mainThread){
if(mResources == null){
mResources = mainThread.getTopLevelResources(mResDir,this);
}
}

以上代码中,参数mainThread指的就是ActivityThread对象,每个应用程序只有一个ActivityThread对象。getTopLevelResources()方法就是获取本应用程序中的Resources对象。

在ActivityThread对象中,使用HashMap<ResourcesKey,WeakReference<Resources>> mActiveResources保存该应用程序所有的Resources对象,并且这些Resources都是以一个弱引用保存起来的,这样在内存紧张时可以释放Resources所占的内存。

在mActiveResources中,使用ResourcesKey映射Resources类,ResourcesKey仅仅是一个数据类,其创建方式如下所示:

1
ResourcesKey key = new ResourcesKey(resDir,compInfo.applicatioScale);

resDir变量代表资源文件所在路径,实际是指APK程序所在路径,例如 /data/app/xxx.apk。该APK会对应/data/dalvik-cache目录下的data@app@xxx.apk@classes.dex文件,这两个文件也是应用程序安装后自动生成的文件。

如果一个应用程序没有访问该应用程序以外的资源,那么mActivieResources变量中就仅有一个Resources对象。当应用程序想要访问其他应用程序的资源则需要构建不同的ResourcesKey,也就是需要不同的resDir,毕竟每一个ResourcesKey对应一个Resources对象,这样该应用程序就可以访问其他应用程序中的资源。

如果mActiveResources中还没有包含所要的Resources对象,那就需要重新创建一个:

1
2
3
4
5
6
AssetManager assets = new AssetManager();
if(assets.addAssetPath(resDir) == 0){
return null;
}
DisplayMetrics metrics = getDisplayMetricsLocked(false);
r = new Resources(assets,metrics,getConfiguration(),compInfo);

创建Resources需要一个AssetManager对象。在开发应用程序时,使用Resources.getAssets()获取的就是这里创建的AssetManager对象。AssetManager其实并不只是访问res/assets目录下的资源,而是可以访问res目录下的所有资源。

AssetManager在初始化的时候会被赋予两个路径,一个是应用程序资源路径 /data/app/xxx.apk,一个是Framework资源路径/system/framework/framework-res.apk(系统资源会被打包到此apk中)。所以应用程序使用本地Resources既可访问应用程序资源,又可访问系统资源。

AssetManager中很多获取资源的关键方法都是native实现,当使用getXXX(int id)访问资源时,如果id小于0x1000 0000时表示访问系统资源,如果id都大于0x7000 0000则表示应用资源。aapt在对系统资源进行编译时,所有资源id都被编译为小于0x1000 0000。

当创建好Resources后就把该对象放到mActivieResources中以便以后继续使用。

3.2 使用PackageManager获取Resources

该方法主要是用来访问其他应用程序中的资源,最典型的就是切换主题,但这种主题一般仅限于一个应用程序内部。获取Resources的过程如下所示:
使用PackageManager获取Resources对象流程

使用PackageManager获取Resources对象:

1
2
PackageManager pm = mContext.getPackageManager();
pm.getResourcesForApplication("com.android...your package name");

其中getPackageManager()返回一个PackageManager对象,PackageManager本身是一个abstract类,其真正实现类是ApplicationPackageManager。其内部方法一般调用远程PackageManagerService。ApplicationPackageManager在构造时传入一个远程服务的引用IPackageManager,该对象是通过调用getPackageManager()静态方法获取的。这种获取远程服务的方法和大多数获取远程服务的方法类似:

1
2
3
4
5
6
7
8
public static IPackageManager getPackageManager(){
if(sPackageManager !=null){
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b);
return sPackageManager;
}

获得了PackageManager对象后,接着调用getResourcesForApplication()方法,该方法位于ContextImpl.ApplicationPackageManager中:

1
2
3
4
5
6
7
8
9
10
11
@Override
public Resources getResourcesForApplication(ApplicationInfo app) throws NameNotFoundException{
if(app.packageName.equals("system")){
return mContext.mMainThread.getSystemContext().getResources();
}
Resources r = mContext.mMainThread.getTopLevelResources(app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir,mContext.mPackageInfo);
if(r != null){
return r;
}
throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
}

以上代码内部调用mMainThread.getTopLevelResources()方法,又回到了使用Context获取Resources对象的过程中。注意,此处调用参数的含义:如果目标资源程序和当前程序是同一个uid,那么就使用目标程序的sourceDir作为路径,否则就使用目标程序的publicSourceDir目录,该目录可以在AndroidManifest.xml中指定。在大多数情况下,目标程序和当前程序不属于同一个uid,因此,多为publicSourceDir,而该值默认情况下和sourceDir的值相同。

当进入mMainThread.getTopLevelResources()方法后,ActivityThread对象就会在mActivieResources变量中保存一个新的Resources对象,其键值对应目标程序的包名。

3.3 加载应用程序资源

应用程序打包的最终文件是xxx.apk。APK本身是一个zip文件,可以使用压缩工具解压。系统在安装应用程序时首先解压,并将其中的文件放到指定目录。其中有一个文件名为resources.arsc,APK所有的资源均在其中定义。

resources.arsc是一种二进制格式的文件。aapt在对资源文件进行编译时,会为每一个资源分配唯一的id值,程序在执行时会根据这些id值读取特定的资源,而resources.arsc文件正是包含了所有id值得一个数据集合。在该文件中,如果某个id对应的资源是String或者数值(包括int,long等),那么该文件会直接包含相应的值,如果id对应的资源是某个layout或者drawable资源,那么该文件会存入对应资源的路径地址。

事实上,当程序运行时,所需要的资源都要从原始文件中读取(APK在安装时都会被系统拷贝到/data/app目录下)。加载资源时,首先加载resources.arsc,然后根据id值找到指定的资源。

3.4 加载Framework资源

系统资源是在zygote进程启动时被加载的,并且只有当加载了系统资源后才开始启动其他应用进程,从而实现其他应用进程共享系统资源的目标。

启动第一步就是加载系统资源,加载完毕后再调用startSystemServer()启动系统进程,并最后调用runSelectLoopMode()开始监听Socket,并启动指定的应用进程。加载系统资源是通过preLoadResources()完成的,该方法关键代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
mResources = Resources.getSystem();
mResources.startPreLoading();
if(PRELOAD_RESOURCES){
long startTime = SystemClock.uptimeMillis();
TypeArray ar = mResources.obtainTypedArray(com.android.internal.R.array.preloadingdrawables);
int N = prelaodDrawables(runtime,ar);
Log.i(TAG,"...preloading " + N + "resources in " + (SystemClock.uptimeMillis()-startTime) + "ms.");
startTime = SystemClock.uptimeMillis();
ar = mResources.obtainTypedArray(com.android.internal.R.array.preloading_color_state_lists);
N = preloadingColorStateLists(runtime,ar);
Log.i(TAG,"...preloaded " + N + "resources in " + (SystemClock.uptimeMillis()-startTime) + "ms.");
}
mResources.finishPreloading();

在以上代码中使用Resources.getSystem()创建Resources对象,一般情况下应用程序不应该调用此方法,因为该方法返回的Resources仅能访问Framework资源。

当Resources对象创建完成后,调用preloadDrawables()和preloadColorStateLists()装在需要”预装载”的资源。这两个方法都需要传入一个TypeArray,其来源是res/values/arrays.xml中定义的一个array数组资源,例如:

1
2
3
4
5
6
7
8
9
<array name="preloaded_drawables">
<item>@drawable/sym_def_app_icon</item>
<item>@drawable/arrow_down_float</item>
</array>

<array name="preloaded_color_state_lists">
<item>@color/hint_foreground_dark</item>
<item>@color/hint_foreground_light</item>
</array>

在Resources类中,相关资源读取函数需要将读取到的资源缓冲起来,以便以后使用,Resources类中定义了四个静态变量缓冲这些资源:

1
2
3
4
private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables = new LongSparseArray<Drawable.ConstantState>();
private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists = new LongSparseArray<ColorStateList>();
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables = new LongSparseArray<Drawable.ConstantState>();
private static boolean mPreloaded;

其中前三个变量是列表类型,并且被static修饰,所有Resources对象均共享这三个变量。所以当应用程序创建新的Resources对象时可以访问系统资源。

第四个变量用来区分是zygote装在资源还是普通应用进程装在资源。因为zygote与普通进程装载资源的方式类似,所以增加mPreloaded变量进行区分。

mPreloaded在startPreloading()中被置为true,在finishPreloading()中被置为false,而startPreloading()和finishPreloading()正是在ZygoteInit.java的preloadResources()中被调用,这就区别了zygote调用和普通进程调用。

最后,在Resources的具体资源读取方法中,会判断mPreloaded变量,如果为true,则同时把读取到的资源存储到三个静态列表中,否则把资源放到非静态列表中,这些非静态列表的作用范围为调用者所在进程。

Resources.loadDrawable()方法代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(mPreloading){
if(isColorDrawable){
sPreloadedColorDrawables.put(key,cs);
} else {
sPreloadedDrawables.put(key,cs);
}
} else {
synchronized(mTmpValue){
if(isColorDrawbale){
mColorDrawableCache.put(key,new WeakReference<ColorDrawable>(cs));
} else {
mDrawableCache.put(key,new WeakReference<Drawable>(cs));
}
}
}

上面所介绍的资源加载仅仅只是加载在res/values/arrays.xml中预先定义的资源值,Framework包含了更多的资源,zygote所加载的仅仅是一小部分。对于那些非”预装载”的系统资源则不会被缓冲到静态列表变量中,这时应用进程如果需要一个非预装载资源则会在各自进程中保持一个资源缓冲。

Git安装教程

Git是用于代码管理的工具,通过仓库(repository)来保存版本管理所需要的信息。Git在每台开发机上都会有一个仓库,代码可以先提交到本地仓库,然后再从本地仓库推送到远端仓库。Git的每次提交(commit)都会生成一个快照,快照保存了所有被修改文件的副本而不是增量。Git每次分支切换都是直接从快照中提取文件,而不是根据增量重新计算出最终文件,因此操作速度会比较。当前最流行的Android源码就是使用Git进行代码管理。

Windows上安装Git

在官网下载Git安装文件。按照默认选项安装即可。安装完成后,在开始菜单里找到“Git”->“Git Bash”,打开Git Bash命令行窗口,说明安装成功。以后所有的Git命令均在这里运行。

MacOS上安装Git

Mac上的安装方法比较多,可以下载dmg安装文件安装。也可以使用homebrew安装。最简单也是推荐的方式是安装 Xcode Command Line Tools。 Mavericks (10.9) 或更高版本的系统中,在 Terminal 里尝试首次运行 git 命令即可。 如果没有安装过命令行开发者工具,将会提示你安装。

Linux上安装Git

对于Ubuntu或者Debian,运行以下命令就可以直接安装,老版本的Ubuntu需要运行sudo apt-get install git-core

1
$ sudo apt-get install git

对于CentOS和Fedora使用以下命令

1
$ sudo yum install git

其他版本的Linux可以下载源码自己编译。

Git配置

打开命令行工具(Windows打开Git Bash),运行以下命令:

1
2
3
4
5
6
$ git config --global user.name  "your name"  
$ git config --global user.email "your email"
$ git config --global push.default simple # 每次push仅push当前分支
$ git config --global core.autocrlf false # 忽略window/unix换行符
$ git config --global gui.encoding utf-8 # 避免乱码
$ git config --global core.quotepath off # 避免git status显示的中文文件名乱码

Windows上还需配置:

1
$ git config --global core.ignorecase false

以上配置适用于全部的Repository,如果某个Repository需要其他的用户名和邮箱,则cd到相应Repository目录执行以下命令:

1
2
git config user.name "your name"
git config user.email "your email"

设置SSH

打开命令行(Windows用户打开Git bash)键入以下命令:

1
$ ssh-keygen -t rsa -C "your email"

然后一路回车,不需要输入任何密码。在当前用户目录的.ssh文件夹下(~/.ssh/id_rsa.pub)会生成id_rsa.pub文件,其内容就是ssh key pair。
对于Linux还需执行以下命令将ssh key告诉系统:

1
$ ssh-add ~/.ssh/id_rsa

将生成的ssh key添加到github(账户创建和配置),git@osc或者任何提供git服务的网站,以后使用git提交到远端服务器就不需要密码了。

参考资料
https://git-scm.com/doc

科学上网的正确姿势

最近各家VPN代理都无法登陆,无奈需要查阅一些资料,所以决定自已动手搭建一台VPN服务器。对比了网上搭建VPN的几种方式,决定使用Ubuntu+Shadowsocks搭建VPN服务器。

不同于传统VPN,Shadowsocks(中文名影梭)工作在应用层,使用自行设计的协议进行加密通信。除创建TCP连接外无需握手,每次请求只转发一个连接,因此使用起来网速较快,在移动设备上也比较省电。影梭的设计目的就是协助用户在严苛的网络环境中突破封锁,所以不容易被墙。而且SS服务器搭建极其简便,客户端软件在MAC,window,Linux,IOS,android上都有支持。

购买国外云主机

这里选择购买阿里云主机,在阿里云官网上购买一台位于国外的ECS服务器,系统选择Ubuntu。并设置密码。

搭建VPN服务器

1. 连接
打开命令行工具,输入以下命令连接远端服务器

1
$ ssh root@你的ip地址

windows用户下载secure shell client,选择quick connect直接输入ip地址,密码连接。

2. 安装

1
2
3
$ apt-get update
$ apt-get install python-pip
$ pip install shadowsocks

3. 配置

1
$ vim /etc/shadowsocks.json

创建配置文件,将如下配置输入到shadowsocks.json中:

{
“server”:”你服务器的ip地址”,
“server_port”:8388,
“local_address”:”127.0.0.1”,
“local_port”:1080,
“password”:”你要设置的登录密码”,
“timeout”:600,
“method”:”aes-256-cfb”,
“fast_open”:false
}

method代表加密方式,推荐使用 AES-256-CFB 或 RC4-MD5
为了支持这些加密方式,我们需要安装:

1
$ apt-get install python-m2crypto

4. 启动
推荐使用后台启动,这样我们的客户端可以随时连接到服务器上
后台启动

1
$ ssserver -c /etc/shadowsocks.json -d start

后台关闭

1
$ ssserver -c /etc/shadowsocks.json -d stop

5. 客户端
下载MacWindowsLinuxAndroid,IOS

6. 客户端配置
客户端配置
只需要填写你服务器的ip地址以及密码,选择加密方式就可以科学上网了。


参考资料
https://github.com/ziggear/shadowsocks
https://zh.wikipedia.org/wiki/Shadowsocks

企业家宣言

我是不会选择做一个普通人的。
如果我能够做到的话,
我有权成为一位不寻常的人。
我寻找机会,但我不寻求安稳,
我不希望在国家的照顾下,
成为一名有保障的国民,
那将被人瞧不起,而使我感到痛苦不堪。
我要做有意义的冒险。
我要梦想,我要创造,
我要失败,我也要成功。
我的天性是挺胸直立,
骄傲而无所畏惧。
我勇敢地面对这个世界,
自豪的说:“在上帝的帮助下,我已经做到了。”

Entrepreneur’s Credo
I do not choose to be a common man,
It is my right to be uncommon … if I can,
I seek opportunity … not security.
I do not wish to be a kept citizen.
Humbled and dulled by having the
State look after me.
I want to take the calculated risk;
To dream and to build.
To fail and to succeed.
I refuse to barter incentive for a dole;
I prefer the challenges of life
To the guaranteed existence;
The thrill of fulfillment
To the stale calm of Utopia.
I will not trade freedom for beneficence
Nor my dignity for a handout
I will never cower before any master
Nor bend to any threat.
It is my heritage to stand erect.
Proud and unafraid;
To think and act for myself,
To enjoy the benefit of my creations
And to face the world boldly and say:
This, with God’s help, I have done
All this is what it means
To be an Entrepreneur.
(Excerpt from Common Sense, written in 1776 by Thomas Paine)