多进程使用及其带来的问题

Posted by pxfile Blog on June 22, 2018

Android多进程使用及其带来的问题

1.Android程序如何使用多进程

根据Processes and Threads,默认情况下,一个Android程序的所有组件都是在一个进程中执行的,该进程的名字就是程序的包名。同时,Android也允许开发者将程序的一些组件在其他进程中执行。四大组件均支持android:process属性,我们可以设置该属性的名字,将组件运行在指定的进程。参考以下的例子,

        <activity
            android:name=".SecActivity"
            android:label="@string/title_activity_sec"
            android:process=":secActivity" >
        </activity>

在该例子中,我们将SecActivity的android:process属性设置为”:secActivity”(暂时忽略“:”的意思,稍后介绍),启动程序,打开SecActivity,然后进入adb shell,执行ps命令,查看当前正在运行的进程信息,

这里写图片描述

由上图可以看到,的确由两个进程在运行,其中一个的名字是paul.example.mutliprocessdemo,这正是我们的包名,也就是说,这个就是我们的主进程。另一个的名字是paul.example.mutliprocessdemo:secActivity,这是我们在Manifest文件中设置”:secActivity”的结果。

接下来来介绍“:”的含义,如果一个组件指定的进程名是以“:”开头的,就意味着该进程是一个私有进程。那么何为私有进程?私有进程是和全局进程对应,如果我们在android:process中填写的是完整的包名,那么该进程就是一个全局进程。全局进程允许两个不同的应用(或组件)运行在同一个进程中(使用ShareUID,且签名完全一致);私有进程则不可以。

另外,Application也可以设置android:process属性,来修改所有组件运行进程的名称。

2. 为什么使用多进程

为什么Android应用要使用多进程?有以下理由,欢迎各位读者补充~ 1. 更大的内存分配,我们知道,Android设备限制了每一个进程所分配的内存大小,如果这个大小是32M,那么如果我们的应用有两个进程,那么所能使用的最大内存就是32*2=64M。所以如果用户经常收到OutOfMemory异常,那么就应该考虑使用多进程。

2. 防止进程被杀死。考虑这样一种情况,一个后台进程在播放音乐,此时,内存吃紧,系统需要释放更多的内存,此时系统就会优先把UI进程给杀掉,而不是播放音乐的进程。还有另外一个应用场景,我们可以使用守护进程来保护我们的主进程不被杀死。具体的做法是主进程和守护进程相互监护,若对方被杀死,则重启。

3. 分担主进程的内存压力。现在的应用越来越大,我们可以将一些其他的工作放在额外的进程中运行,来降低主进程的压力。

3. Android多进程会带来什么问题

坏处:

  • 消耗用户的电量。

  • 多占用了系统的空间,若所有应用都这样占用,系统内存很容易占满而导致卡顿。

  • 应用程序架构会变得复杂,因为要处理多进程之间的通信。这里又是另外一个问题了。

我们知道,在Android设备上,一个进程对应一个虚拟机实例。而不同的虚拟机之间是相互隔离的,也就意味着,我们不能使用常规方法来共享数据。

但是,这并不意味着两个进程之间无法通信。事实上,有一些方法可以实现进程间通信。比如,Intent,Handler,Messenger,AIDL或者Binder。

另外,多进程还会带来以下问题,

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

原因

(不是同一块内存,会产生不同的副本)

因为进程间,内存空间是相互独立的,所以VM方法区内的静态变量也都是相互独立的。因为单利模式是基于静态变量的,所以单例也会失效。在两个不同进程访问一个相同类的静态变量,所得的值未必相同,所以在开发中请避免此类代码。

解决方法

使用Intent或者aidl等进程通讯方式传递内容,不能用静态或单例模式。

线程同步机制完全失效

原因

(不是同一块内存,所以对象也不是同一个,因此类锁、对象锁也不是同一个,不能保证线程同步)

由于Java的同步机制是VM来进行调度的,两个进程拥有两个不同的VM,所以,同步也会在多进程开发中失效。synchronized关键字、voliate关键字等都是基于VM级别的同步,所以请不要跨进程去使用线程同步。

解决方法

比如主进程有个生产者,子进程的消费者是无法正常使用消费功能的,只能通过跨进程通信,让主进程的消费者去消费,然后再回调。

SharedPreferences 可靠性下降

原因

(SharedPreferences不支持多个进程同时写,会有一定的几率丢失数据)

这里的文件指的泛指所有需要并发访问的文件,例如:本地文件,数据库文件,sharepreference等。由于Java中,文件锁、队列机制都是VM级别的,所以不同进程访问同一个文件锁是不管用的。(通过C++可以实现多进程文件锁机制,不过不在文本讨论范围内。)所以在实际开发过程中,还是避免多进程同时访问统一文件,多利用Android中IPC的C/S思想,提供服务,接口调用,避免直接去访问对方进程的文件或者数据库,提升设计美感,同时也能提升代码的稳定性。

解决方法

多进程情况下会出现两个进程在同一时刻访问同一个数据库文件的情况。这就可能造成资源的竞争访问,导致诸如数据库损坏、数据丢失等。在多线程的情况下我们有锁机制控制资源的共享,但是在多进程中比较难,虽然有文件锁、排队等机制,但是在Android里很难实现。解决办法就是多进程的时候不并发访问同一个文件,比如子进程涉及到操作数据库,就可以考虑调用主进程进行数据库的操作。

Application 多次创建

原因

(Android为每个进程分配独立的虚拟机,这个过程其实就是启动一个应用,所以Application会被创建多次),所以我们不能直接将一些数据保存在Application中。

解决方法

在Application的onCreate中获取进程Id来判断不同进程,然后做不同的事情。

public class MyApplication extends Application {

	@Override
	
	public void onCreate() {
	
		super.onCreate();
		
		//获取进程Id
		
		int pid = android.os.Process.myPid();
		
		Log.e("m_tag", "MyApplication onCreate pid is " + pid); //根据进程id获取进程名称
		
		String pName = getProcessName(this,pid);
		
		if("com.xyy.processtest".equals(pName)){
			//处理该进程的业务
			...
				}
	}
}

public String getProcessName(Context cxt, int pid) {

	ActivityManager am = (ActivityManager)
	
	cxt.getSystemService(Context.ACTIVITY_SERVICE);
	
	List<RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
	if (runningApps == null) {
		return null;
	}

for (RunningAppProcessInfo procInfo : runningApps) {
			if (procInfo.pid == pid) {
				return procInfo.processName;
			}
		}
	return null;
}

断点调试

原因

调试就是跟踪程序运行过程中的堆栈信息,正如前面所讲,每个进程都有自己独立的资源和内存空间,每个进程的堆栈信息也是独立的,如果要在不同的进程间调试,是实现不了的。

解决方法

不过可以通过如下两种方式进行调试:

  • 调试的时候去掉AndroidManifest.xml文件中Activity的android:process标签,这样保证调试状态下是在同一进程中,堆栈信息是连贯的,在调试完成后记得复原该属性;

  • 通过打印进行调试,但这种效率比较低。

Activity管理:

原因

通常我们为了完全退出一个应用,会在Application里面实现ActivityLifecycleCallbacks接口,监听Activity的生命周期,通过LinkedList来管理所有的Activity:

是如果应用内有多个进程,每创建一个进程就会跑一次Application的onCreate方法,每个进程内存都是独立的,所以通过这种方式无法实现将应用的Activity放在同一个LinkedList中,不能实现完全退出一个应用。