IPC 机制(一)之 Android 中的多进程模式

在正式介绍进程间通信之前,我们必须先要理解 Android 中的多进程模式。通过给四大组件指定 android:process 属性,我们就可以轻松地开启多进程模式,这看起来很简单,但是实际使用过程中却暗藏杀机,

1.开启多进程模式

正常情况下,在 Android 中多进程是指在一个应用中存在多个进程的情况,因此这里就不讨论两个应用之间的多进程情况了。首先,在 Android 中使用多进程只有一种方法,那就是给四大组件在 AndroidMenifest 中指定 android:process 属性,除此之外没有其他的办法,也就是说我们无法给一个线程或者一个实体类指定其所运行的进程。其实还有一种非常规的方法,那就是通过 JNI 在 native 层去 fork 一个新进程,但是这种方法属于特殊情况,也不是常用的创建多进程的方法,因此战术不考虑这种情况。下面一个实例,描述了如何在 Android 中创建多进程:

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".SecondActivity"
            android:process=":remote"/>
        <activity
            android:name=".ThirdActivity"
            android:process="com.demo.text.demotext.remote" />

上述代码分别为 SecondActivity 和 ThirdActivity 制定了 process 属性,并且他们的属性值不同,这意味着当前应用又增加了两个进程,当前应用包名为 “com.demo.text.demotext”,当 SecondActivity 启动时,系统为为它创建一个单独的进程,进程名为“com.demo.text.demotext:remote”;当 ThirdActivity 启动时,系统也会为它创建一个单独的进程,进程名为 “com.demo.text.demotext.remote”。而 MainActivity 没有为它指定 process 属性,那么它运行在主线程也就是 UI 线程中,进程名为包名。下面我们来看下效果:

除了上图,我们还可以使用 adb 命令来查看,命令为:adb shell ps 或者 adb shell "ps | grep pachageName"。运行结果如下图:

通过上边的代码,我们发现,SecondActivity 和 ThirdActivity 的 process 属性分别为“:remote” 和 “com.demo.text.demotext.remote”。这两种方式有什么区别呢?其实是有区别的,区别在两个方面:

(1)“:” 的含义是在当前的进程名前面附加上当前的包名,这是一种简写方法,对于 SecondActivity 来说,它完整的进程名是 “com.demo.text.demotext:remote”,这一点从上边的进程查询中能看出,而对于 ThirdActivity 中的命名方式,它是一种完整的命名方式,不会附包名信息;

(2)进程名以 “:” 开头的进程属于当前应用的私有进程,其他应用的组件和进程不可以和它跑在同一个进程中,而进程名不以 “:” 开头的进程属于全局进程,其他应用可以通过 SharedUID 方式可以和它跑在同一个进程中

我们知道 Android 系统会为每一个应用分配一个 UID,具有相同 UID 的应用才能共享数据,这里要说明的是,两个应用通过 SharedUID 跑在同一个进程中是有要求的,需要这两个应用具有相同的 SharedUID 并且签名相同才对。在这种情况下,它们可以互相访问对方的私有数据,比如 data 目录、组件信息等,不管它们是否在同一个进程。当然如果它们跑在同一个进程中,需要保证 process 属性相同,它们还可以共享内存数据,或者说它们看起来就像一个应用的两个部分。

2.多进程模式的运行机制

如果用一句话来形容多进程,那就是“当应用开启了多进程,各种奇怪的现象都出现了”。为什么这么说呢?这是有原因的。大部分人认为开启多进程是很简单的事情,只要给四大组件设置 process 属性即可。比如在实际的产品开发中,可能会有多进程的需求,需要把某些组件放在单独的进程中去执行,很多人都觉得这还不简单吗?然后迅速的给那些组件制定了 process 属性,然后编程之后,发现 “真的运行起来了”。但是你确定他真的运行起来了吗?现在先不置可否,下面举个例子,还是之前的那个例子,我们新建一个类,叫 UserMangeer,这个类有一个静态成员变量,代码如下:

public class UserManageer {

    public static int userId = 1;
}

然后在 MainActivity 的 onCreate 中 我们把 userId 重新复制为 2,打印出这个静态变量的值后再启动 SecondActivity,在 SecondActivity 中我们打印一下 userId 的值。按正常的逻辑,静态变量是可以在所有的地方共享的,并且一处有修改,处处都有同步,打印日志如下:

