第二章 IPC机制详解(1)

本文为Android开发艺术探索的笔记,仅供学习

第二章知识点大纲

1 IPC 含义就是进程间通信或者跨进程通信

线程是CPU调度的最小单位,同时线程也是一种有限的系统资源。
进程一般指的是一个执行单元,在PC和移动设备上指一个程序或者是一个应用。

一个进程可以有很多个线程,但只有一个线程的时候即为主线程,在android里也称为UI线程。UI线程里才能去操作界面元素。很多时候,一个进程需要执行大量耗时任务,如果这些任务放在主线程里,就会造ANR(应用程序无响应)。解决这个问题就需要用到线程,需要建立子线程通过Handle去操作些耗时操作或更新UI。

2 多进程模式分析

2.1 android中的多进程模式

想要使用多进程,只需在四大组件的XML文件里使用android:process的属性即可。

<activity
            android:name=".MainActivity"
            android:configChanges="orientation|screenSize"
            android:label="@string/app_name"
            android:launchMode="standard" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
            </intent-filter>
        </activity>
        <activity
            android:name=".SecondActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process=":remote" />
        <activity
            android:name=".ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process="com.ryg.chapter_2.remote" />

让我们来看看这三个Activity分别对应的进程是什么


可以看到有三个进程,分别表示在xml里声明的三个Activity,一次对应MainActivity,SecondActivity,ThirdActivity
SecondActivity的进程名字是com.ryg.chapter_2:remote ,:的前面部分是当前进程是在的包名。
ThirdActivity所对应的进程com.ryg.chapter_2 .remote ,这个命名方式是完整的命名方式,不会附加包名。

开头的进程是私有进程,其他应用组件是不会和它跑在同一个进程里的,而不是以:开头的则是全局进程其他应用可以通过ShareUID的方式,可以跑在同一个进程里。

注:Android的系统会给每一个应用分配一个唯一的UID,具有相同的UID才能共享数据。若两个相同的UID想要跑在同一个进程里是有要求的,必须要有相同签名可以。这种情况下,他们可以互相访问私有数据,还可以共享数据。

2.2 多进程模式的运行机制

首先举个例子好让大家理解,在上面的基础上,我们再添加一个类,并且声明一个全局变量如下图
public class UserManager{ public int sUserId = 1;}

那么我们需要在MainActivity中去修改sUserId ,把值改为2 并且打印出来,在SecondActivity将sUserId打印出来,结果


按照常理来说SecondActivity应该输出2,因为MainActivity已经把值改为2了,而事实却是相反的,因为Android中每一个进程,都会为其分配一个DVM(虚拟机),所以每一个进程都是相对独立的,而MainActivity修改的值,仅对同一进程有效。SecondActivity和MainActivity并不在同一个进程中,所以MainActivity修改的值SecondActivity会接收不到。

所以这就是多进程带来的主要影响有以下几点

  1. 静态成员和单例模式完全失效
  2. 线程同步机制完全失效
  3. SharePreferences的可靠性降低
  4. Application会被多次创建

第一个影响就是上面的例子,第二个影响也一样,既让都不在同一个虚拟机中,不再同一个进程中线程又怎么去同步呢。第三个影响就是Sharepreferences本生不支持两个进程同事去写,这样有一点的几率会造成数据的丢失。第四个影响每一个应用对于一个进程,对于一个虚拟机,对于一个Application,所以开启多个进程会开启多个虚拟机开启多个Application 是一样的道理。

3 IPC的基础概念

主要包括三个,serializable接口,parcelable接口以及binder

3.1 serializable的接口

Serializable 是java提供的一个序列化的接口,就是对对象进行序列化和反序列化的操作。首先想要实现序列化的操作需要让类去实现Serialezable接口并声明一个serialVersionUID即可,事实上serialVersionUID并不是必须的,不声明serialVersionUID也可以实现序列化。

public class User implements Serializable {
        private static final long serialVersionUID = 519067123721295773L;
        public int UserId;
        public String username;
        public boolean isMale;
    }

就完成对象的序列化操作,几乎所有的工作都让系统去自动完成。
如何对对象进行序列化和反序列化的操作,只需采用ObjectInputStream和ObjectOutputStream

