Android里的多进程和跨进程通讯方式

Posted 小陈乱敲代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android里的多进程和跨进程通讯方式相关的知识,希望对你有一定的参考价值。

IPC简介

进程间通信(InterProcess Communication缩写IPC)是指在不同进程之间传播或交换信息。进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元。

IPC不是android中所独有的,任何一个操作系统都需要有相应的IPC机制。在Android系统中一个进程会对应一个虚拟机实例,不同的虚拟机在内存分配上有不同的地址空间,所以:只有在多进程的环境下才需要考虑使用IPC进行通讯。Android中的应用程序可以为一个进程,也可以配置成多进程,每个进程都在自己独立的空间中运行

在Android中有两个典型的场景:第一种是应用自身需要开启多个进程,比如多模块应用,由于系统设置了应用获取的最大内存限制,为了获得更多的内存空间将不同的模块放在不同的线程中。另外一种情况就是获取其他应用里的数据,典型的案例就是获取通讯录和短信。

Android中的多进程模式

Android默认是运行在默认名为包名的进程中,除非特别指定,所有的组件都运行在默认进程中。可以通过修改AndroidManifest文件,在 application 标签下添加 android:process 属性可以修改Android默认的进程名字:

<application
    android:name=".MainApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:process="com.aa.bbb"
    android:theme="@style/Theme.Sunflower">
   
</application> 

在Android中只有给四大组件(Activity Service Broadcast ContentProvider)设置android:process属性这一种使用多进程的方法。

<service
    android:name=".messenger.MessengerService"
    android:process=":services" /> 

上面的代码为MessengerService指定了process属性,为应用增加了一个新的进程。当MessengerService 启动时,系统会为它创建单独的进程。使用:adb shell ps | grep [包名]查看进程信息:

注意上面中的XML代码,process 属性有两种设置方式,一种是如上文中":"开头后面接进程名,一种则是完整的进程名,例如android:process="com.packages.name.services"。前者会在进程名前加上包名(“com.packages.name:services”),且规定了进程为当前应用的私有进程,其他应用的组件不可以和它使用统一进程。后者则你写了什么,进程名就是什么,且为全局进程,其他应用也可以使用该进程。

虽然开启多进程的方法很简单,但是这种看似简单的操作稍有不深就会带来巨大的问题:

首先,多进程会造成Application的多次创建:当一个组件需要运行在新的进程中时,实际的创建过程就相当于又重新启动了一次应用,应用启动,必然会创建Application。而运行在不同进程中的组件,不仅属于不同的虚拟机,而且其Application也是不同的。

其次,多进程会导致静态成员和单例完全无效:由于不同进程都被分配了独立且不同的虚拟机,其在内存分配上有这不同的地址空间。这就会导致在着一个类的多个副本,各自修改互不影响。

再次,多进程模式下,线程的同步机制也会失效:因为不同的进程,其线程不所属同一内存,那么无论是对象锁还是类锁,锁本身都不是同一个了。

最后,多进程模式下SharedPreferences风险会增大:SharedPreferences底层是通过文件读写实现的,并发操作可能会造成问题。

总之:在不同进程中的组件,如果使用内存来通讯,都会存在隐患或者直接失败。这就是多进程带来的最主要的影响。

Android中的跨进程通讯(IPC)

上文对Android中的多进程做了一些介绍,接下来讲解一下多进程间通讯的方式:

利用Bundle

在Android开发中,我们通过Intent启动Activity、Service和Receiver都是可以通过Bundle传递参数的。它实现了Parcelable接口,并以键值对的方式保存数据。可以将其视为一个容器,其支持基本数据类型(string、int、boolean、byte、float、long、double)以及它们对应的数据。当需要传递对象或对象数组时,被传递的对象必须实现Serialiable或Parcelable接口。

接下来我们通过一个Activity(MainMessengerActivity)和一个Service(MessengerService)来看一下如何利用Bundle实现跨进程通讯。