com.demo.text.demotext I/com.demo.text.demotext.MainActivity: 2
com.demo.text.demotext:remote I/com.demo.text.demotext.SecondActivity: 1

上述问题出现的原因是 SecondActivity 运行在一个单独的进程里,我们知道 Android 为每一个进程分配了一个独立的虚拟机,不同的虚拟机在内存上有不同的地址空间,这就导致了不同的虚拟机中访问同一个类的对象会产生多份副本。拿上边的例子来说,在进程 com.demo.text.demotext 和进程 com.demo.text.demotext:remote 中都存在一个 UserManager 类,并且这两个类互不干扰,在一个进程中修改 userId 的值,只会影响当前进程,对其他进程不会造成影响,这样我们就可以理解上述现象了。

所以在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。正常情况下,四大组件中间不可能不通过一些中间层来共享数据,那么通过简单的指定进程名来开启多进程都会无法正确运行。当然,特殊情况下,某些组件之间不需要共享数据,这个时候可以直接指定 process 属性来开启多进程,但是这种情况不常见的,几乎所有情况都需要共享数据。

一般来说,使用多进程会造成如下几方面的问题:

(1)静态成员和单例模式完全失效。

(2)线程同步机制完全失效。

(3)SharedPreferences 的可靠性下降。

(4)Application 多次创建。

第一个问题在上边已经进行了分析,第二个问题本质上和第一个问题是类似的,既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证县城同步,因为不同进程锁的不是同一个对象。第三个问题是因为 SharedPreferences 不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失。这是因为 SharedPreferences 底层是通过读/写 XML 文件来实现的,并发写显然是可能出现问题,甚至并发读写都可能出现问题。第四个问题,也是显而易见的,当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的 Application。这个问题可以这么理解,运行在同一个进程中的组件是属于同一个虚拟机和同一个 Application 的,同理,运行在不同进程中的组件是属于两个不同虚拟机 和 Application 的。为了更加清晰的展示这一点,下面我们来做一个测试,首先在 Application 的 onCreate 方法中打印当前进程的名字,然后连续启动三个同一个应用内但不同进程的 Activity,按照期望,Application 的 onCreate 应该启动三次并打印三次进程名不同的 log,代码如下:

public class BaseApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("BaseApplication", "application start, processName:" + getAppNameByPID(getApplicationContext(), Process.myPid()));
    }

    public String getAppNameByPID(Context context, int pid) {
        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
            if (processInfo.pid == pid) {
                return processInfo.processName;
            }
        }
        return "";
    }
}

看一下运行 log,如下图,通过 log 可以看出, Application 执行了三次 onCreate,并且每次的进程名和进程 id 都不一样,它们的进程名和我们为 Activity 指定的 android:process 属性一直,这也就证实了在多进程模式中,不同的组件的确会拥有独立的虚拟机、Application 以及存储空间,这会给实际的开发带来很多困扰,是尤其需要注意的。或者我们也可以理解同一个应用的多进程,它们相当于两个不用的应用采用了 SharedUID 的模式,这样就更加直接的理解多进程模式的本质了。

com.demo.text.demotext I/BaseApplication: application start, processName:com.demo.text.demotext  PID:16495
com.demo.text.demotext:remote I/BaseApplication: application start, processName:com.demo.text.demotext:remote  PID:16719
com.demo.text.demotext.remote I/BaseApplication: application start, processName:com.demo.text.demotext.remote  PID:16981

上边我们分析了多进程带来的问题,但是我们不能因为多进程有很多问题就不去正视它。为了解决这个问题,系统提供了很多跨进程通信方法,索然说不能直接的共享内存,但是通过跨进程通信我们还是可以实现数据交互。实现跨进程通信的方法很多,比如通过 Intent 来传递数据,共享文件和 SharedPreferences,基于 Binder 的 Messager 和 AIDL 以及 Socket 等,但是为了更好的理解各种 IPC 方式,我们需要先熟悉一下基本概念,比如序列化相关的 Serializable 和 Parcelable 接口,以及 Binder 的概念,熟悉玩这些概念之后,再去理解各种 IPC 方式就简单多了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值