Android的跨进程通讯

Posted by 凯琪的Blog on March 6, 2020

Android中的跨进程通讯

进程与线程的简单区别

线程是CPU调度的最小单元,同时是一种有限的系统资源,进程是一个执行单元。

在电脑和手机中指一个程序应用,但是一个进程能包含多个线程。

打个比方,一个进程相当于一个封闭的盒子,进程则在封闭的盒子里面运行,每个盒子有独立的空间,互不干扰。

1.ContentProvider

系统预置了许多ContentProvider,例如通讯录,要想跨进程获取,操作这些消息,只需要通过ContentResolver的query、update、insert、delete方法就行。

ContentProvider主要以表格的形式来组织数据,并且可以包含多个表

s例子就用A端识别B端登录的用户为例吧。

ContentProvider,提供数据

A端为B端提供的ContentProvider

public class UserInfoContentProvider extends ContentProvider {

    private static final UriMatcher sMATCHER = new UriMatcher(UriMatcher.NO_MATCH);  // Uri匹配器
    public static final int USER_INFO_CODE = 1;// 用户基本信息
    public static final int USER_ACCOUNT_CODE = 2;// 用户账号密码信息,需要rsa加密

    private Context mContext;
    private DataBaseOpenHelper mDBhelper = null;

  //这里是为user_info和user_account表指定了uri并关联Uri_code,分别为1和2
  //db.UserInfoContentProvider/user_info
  //db.UserInfoContentProvider/user_account
    static {
        // 增加俩条匹配规则
        sMATCHER.addURI("UserInfoContentProvider",
                "user_info", USER_INFO_CODE);
        sMATCHER.addURI("UserInfoContentProvider",
                "user_account", USER_ACCOUNT_CODE);
    }

    @Override
    public boolean onCreate() { //该方法是由系统回调运行在主线程中
        mContext = getContext();
        mDBhelper =DataBaseOpenHelper.getInstance(mContext);
        return true;
    }

  //由外界回调运行在Binder线程池中
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        mDBhelper =DataBaseOpenHelper.getInstance(mContext);
        synchronized (mDBhelper) {
            SQLiteDatabase db = mDBhelper.getWritableDatabase();
            String table = getTableName(uri);
            if (table == null) {
                throw new IllegalArgumentException("Unsupported URI: " + uri);
            }
            mContext.getContentResolver().notifyChange(uri, null);
            return db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
        }
    }

	//由外界回调运行在Binder线程池中
  //getType用来返回一个Uri请求所对应的MIME类型(媒体类型)
    @Override
    public String getType(Uri uri) {
        switch (sMATCHER.match(uri)) {
            case USER_INFO_CODE:
               return "vnd.android.cursor.dir/db.UserInfoContentProvider/user_info";
            case USER_ACCOUNT_CODE:
                return "vnd.android.cursor.dir/db.UserInfoContentProvider/user_account";
        }
        return null;
    }

  //由外界回调运行在Binder线程池中
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        mDBhelper =DataBaseOpenHelper.getInstance(mContext);
        synchronized (mDBhelper) {
            SQLiteDatabase db = mDBhelper.getWritableDatabase();
            String table = getTableName(uri);
            if (table == null) {
                throw new IllegalArgumentException("Unsupported URI: " + uri);
            }

            db.insert(table, null, values);
            mContext.getContentResolver().notifyChange(uri, null);

        }
        return uri;
    }

  //由外界回调运行在Binder线程池中
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        mDBhelper =DataBaseOpenHelper.getInstance(mContext);
        synchronized (mDBhelper) {
            int count = -1;
            SQLiteDatabase db = mDBhelper.getWritableDatabase();
            String table = getTableName(uri);
            if (table == null) {
                throw new IllegalArgumentException("Unsupported URI: " + uri);
            }

//            if(!Constant.TABLE_ACCOUNT_INFO.equals(table)) {
            count = db.delete(table, selection, selectionArgs);
            if (count > 0) {
                getContext().getContentResolver().notifyChange(uri, null);
            }
//            }
            return count;
        }
    }

  //由外界回调运行在Binder线程池中
    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        mDBhelper =DataBaseOpenHelper.getInstance(mContext);
        int row = -1;
        synchronized (mDBhelper) {
            SQLiteDatabase db = mDBhelper.getWritableDatabase();
            String table = getTableName(uri);
            if (table == null) {
                throw new IllegalArgumentException("Unsupported URI: " + uri);
            }
//            if(!Constant.TABLE_ACCOUNT_INFO.equals(table)) {
            row = db.update(table, values, selection, selectionArgs);
            if (row > 0) {
                getContext().getContentResolver().notifyChange(uri, null);
            }
//            }
        }
         return row;
    }

  //这里是根据uri的uri_code得到数据表名称,知道外界需要访问哪张表
    private String getTableName(Uri uri) {
        String tableName = null;
        switch (sMATCHER.match(uri)) {
            case USER_INFO_CODE:
                tableName = Constant.TABLE_USERINFO_CACHE;
                break;
            case USER_ACCOUNT_CODE:
                tableName = Constant.TABLE_ACCOUNT_INFO;
                break;
            default:break;
        }

        return tableName;
    }

}