首先,让二者运行在不同的进程中:

<activity 
    android:name=".messenger.MainMessengerActivity"/>
<service
    android:name=".messenger.MessengerService"
    android:process=":services" /> 

在配置文件指定Service的运行线程即可。

在通过 bindService(Intent, ServiceConnection,int)MainMessengerActivity绑定服务时,为intent添加bundle:

val intent = Intent(this, MessengerService::class.java)
val bundle = Bundle()
bundle.putString("name", "Butler")
bundle.putInt("age", 28)
intent.putExtra("message", bundle)
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE) 

在service中的onBind方法接受数据:

override fun onBind(intent: Intent?): IBinder? 
    intent?.getBundleExtra("message")?.apply 
        Log.e("Name is:", "$this.getString("name")")
        Log.e("age is:", "$this.getInt("age") ")
    
    return mMessenger.binder
 

运行结果如下:

使用方法很简单,跟我们平时使用Bundle没有什么区别。

使用文件共享

通过共享文件,不同的进程可以使用读写文件的方式交换数据。在Android中,对文件的并发读写并没有什么特殊的要求。虽然这可能造成问题,但是却依然可以帮我们在不同进程间传递数据。我们创建一个新的Activty(FileActivity),并指定其进程android:process=":file"

MainMessengerActivity中写文件并实现跳转:

findViewById<Button>(R.id.file).setOnClickListener 
    val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    coroutineScope.launch 
        val message = "name:Butler,age:28"
        try 
            val fileOutputStream = openFileOutput(FileName, MODE_PRIVATE)
            fileOutputStream.write(message.toByteArray())
            fileOutputStream.close()
         catch (e: Exception) 
            print(e.message)
         finally 
            startActivity(Intent(this@MainMessengerActivity, FileActivity::class.java))
        
    
 

在FileActivity读取文件内容:

coroutineScope.launch 
    try 
        val fileInputStream = openFileInput(FileName)
        var n = 0
        val sBuffer = StringBuffer()
        while (n != -1) 
            n = fileInputStream.read()
            val by = n.toChar()
            sBuffer.append(by)
        
        Log.e("message:","$sBuffer")
     catch (e:Exception)
        print(e.message)
     finally 

    
 

运行结果为:E/message:: name:Butler,age:28

注意,不要使用SharedPreferences去做跨进程通讯,原则上它不支持多进程。虽然它本质上也是一个文件,但是由于它在应用运行时会再内存中加载缓存,然而进程间是不能内存共享的,每个进程操作的SharedPreferences都会是一个单独的实例,这就会导致安全性问题,这个问题只能通过多进程间其它的通信方式或者是在确保不会同时操作SharedPreferences数据的前提下使用SharedPreferences来解决。

使用ContentProvider

ContentProvider提供一种应用管理其自身和其他应用所存储数据的能力,并提供与其他应用共享这些数据的方法。它会封装数据,并提供用于定义数据安全性的机制。无论你是否需要和其他应用分享数据,你都可以使用ContentProvider去访问这些数据,虽然当无需和其他应用共享数据时没必要使用它。系统预置了许多ContentProvider,比如通话记录,通讯录,信息等,只需要通过ContentProvider就可以拿到这些信息。ContentProvider以一个或多个表的形式将数据呈现给外部应用,这些表与关系型数据库中的表类似。行表示提供程序收集的某种类型数据的实例,行中的每一列表示为一个实例所收集的单个数据。

通常有两个典型的使用场景:一种是通过实现代码访问其他应用中的现有的ContentProvider用来获得数据;另一种是创建新的ContentProvider,与其他应用共享数据。

下面我们用一个最简单的例子,演示一下使用它进行跨进程通讯。为了方便演示,我们在同一个应用内进行:

首先创建一个ContentProvider