//序列化的过程
               String CHAPTER_2_PATH = Environment.getExternalStorageDirectory().getPath()
            + "/singwhatiwanna/chapter_2/";
               String CACHE_FILE_PATH = CHAPTER_2_PATH + "usercache";

                User user = new User(1, "hello world", false);
                File dir = new File(MyConstants.CHAPTER_2_PATH);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                ObjectOutputStream objectOutputStream = null;
                try {
                    objectOutputStream = new ObjectOutputStream(
                            new FileOutputStream(cachedFile));
                    objectOutputStream.writeObject(user);
                    Log.d(TAG, "persist user:" + user);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    MyUtils.close(objectOutputStream);
                }
//反序列化过程
                User user = null;
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                if (cachedFile.exists()) {
                    ObjectInputStream objectInputStream = null;
                    try {
                        objectInputStream = new ObjectInputStream(
                                new FileInputStream(cachedFile));
                        user = (User) objectInputStream.readObject();
                        Log.d(TAG, "recover user:" + user);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } finally {
                        MyUtils.close(objectInputStream);
                    }
                }

这就是一个简单的使用,将对象输出到txt中就是序列化,将txt的数据读取出来就是反序列化。


android中Intent 是可以来传递一个序列化的对象


好了 之前说serialVersionUID 没有也可以序列化,但到底有什么用呢?

  1. serialVersionUID就像是标识符,当你序列化的时候serialVersionUID的值也会被写入,在反序列化的时候 系统会通过对比类里的serialVersionUID与文件中的serialVersionUID 是否一致来进行反序列化,但一致的时候反序列化成功不一致则不成功。
  2. 当你没声明serialVersionUID的时候,在反序列化的时候 修改了类那么反序列化会不成功 因为当前类与序列化时候的类不一样导致了反序列化的失败。(因为不声明serialVersionUID,系统会自动声明serialVersionUID,因为你进行类修改所以会导致 前后两次serialVersionUID的值不一样,所以反序列化失败)
  3. 但是如果你手动声明了serialVersionUID,当你修改了序列化对象,对其进行增删改属性之后再进行反序列化,此时反序列化会是成功的,也是无论修改这样serialVersionUID的值都是一样的,系统会最大程度的去回复数据。

3.2 Parcelable的接口

Parcelable也是序列化的一种方式,使用Parcelable只需去实现Parcelable接口即可

public class User implements Parcelable {
    public int userId;
    public String userName;
    public boolean isMale;
    public User() {
    }
    public User(int userId, String userName, boolean isMale) {
        this.userId = userId;
        this.userName = userName;
        this.isMale = isMale;
    }
    public int describeContents() {
        return 0;
    }
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(userId);
        out.writeString(userName);
        out.writeInt(isMale ? 1 : 0);
    }
    public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
        public User createFromParcel(Parcel in) {
            return new User(in);
        }
        public User[] newArray(int size) {
            return new User[size];
        }
    };
    private User(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        isMale = in.readInt() == 1;
    }
}


其实下列方法都会自动生成,不需要手打

既让Serializable 和Parcelable都可以序列化,那就说说两者之间的区别,Serializable 在序列化和反序列化的时候,需要进行大量 I/O操作,很耗时。Parcelable是android的序列化方式,他很高效,但是使用起来很麻烦。
Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化

3.3 Binder

Binder就是android的一个类,它实现了IBinder的接口。从IPC角度上来说,就是Binder是Android的一种跨进程通信的方式。从Android Framework的角度来分析就是Binder 是ServiceManager用来连接各种Manager的(ActivityManager WindowManager)桥梁。从Android应用层的角度来分析就是客户端和服务端进行通信的媒介,当bindService的时候,服务端会调用一个Binder对象,通过这个对象,客户端就可以获取到一些数据。

在Android的开发中,Binder主要用在Service中,包括AIDL和Messenger。普通的Service中的Binder不涉及到进程通信,所以适用AIDL来分析Binder的工作机制。

先演示一下AIDL的建立和通过AIDL生成JAVA文件去看看Binder的机制

其中Book.java是自定义类,用途就是个Bean。如果需要使用自定义类 必须要建立一个同名的aidl文件。
系统可能会找不到我们创建的类,那么就是要一下声明