在AndroidManifes.xml文件中注册ContentProvider

 <provider
            android:name=".db.UserInfoContentProvider"
            android:authorities="db.UserInfoContentProvider"
            android:exported="true" />

获取ContentProvider的数据

B端获取A端信息代码:

public class DbDataUtils {
    public final static String TAG = "DbDataUtils";

    public final static int IS_STU = 0;
    public final static int IS_STU_HD = 1;


    public static UserInfoEntity getContentProviderInfo(Context mCtx) {
        //指定uri并指定操作哪个表
        //这串字符对应着注册的ContentProvider的authorities
        Uri uri = Uri.parse("content://db.UserInfoContentProvider/user_info");
        UserInfoEntity infoEntity = getStuCursorInfo(mCtx, uri, IS_STU);//从A获取信息
				.........
        uri = Uri.parse("content://global.provider.EkwContentProvider/userprovider");
        infoEntity = getStuCursorInfo(mCtx, uri, IS_STU_HD); 获取信息

        return infoEntity;
    }

    private static UserInfoEntity getStuCursorInfo(Context mCtx, Uri uri, int isStu){
        ContentResolver cr = mCtx.getContentResolver();
        //执行query方法返回一个结果集
        Cursor cursor = null;
        try {
            cursor = cr.query(uri, null, null, null, null);
        }catch (Exception e) {
            e.printStackTrace();
        }

        if(cursor == null) return null;
        //遍历结果集,取出数据;
        UserInfoEntity infoEntity = new UserInfoEntity();
        if (isStu == IS_STU) {
            while (cursor.moveToNext()) {
                Log.e(TAG, "1==" + cursor.getColumnName(1) + "   2==" + cursor.getColumnName(2) + "   3=="
                        + cursor.getColumnName(3) + "   4==" + cursor.getColumnName(4) + "   5==" + cursor.getColumnName(5) + "   6=="
                        + cursor.getColumnName(6) + "   7==" + cursor.getColumnName(7));
                infoEntity.setUsername(cursor.getString(1));
                infoEntity.setStu_id(cursor.getString(2));
                infoEntity.setSchool(cursor.getString(3));
                infoEntity.setNicename(cursor.getString(4));
                infoEntity.setClasses(cursor.getString(5));
                infoEntity.setAvatar(cursor.getString(6));
                infoEntity.setParentPhone(cursor.getString(7));
            }
        }else {
            while(cursor.moveToNext()){
                infoEntity.setUsername(cursor.getString(cursor.getColumnIndex("username")));
                infoEntity.setStu_id(cursor.getString(cursor.getColumnIndex("stu_id")));
                infoEntity.setSchool(cursor.getString(cursor.getColumnIndex("school")));
                infoEntity.setNicename(cursor.getString(cursor.getColumnIndex("nicename")));
                infoEntity.setClasses(cursor.getString(cursor.getColumnIndex("classes")));
                infoEntity.setAvatar(cursor.getString(cursor.getColumnIndex("avatar")));
                infoEntity.setParentPhone(cursor.getString(cursor.getColumnIndex("parentphone")));
            }
        }
        return infoEntity;
    }

需要注意点⚠️

query、update、insert、delete方法是存在多线程并发访问的,因此方法内部要做好线程同步。

在上面中,A端是只使用了一个SQLiteHelper对象,并且进行同步锁操作,确保了多线程的安全。

适用性:这种方式适用于有数据源,一对多的进程间共享场景下。

2.文件共享

序列化

是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。


两个进程通过读/写同一个文件来交换数据。Android基于Linux,并发读/写文件可以没有限制地进行,如果存在并发写的问题,那么读的数据可能不是最新的数据,从而产生脏数据。

传输的对象需实现序列化(Serializable或者Parcelable)接口。

data class User(
    var userId: Int,
    var userName: String
): Serializable


//把序列化对象数据存储进文件
private fun persistToFile() {
        Thread(Runnable {
            val user = User(1, "hello")
            val dir = File("path")
            if (!dir.exists()) {
                dir.mkdirs()
            }
            val cacheFile = File("cacheFile")
            var objectOutputStream: ObjectOutputStream? = null
            try {
                objectOutputStream = ObjectOutputStream(FileOutputStream(cacheFile))
                objectOutputStream.writeObject(user)
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                objectOutputStream?.close()
            }
        }).start()
    }
    
//从文件中反序列化数据对象
 private fun recoverFromFile() {
        Thread(Runnable { 
            var user: User? = null
            val cacheFile = File("cacheFilePath")
            if (cacheFile.exists()) {
                var objectInputStream: ObjectInputStream? = null
                try {
                    objectInputStream = ObjectInputStream(FileInputStream(cacheFile))
                    user = objectInputStream.readObject() as User
                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    objectInputStream?.close()
                }
            }
        }).start()
    }

像上述所说的,存在并发的问题,文件共享如果想在高并发中使用,显示不太理想,因为线程同步处理耗费时间性能,另外对象的序列化之后的写入文件耗时,IO操作耗时导致线程持有的锁占用时间过长也是一方面的限制。

适用性:所以文件共享这种形式适合在对数据同步要求不高的进程之间进行通信,并且需要妥善处理并发读写的问题,适用于交换简单的数据,没有或者少并发的场景。

3.广播

广播相当于订阅模式,当某个程序发送广播的时候,其他订阅了该应用的广播,那么就能接收到该广播发出的消息。广播发送的时候也能携带数据,通过Bundle进行传递。

典型的应用场景就是Android中的读取验证码了。应用对手机号发送验证码短信,监听系统接收短信的广播,然后系统收到短信后发出广播进行通知,应用接收到广播并对短信内容进行读取判断,提取验证码。

以下为示例代码:

public class SMSBroadcastReceiver extends BroadcastReceiver {
 