class UserProvider : ContentProvider() 

    private lateinit var appDatabase: AppDatabase
    private var userDao: UserDao? = null

    override fun onCreate(): Boolean 
        appDatabase =
            Room.databaseBuilder(context!!, AppDatabase::class.java, "database-provider").build()
        userDao = appDatabase.userDao()
        return true
    

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? 

        return userDao?.getAll()
    

    override fun getType(uri: Uri): String? 

        return ""

    

    override fun insert(uri: Uri, values: ContentValues?): Uri? 
        val user = User(
            System.currentTimeMillis().toInt(),
            values?.getAsString("name"),
            values?.getAsInteger("age")
        )
        Log.e("-------$user.firstName", "------------$user.age")
        userDao?.insertAll(user)
        return uri
    

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int 
        //不展示
        return 0
    

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int 
        //不展示
        return 0
    
 

代码很简单,就是一个使用Room的数据持久化,只做了insert和查询的逻辑处理,接下来在配置文件里配置它,并知名其所在的进程。

<provider
    android:name=".provider.UserProvider"
    android:authorities="com.karl.butler.provider"
    android:permission="com.karl.PROVIDER"
    android:process=":provider" /> 

然后创建Activity,并让它运行在应用的默认进程里面

class ProviderActivity : AppCompatActivity() 
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_provider)
        val textName = findViewById<EditText>(R.id.et_name)
        val textAge = findViewById<EditText>(R.id.et_age)
        val textView = findViewById<TextView>(R.id.tv)
        findViewById<Button>(R.id.btn_save).setOnClickListener()
            if (textName.text != null && textAge.text != null) 
                val uri = Uri.parse("content://com.karl.butler.provider")
                val contentValue = ContentValues()
                contentValue.put("name", "$textName.text")
                contentValue.put("age", textAge.text.toString().toInt())
                contentResolver.insert(uri, contentValue)
            
        
        findViewById<Button>(R.id.btn_find).setOnClickListener()
            textView.text = ""
            val uri = Uri.parse("content://com.karl.butler.provider")

            val query = contentResolver.query(uri, null, null, null)
            var text = ""
            while (query?.moveToNext()!!)
                text += "姓名:$query.getString(1)  年龄:$query.getInt(2)\\n"
            
            textView.text = text

        
    
 

代码很简单:两个输入框分别输入姓名和年龄,一个保存按钮一个查询按钮以及一个展示所有User的Text。运行效果如下:

可以看到,处于两个不同进程中的Activity和Provider成功的实现了数据通讯。但是,由于设计原因,ContentProvider只支持增删改查,更像是一个跨进程的数据库。

使用Messenger

Messenger是跨进程通信的类,通过它可以再不同进程中传递Message。底层通过AIDL实现,可以理解为是官方为了怕我们使用AIDL太过于繁琐而提供的一种简单的方案。

它是使用也很简单,首先创建服务端ServiceMessengerService,并设置它所在的进程android:process=":services"

class MessengerService : Service() 

   private val mHandlerThread: HandlerThread = HandlerThread("服务端")
   private lateinit var mMessenger: Messenger
   private lateinit var mHandler: Handler

   override fun onCreate() 
       super.onCreate()
       mHandlerThread.start()
       mHandler = object : Handler(mHandlerThread.looper) 
           override fun handleMessage(msg: Message) 
               if (msg.what == 0) 
                   val obtain = Message.obtain(msg)
                   obtain.what  = 1
                   obtain.arg2  = 2*obtain.arg1
                   Thread.sleep(2000L)
                   obtain.replyTo.send(obtain)
                   return
               
               super.handleMessage(msg)
           
       
       mMessenger = Messenger(mHandler)
   
 

代码很简单,接受客户端传来的Int值,延迟两秒后并返回它的倍数。

接下来在应用默认进程中实现一下客户端Activity