sourceSets {
    main {
        manifest.srcFile ='src/main/AndroidManifest.xml'
        java.srcDirs = ['src/main/java', 'src/main/aidl']
        resources.srcDirs = ['src/main/java', 'src/main/aidl']
        aidl.srcDirs = ['src/main/aidl']
        res.srcDirs = ['src/main/res']
        assets.srcDirs = ['src/main/assets']
    }
}

接下来 我们来看看生成的JAVA文件 去看看Binder的机制,先看看生成的java文件,代码确实比较多,我就截取一个方法为例子,我直接在代码上进行解释


public interface IBookManager extends android.os.IInterface
{
/** Stub是运行在服务端中的 */
public static abstract class Stub extends android.os.Binder implements com.example.gyh.myapplication.IBookManager
{
//这个属性就是Binder的唯一标识,也就是包名
private static final java.lang.String DESCRIPTOR = "com.example.gyh.myapplication.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
//该方法就是为了将服务端的Binder转化为用户端的AIDL接口类型的对象,这种转化过程是区分进程的,若是同一个进程则就返回服务端本生的Stub对象,不是则转化成Stub.Proxy对象。
public static com.example.gyh.myapplication.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.gyh.myapplication.IBookManager))) {
return ((com.example.gyh.myapplication.IBookManager)iin);
}
return new com.example.gyh.myapplication.IBookManager.Stub.Proxy(obj);
}
//该方法就是返回Binder对象
@Override public android.os.IBinder asBinder()
{
return this;
}
//该方法是运行在服务端的Binder线程池中的,当客户端发起跨进程请求的时候,远程请求会通过系统底层的封装后交由改方法进行处理。现在来解释一下里面的参数代表着什么意思
code表示客户端请求的方式是什么,data保存着方法的参数,reply是要返回的值,flags为false的时候,客户端请求失败,这样利用这参数做权限验证。
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.example.gyh.myapplication.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.gyh.myapplication.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
//该类是运行在客户端中的
private static class Proxy implements com.example.gyh.myapplication.IBookManager
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public void addBook(com.example.gyh.myapplication.Book book) throws android.os.RemoteException
{
//首先要创立三个对象,_data,_reply,_result , 然后如果有参数的话把参数写到_data中,接着调用transact方法去发起远程过程调用(RPC),同时将现场挂起(就是暂停的意思);然后服务端的ontransact调用,直到RPC过程返回后,该线程继续。并从_reply中取出返回值,并且赋予_result去返回。

android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
//transact回去调用stub中的ontransact方法,找到对应的方法也就是ontransact里的addBook方法,取出返回的_reply,从中取出客户端需要的返回值
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
}
public void addBook(com.example.gyh.myapplication.Book book) throws android.os.RemoteException;
}

通过上面的分析,我们可以知道客户端发起远程请求时,当前线程会被挂起直到服务端进程返回结果,这是一个耗时操作,所以不能再UI线程中发起。其实服务端的Binder是运行在Binder线程池中的,所以不管Binder是否耗时都应该是同步的。下图解释



那么我再来总的概括一下,当我们和服务器建立连接之后,通过服务器返回的Binder,将Binder转化为客户端的AIDL接口对象,因为是跨进程所以返回的是Stub.Proxy对象,通过这个对象去调用相应的方法如addBook,addBook方法中则会通过tranasct方法去调用Stub中onTranasct方法中对应的addBook,并通过_reply返回相应的数据给客户端。这样一来整个跨进程通信就结束了。


Binder的死亡代理

Binder的两个很重要的方法linkToDeath和unlinkToDeath,当我们服务端的进程由于某种原因断裂,那么对导致我们远程调用失败。但更为重要的是我们不知道Binder是否断裂,那么客户端的功能就会收到影响,所以Binder给我们配置了两个方法,通过linkToDeath我们可以设置一个死亡代理,当Binder死亡的时候,可以给客户端发来消息,从而我们可以重新开启服务。然后如何去设置这个代理呢?我们先来看看demo


首先声明一个DeathRecipent对象,DeathRecipent是一个接口,其内部只有一个方法binderDied,当我们实现该方法的时候,当Binder死亡的时候系统就会回调改方法,然后我们可以移除之前绑带的Binder去重新开去新的Binder

linkToDeath方法的第二个参数是标记位,我们直接可以设置为0,这样我们就设置好了死亡代理。另外我们可以通过Binder的isBinderAlive的方法去判断Binder是否死亡。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容