	private static final String TAG = "SMSBroadcastReceiver";
 
	private static MessageListener mMessageListener;
 
	public SMSBroadcastReceiver() {
		super();
	}
 
	@Override
	public void onReceive(Context context, Intent intent) {
		Object[] pdus = (Object[]) intent.getExtras().get("pdus");
		for (Object pdu : pdus) {
			SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdu);
			String sender = smsMessage.getDisplayOriginatingAddress();
			String content = smsMessage.getMessageBody();
			long date = smsMessage.getTimestampMillis();
			Date timeDate = new Date(date);
			SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			String time = simpleDateFormat.format(timeDate);
 
			Log.i(TAG, "onReceive: 短信来自:" + sender);
			Log.i(TAG, "onReceive: 短信内容:" + content);
			Log.i(TAG, "onReceive: 短信时间:" + time);
 
			//如果短信号码来自自己的短信网关号码
			if ("your sender number".equals(sender) && mMessageListener != null) {
				Log.i(TAG, "onReceive: 回调");
				mMessageListener.OnReceived(content);
			}
		}
	}
 
	// 回调接口
	public interface MessageListener {
 
		/**
		 * 接收到自己的验证码时回调
		 * @param message 短信内容
		 */
		void OnReceived(String message);
	}
 
	/**
	 * 设置验证码接收监听
	 * @param messageListener 自己验证码的接受监听,接收到自己验证码时回调
	 */
	public void setOnReceivedMessageListener(MessageListener messageListener) {
		this.mMessageListener = messageListener;
	}
}

XML中注册相应广播:

<receiver android:name=".SMSBroadcastReceiver">
            <intent-filter android:priority="1000">
                <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
            </intent-filter>
        </receiver>

广播中携带信息的是Bundle,缺点是只能传输Bundle支持的数据类型(String,Int,Double,序列化的对象数据等),不过这些在日常场景中一般够用。

4.共享内存

共享内存在安卓中也称为匿名共享内存(Ashmem),顾名思义就是开辟创建一块内存供两个进程一起使用,双方在这块内存中进行数据交流。