class MainMessengerActivity : AppCompatActivity() 
   

    private lateinit var mServiceMessenger: Messenger
    private val mServiceConnection = object : ServiceConnection 
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) 
            mServiceMessenger = Messenger(service)
        

        override fun onServiceDisconnected(name: ComponentName?) 

        
    

    private val handler: Handler = object : Handler(Looper.getMainLooper()) 
        override fun handleMessage(msg: Message) 
            if (msg.what == 1) 
                Log.e("$currentTime()--客户端收到的消息:", "$msg.arg2")
            
            super.handleMessage(msg)
        
    

    private val mClientMessenger: Messenger = Messenger(handler)

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        val intent = Intent(this, MessengerService::class.java)
        val bundle = Bundle()
        bundle.putString("name", "Butler")
        bundle.putInt("age", 28)
        intent.putExtra("message", bundle)
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
        setContentView(R.layout.activity_main_messenger)
        findViewById<Button>(R.id.Messenger).setOnClickListener 
            val nextInt = Random().nextInt(100)
            Log.e("$currentTime()--客户端发送的消息:", "$nextInt")
            val message = Message.obtain(handler, 0, nextInt, 0)
            message.replyTo = mClientMessenger
            mServiceMessenger.send(message)
        

        findViewById<Button>(R.id.file).setOnClickListener 
            val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
            coroutineScope.launch 
                val message = "name:Butler,age:28"
                try 
                    val fileOutputStream = openFileOutput(FileName, MODE_PRIVATE)
                    fileOutputStream.write(message.toByteArray())
                    fileOutputStream.close()
                 catch (e: Exception) 
                    print(e.message)
                 finally 
                    startActivity(Intent(this@MainMessengerActivity, FileActivity::class.java))
                
            
        
    


    override fun onDestroy() 
        unbindService(mServiceConnection)
        super.onDestroy()
    

    fun currentTime(): String 
        val formatter = SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss")
        return formatter.format(Date(System.currentTimeMillis()))
    
 

代码很简单,Activity启动时和MessengerService绑定,点击按钮向不同进程的服务发送数字并接受新的返回值,运行效果如下:

Messenger通过Handler将Message发送到另一个进程,实现了进程间通信,底层依然是使用了Binder机制,其本质上也是基于AIDL实现的。

Messenger的AIDL文件(IMessenger)内容如下:

package android.os;

import android.os.Message;

/** @hide */

oneway interface IMessenger 

void send(in Message msg);

 

不难发现,它的作用就是跨进程发送Message。它的大致流程如下:

但是,Messenger也有明显的缺点:首先,服务端是以串行的方式在处理消息,不太适合用来出来大量的并发请求。其次,它的主要作用是传递消息,不太适合用来跨进程调用方法。

使用AIDL(Messenger原理)

上一小节我们介绍了Messenger实现跨进程通讯的方法,其本质上也是基于AIDL实现的,这一节我们使用AIDL自己实现一个Messenger,用来演示AIDL的使用以及穿插者学习Messenger的工作原理,为了简化代码。我们并不直接使用Message作为我们传递的数据,而是使用一个Bean作为传递的数据。其核心代码如下:

package com.karl.butler.aidl

import android.os.Parcel
import android.os.Parcelable

class Bean: Parcelable 
    var name: String? = null
    var age = 0

    constructor(parcel: Parcel)  
        name = parcel.readString()
        age = parcel.readInt()
    

 

注意:在AIDL中,并不是所有的数据类型都是可以使用,它支持一下数据类型:

  • 基本数据类型;
  • ArrayList,且里面的元素必须是被AIDL支持的数据类型;
  • HashMap,且里面的元素必须是被AIDL支持的数据类型;
  • 实现了Parcelable接口的对象;
  • AIDL接口本身。

所以我们的Bean类要实现Parcelable接口。同理,上节中Messenger中传递的Message也是实现了Parcelable接口:

接下创建AIDL文件Android跨进程通信

android中的跨进程是啥意思

Android 进程间通信的几种实现方式

Android 进程间通信的几种实现方式

android 史上最简单易懂的跨进程通讯(Messenger)!

Android Studio创建AIDL文件并实现进程间通讯