总体过程就是:客户端通过MemoryFile获取ParcelFileDescriptor,再通过Binder把ParcelFileDescriptor(int类型)传递到服务端。那么这两者是什么呢?

MemoryFile官方解释就是共享内存(ShareMemory)的一个封装,可以理解为共享内存被抽象封装成了一个文件

我们先了解FileDescriptor,它代表了原始的Linux文件描述符,它可以被写入Parcel并在读取时返回一个ParcelFileDescriptor对象用于操作原始的文件描述符。

ParcelFileDescriptor是原始描述符的一个复制:对象和FileDescriptor不同,但是都操作于同一文件流,使用同一个文件位置指针。

所以,总体流程就是:客户端先创建MemoryFile的名字和大小,接着获取它的描述符,再通过Binder传递描述符给服务端,服务端拿到描述符后直接操作MemoryFile。

//客户端实现 
private fun createMemFile() {
        try {
            val mContent = ByteArray(640*480)
          //参数为文件名与文件长度
          //MemoryFile是安卓为匿名共享内存而封装的一个对象
            val mServiceShareMemory =
                MemoryFile("com.yinlib.service" + System.currentTimeMillis(), mContent.size)
            //利用反射得到文件描述符
          val method = MemoryFile::class.java.getDeclaredMethod("getFileDescriptor")
            val fd = method.invoke(mServiceShareMemory) as FileDescriptor
          //把文件描述符序列化,因为通过Binder传输的数据都是要可序列化的。这样子文件描述符就通过Binder传输到服务端
            val mParceServiceShareFile = ParcelFileDescriptor.dup(fd)
            if (mServiceShareMemory != null) {
                mServiceShareMemory.allowPurging(false)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }

//服务端,通过Binder拿到MemoryFile的描述符后进行读取数据
.......
fis = new FileInputStream(FileDescriptor.getFileDescriptor());
fis.read(new byte[100]);

共享内存的会在什么时候应用呢,因为操作都在同一块内存中,减少了中间者,而且交流的数据量也可以很大。

MemoryFile就是在tmpfs文件系统临时创建了一个节点。

我们打开应用,顶部的statusbar或侧边的导航栏,应用的整个界面,这些都是要在APP中draw中执行才能显示出来,那么绘制之后的数据是要经过SurfaceFlinger合成刷新到硬件上进行显示,而我们的应用和SurfaceFlinger是两个不同的进程,这之间传递的数据量也很大,那么这时候就用到匿名共享内存来进行通讯了。靠共享内存的0次Copy,传输数据量大的特点能够快速传递数据。

5.Binder

进程间传递信息就相当于在两个封闭的盒子之间传递信息,没有中间者那肯定是完成不了的。

上述讲到的ContentProvider的原理是Binder 机制(中间者),文件共享的原理是同一个文件(中间者)进行读写操作,广播传递消息通讯也是通过Binder机制,共享内存是通过对共享内存(中间者)进行操作。我们接下来就粗浅得了解一下Binder在跨进程通讯中,了解是如何把数据进行传递的。

从这张图片来看,Binder是ServiceManager连接各种Manager和相应ManagerService的桥梁。

上图中有两个方法,分别是transact和onTransact。

  • transact

    IBinder接口的主要API,使你可以向远端的IBinder对象发送发出调用,也就是通过它进行发送数据。

  • onTransact Binder的方法,使你自己的远程对象能够响应接收到的调用,与transact方法相对应。

    运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交给该方法进行处理,该方法会从data(这里的data实际上已经是一个副对象)中取出目标方法所需的参数并执行,执行完毕之后向reply中写入返回值。

这只是一个很简单的过程图,实际上整个调用链很复杂,这里就不一一列出来。

总结

IPC过程中,contentprovider、广播、匿名共享内存这些方式涉及到Binder,通过序列化(共享文件也用到)使数据变成字节描述,永久保存,从而有利于Binder这个中间者进行传输,完成跨进程通讯;通过Binder进行传输的数据要进行一次对象的copy,而通过共享内存的形式则不需要进行copy。

other

进程间通讯当然不止这么点,这里我只是简单介绍了Android中的进程通讯的方式,类似的还有Linux的管道通讯、消息队列、信号,Socket套接字等等。虽然平时不太可能用到,但是了解这背后的原理,也会加深我们对一些系统设计处理问题的思考。