Android源码分析 - Parcel 与 Parcelable

news2024/11/29 2:51:39

0. 相关分享

Android-全面理解Binder原理

Android特别的数据结构(二)ArrayMap源码解析

1. 序列化 - Parcelable和Serializable的关系

如果我们需要传递一个Java对象,通常需要对其进行序列化,通过内核进行数据转发,可能转发到本地文件,也可能转发到其他进程。序列化的方式很多,只要定好序列化和反序列化的规则,就可以进行Java对象的传输。常见的就是通过Serializable接口进行序列化。

Serializable序列化

Serializable序列化接口,将Java对象转换为字节序列写入文件中,实现了持久化存储。下一次需要使用该Java对象时,可以直接通过Serializable的反序列化规范,将文件中的数据提取出来,转换回Java对象。

Serializable序列化不仅可以让Java对象在本地持久化存储,还可以将此对象数据二进制用于网络传输、进程之间传输。在Android中,为什么还需要设计一个Parcelable来进行序列化呢?对Java对象的序列化方式远不止Serializable、Parcelable,序列化与反序列化的本质目的就是让Java对象能够在不同程序(可能不在一个主机上)进行传输,其实现可能关注在编码,也可能关注在性能,也可能关注在多平台可用。Android中如果要进行进程间通信,使用Serializable并不会表现出良好的性能优势。Serializable序列化过程中会出现反射和IO操作,这对性能要求高的程序来说是不合适的。为针对性能,Android推出了Parcelable序列化接口:

Parcelable序列化

简而言之,Parcelable将Java对象序列化到内存中,其他进程可以通过内核访问到Parcelable序列化后的Java对象的数据,和Serializable不同的是,Parcelable不需要通过内核去进行IO、反射来反序列化,而是直接将序列化的数据写入到内存中。

Parcelable翻译也是“可打包的”,把Java对象的实例数据打包到一块连续的内存空间中(写到Parcel这个native层的对象中,它的大小是可变的,填充数据过程可能会发生扩容,但一定是连续空间)。我们本文主要探讨Parcelable的实现原理,及其在跨进程通信中的表现。

2. Parcelable和Parcel的关系

Parcelable是Android特有的序列化接口,序列化的数据需要存放到内存中,那么再内存中就需要一个Parcel对象来保存这些数据。Parcel对象的结构设计,就表现出了Parcelable序列化的原理。

3. Parcel的结构设计

Parcel是一个C++对象。当我们需要通过Parcelable接口序列化一个Java对象时,需要先通过JNI创建一个Parcel对象,创建Parcel对象时会在内存开辟一块连续的内存空间,Java对象的数据可以按顺序填充到这段内存空间,这样一来,只要知道这段内存空间的地址,就可以按同样的顺序取出数据,填充给另一个Java对象。它的结构大致如下,在内存空间开辟一个Parcel对象,会得到一个首地址。position指针用来表示下一个数据可以存放的位置,也可以表示下一个要读取数据的起始位置。

请添加图片描述

我们知道,不同数据占用的内存空间是不同的,例如Int类型需要占据 4 字节,而double类型则需要占据 8 字节。填入一个数据后,下一个数据可以填充的位置position就需要在原有基础上跨过刚填充数据的字节占用数量。例如上图,position指向了下一个可以插入数据的位置,接下来我要插入一个 int a = 3,将int类型的 3 写入后,将position后移 4 Byte,使之指向未来可以插入数据的位置。

我们来看一下其具体使用:

4. Parcel的使用方法

首先要进行序列化,就要对Java对象实现Parcelable接口,假设我们需要序列化一个User对象,且它的iconUrl属性不参与序列化。代码大致如下:

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

public class User implements Parcelable {

    long id;
    String username;
    String password;
    String iconUrl;//不参与序列化
    int age;
    boolean sex;

    //生成一个User对象,其实例数据,通过Parcel获取
    protected User(Parcel in) {
        id = in.readLong();
        username = in.readString();
        password = in.readString();
//        iconUrl = in.readString();//不参与序列化
        age = in.readInt();
        sex = in.readByte() != 0;
    }

    //将User对象数据序列化存放到Parcel对象中
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(id);
        dest.writeString(username);
        dest.writeString(password);
//        dest.writeString(iconUrl);//不参与序列化
        dest.writeInt(age);
        dest.writeByte((byte) (sex ? 1 : 0));
    }


    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };
}

writeToParcel()方法是序列化的核心,将需要序列化的属性通过 writeLong()、writeString() 等方法写入 Parcel 对象中。未来反序列化时,也需要与序列化时相同顺序进行 readLong()、readString() 进行数据提取。到这里我们只能在上层感受到Parcel对象数据的写入和读取是有序的,而且是有类型要求的,那么具体是如何实现的?我们还需要看到 write() 相关的代码:

5. Parcel实现原理

Parcel实现原理主要关注到它的写入和读出,是如何写入到一个连续内存空间的,以及如何从连续内存空间有序地提取数据出来的。写入Java对象时,通过writeToParcel()方法将Java对象的实例数据写入到Parcel对象中。先来关注一下Parcel是何时被调用writeToParcel()的。如果我们通过AIDL实现一个跨进程通信,会生成一个Binder实体类和代理类,代理类中,就调用了writeToParcel()。如下是定义的一个AIDL:

//IFyService.aidl
import com.company.binderstudy.javabean.User;

interface IFyService{
    int addUser(in User user);
}

我们可以通过Binder代理来调用这个addUser:

//IFyService.java
private static class Proxy implements com.company.binderstudy.IFyService{
    @Override public int addUser(com.company.binderstudy.javabean.User user) throws android.os.RemoteException
      {
        //复用或者创建一个Parcel对象用于序列化发送数据
        android.os.Parcel _data = android.os.Parcel.obtain();
        //复用或者创建一个Parcel对象用于接收返回数据
        android.os.Parcel _reply = android.os.Parcel.obtain();
        int _result;
        try {
            //往parcel中写入token
          _data.writeInterfaceToken(DESCRIPTOR);
          if ((user!=null)) {
              //如果传入参数user不为空,就开始对其序列化
              //写入一个标志,表示对象不为null
            _data.writeInt(1);
              //调用其writeToParcel方法,进行序列化,将数据写入parcel
            user.writeToParcel(_data, 0);
          }
          else {
              //如果传入参数为null,写入一个标志位0,表示对象为null
            _data.writeInt(0);
          }
            //调用远程服务的addUser方法
          boolean _status = mRemote.transact(Stub.TRANSACTION_addUser, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().addUser(user);
          }
          _reply.readException();
          _result = _reply.readInt();
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
}

可以看到,通过Proxy进行远程通信的时候,需要做几件事:

  1. 复用或者创建一个Parcel对象_data用于序列化发送数据
  2. 复用或者创建一个Parcel对象_reply用于接收返回数据
  3. 往 _data 中写入token,标识着这个parcel来自哪个服务(服务的全路径)
  4. 将方法的若干个传入参数序列化到_data中
  5. 将_data 发送给远程服务,发起事务。

我们主要关注Parcel对象的复用或创建,与序列化。

5.1 Parcel对象的复用与创建

Parcel.obtain()方法,进行Parcel对象的复用与创建。Parcel对象的实例化过程,除了C++层的Parcel对象创建,还包括了其Java层外壳Parcel对象的创建。Java层的Parcel对象主要用于对Java应用提供接口,以及提供复用池设计:

//Parcel.java
private static final Object sPoolSync = new Object();
//单链表形式的复用池
private Parcel mPoolNext;
static protected final Parcel obtain(long obj) {
    Parcel res = null;
    //1. 在复用池获取
    synchronized (sPoolSync) {
        if (sHolderPool != null) {
            res = sHolderPool;
            sHolderPool = res.mPoolNext;
            res.mPoolNext = null;
            sHolderPoolSize--;
        }
    }
	//2. 如果没有可复用的,就new一个Parcel
    if (res == null) {
        res = new Parcel(obj);
    } else {
        if (DEBUG_RECYCLE) {
            res.mStack = new RuntimeException();
        }
        res.init(obj);
    }
    return res;
}
//2. 构造函数,调用native层的方法,通过JNI在本地内存中创建一个C++层的Parcel对象
private Parcel(long nativePtr) {
    if (DEBUG_RECYCLE) {
        mStack = new RuntimeException();
    }
    init(nativePtr);
}

private void init(long nativePtr) {
    if (nativePtr != 0) {
        mNativePtr = nativePtr;
        mOwnsNativeParcelObject = false;
    } else {
        mNativePtr = nativeCreate();
        mOwnsNativeParcelObject = true;
    }
}
//4. native层的方法,通过JNI调用
private static native long nativeCreate();

因为在本地内存中开辟一块连续内存空间是耗时的(使用过程中可能需要扩容,一开始创建Parcel对象的时候并不是确定长度的),所以尽量不要频繁地创建、删除native层的Parcel对象,通过复用池来保存复用对象。Java层的Parcel对象是如何持有native层的Parcel对象引用的?其实从nativeCreate()方法的返回值就能猜出来,将native层的Parcel的内存地址,将会交给mNativePtr。

我们来到native层看一下Parcel的创建:

//android_os_Parcel.cpp
static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
    Parcel* parcel = new Parcel();
    return reinterpret_cast<jlong>(parcel);
}
//Parcel.cpp
//构造函数
Parcel::Parcel()
{
    LOG_ALLOC("Parcel %p: constructing", this);
    initState();
}
//用来释放内存
Parcel::~Parcel()
{
    freeDataNoInit();
    LOG_ALLOC("Parcel %p: destroyed", this);
}


void Parcel::initState()
{
    LOG_ALLOC("Parcel %p: initState", this);
    mError = NO_ERROR;
    mData = nullptr;
    mDataSize = 0;
    mDataCapacity = 0;
    mDataPos = 0;
    ALOGV("initState Setting data size of %p to %zu", this, mDataSize);
    ALOGV("initState Setting data pos of %p to %zu", this, mDataPos);
    mObjects = nullptr;
    mObjectsSize = 0;
    mObjectsCapacity = 0;
    mNextObjectHint = 0;
    mHasFds = false;
    mFdsKnown = true;
    mAllowFds = true;
    mDeallocZero = false;
    mOwner = nullptr;
    clearCache();
	//...
}

至此,Java层的Parcel对象的mNativePtr就指向了native层的Parcel对象的地址。刚创建的nateive层的Parcel对象占用空间很小,只有在不断写入数据的过程中,才会发生扩容。

5.2 Parcel对象序列化数据的写入

创建好Parcel对象之后,就可以往里写入序列化数据,通过调用需要序列化的Java对象的writeToParcel()方法进行写入,仍然还是上面的 User 类的例子:

@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeLong(id);
    dest.writeString(username);
    dest.writeString(password);
    //        dest.writeString(iconUrl);//不参与序列化
    dest.writeInt(age);
    dest.writeByte((byte) (sex ? 1 : 0));
}

这些写入的方法大同小异,我们就看writeString():

//Parcel.java
public final void writeString16(@Nullable String val) {
    mReadWriteHelper.writeString16(this, val);
}
//最终调用到ReadWriteHelper的writeString16方法
public void writeString16(Parcel p, String s) {
    p.writeString16NoHelper(s);
}
//最后来到Parcel的nativeWriteString16
private static native void nativeWriteString16(long nativePtr, String val);

来到native层的Parcel对象数据写入:

//android_os_parcel
static void android_os_Parcel_writeString16(JNIEnv *env, jclass clazz, jlong nativePtr,
        jstring val) {
    //根据mNativePtr反向获取到native层的Parcel对象
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != nullptr) {
        status_t err = NO_ERROR;
        if (val) {
            //获取String数据的长度(char数组的长度)
            const size_t len = env->GetStringLength(val);
            //计算需要申请的控件长度
            const size_t allocLen = len * sizeof(char16_t);
            //先写入长度再写入数据
            err = parcel->writeInt32(len);
            //先判断空间,写入对齐填充
            char *data = reinterpret_cast<char*>(parcel->writeInplace(allocLen + sizeof(char16_t)));
            if (data != nullptr) {
                //将数据填充到data指针指向的地址,写入val数据
                env->GetStringRegion(val, 0, len, reinterpret_cast<jchar*>(data));
                *reinterpret_cast<char16_t*>(data + allocLen) = 0;
            } else {
                err = NO_MEMORY;
            }
        } else {
            err = parcel->writeString16(nullptr, 0);
        }
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}

在写入字符串之前,先写入字符串的长度(便于反序列化的时候,确认要从内存中连续读取多少内容),然后再写入数据。

首先通过 witeInt32() 写入字符串长度:

//Parcel.cpp
status_t Parcel::writeInt32(int32_t val)
{
    return writeAligned(val);
}

template<class T>
status_t Parcel::writeAligned(T val) {
    static_assert(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));

    if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
        //通过mData基地址+mDataPos偏移量,在可以写入的位置写入新的值
        *reinterpret_cast<T*>(mData+mDataPos) = val;
        //更新下一个可以写入的位置,即更新mDataPos的值
        return finishWrite(sizeof(val));
    }
	//如果需要扩容,先扩容,然后通过goto回到写入部分再次写入
    status_t err = growData(sizeof(val));
    if (err == NO_ERROR) goto restart_write;
    return err;
}

//完成写入,更新mDataPos位置
status_t Parcel::finishWrite(size_t len)
{
    //写入长度太长就报错
    if (len > INT32_MAX) {
        return BAD_VALUE;
    }
	//更新mDataPos位置
    mDataPos += len;
    //mDataSize记录的是现有数据个数
    //mDataPos有可能会回撤用于重写之前填入的数据,所以还需要mDataSize来记录现有全部数据个数
    if (mDataPos > mDataSize) {
        mDataSize = mDataPos;
    }
    return NO_ERROR;
}

//扩容
status_t Parcel::growData(size_t len)
{
    if (len > INT32_MAX) {
        return BAD_VALUE;
    }
    if (len > SIZE_MAX - mDataSize) return NO_MEMORY; 
    if (mDataSize + len > SIZE_MAX / 3) return NO_MEMORY; 
    size_t newSize = ((mDataSize+len)*3)/2;
    //在continueWrite()中进行了alloc申请新空间
    return continueWrite(newSize);
}

可以看到,在真正数据写入的时候,会进行扩容判断,如果容量不够了,会先通过 growData() 进行扩容,然后再进行写入。注意几个指针:

  • mData - 表示 native层Parcel的数据的起始地址
  • mDataPos - 类似于游标,可以用来表示下一个插入数据的位置,也可以用来遍历提取数据
  • mDataSize - 表示当前被序列化的元素总大小

写入数据后,会更新mDataPos。写入字符串首先写入完int类型表示长度之后,就写入字符串的char[]数据,首先会根据字符串长度计算,并做对齐填充,同样的,也可能会通过growData()进行扩容,然后通过env->GetStringRegion(val, 0, len, reinterpret_cast<jchar*>(data))将val的数据写入data。

5.3 并不是所有类型都能写入Parcel

写入Parcel的类型判断在Java层的Parcel完成。我们可以看到方法列表中,都给出了可以写入的类型。

请添加图片描述

比较特别的是Map类型的数据写入,我们知道Map的Value类型是不确定的,Parcel当然也在对Map遍历写入的过程中会进行类型判断,只允许写入规范内的类型,以写入ArrayMap为例:

//Parcel.java
//写入ArrayMap
public void writeArrayMap(@Nullable ArrayMap<String, Object> val) {
    writeArrayMapInternal(val);
}

void writeArrayMapInternal(@Nullable ArrayMap<String, Object> val) {
    if (val == null) {
        writeInt(-1);
        return;
    }
    final int N = val.size();
    writeInt(N);
    int startPos;
    for (int i=0; i<N; i++) {
        //先写入Key,在写入Value,Key必须是String类型
        writeString(val.keyAt(i));
        writeValue(val.valueAt(i));     
}

核心看到这里的writeValue()是如何做判断的:

//Parcel.java
public final void writeValue(@Nullable Object v) {
    if (v instanceof LazyValue) {
        LazyValue value = (LazyValue) v;
        value.writeToParcel(this);
        return;
    }
    //拿到Value的类型,在这类做类型判断,如果类型不符合要求,会抛异常
    int type = getValueType(v);
    //如果没有抛异常,就继续执行下去,先写入类型
    writeInt(type);
    //如果是一个有长度的type,除了写入value,还要写入长度
    if (isLengthPrefixed(type)) {
        // Length
        int length = dataPosition();
        writeInt(-1); // Placeholder
        // Object
        int start = dataPosition();
        writeValue(type, v);
        int end = dataPosition();
        // Backpatch length
        setDataPosition(length);
        writeInt(end - start);
        setDataPosition(end);
    } else {
        //writeValue写入的时候只能写入确定类型的,如果不在范围内,将会报错,即无法Parcel打包
        //例如你写了一个Object的子类例如Student,但是没有实现Parcelable接口,或者没有符合Parcel写入规约,将会在writeValue的时候报错
        writeValue(type, v);
    }
}

写入value的时候,首先会对Value的类型进行判断,如果不是规范类型,将会抛出异常,如果是规范类型,还会分成不定长度的类型和定长类型。比如String、Map就是不定长,Integer这类的就是定长数据。

//Parcel.java
//获取Value的类型
public static int getValueType(@Nullable Object v) {
    if (v == null) {
            return VAL_NULL;
        } else if (v instanceof String) {
            return VAL_STRING;
        } else if (v instanceof Integer) {
            return VAL_INTEGER;
        } else if (v instanceof Map) {
            return VAL_MAP;
        } else if (v instanceof Bundle) {
            // Must be before Parcelable
            return VAL_BUNDLE;
    	}
    	//...
        else {
            Class<?> clazz = v.getClass();
            if (clazz.isArray() && clazz.getComponentType() == Object.class) {
                return VAL_OBJECTARRAY;
            } else if (v instanceof Serializable) {
                // Must be last
                return VAL_SERIALIZABLE;
            } else {
                //如果类型不对,抛异常
                throw new IllegalArgumentException("Parcel: unknown type for value " + v);
            }
        }
}

至此,将Java对象的数据序列化写入native层的Parcel对象的过程以及跟通了。小小感受一下它和Serializable的区别,Serializable将序列化的数据直接写入文件,而Parcelable接口则将序列化的数据写入内存,更加适用于跨进程通信。既然Parcelable适用于跨进程通信,我们就来看一下Parcel在跨进程通信过程中的表现:

6. Parcel在Bundle中的使用

通常我们使用Intent来发起进程间通信,传递的数据可以放到Intent中,其实最终都是放到Intent的Bundle类型的mExtras中:

//Intent.java
private Bundle mExtras;

public Intent putExtra(String name, Charsequence value){
    if (mExtras == null) {
            mExtras = new Bundle();
        }
        mExtras.putCharSequence(name, value);
        return this;
}

public Intent putExtra(String name, Parcelable value){
    if (mExtras == null) {
        mExtras = new Bundle();
    }
    mExtras.putParcelable(name, value);
    return this;
}
//...

放到Intent中的数据将通过Bundle类型的mExtras.putXXX()存放到Bundle中,这个方法在Bundle的父类BaseBundle中实现:

//BaseBundle.java
ArrayMap<String, Object> mMap = null;
volatile Parcel mParcelledData = null;//如果mParcelledData不为空,那么mMap将为空,并且数据存储为包含Bundle的Parcel。但数据被拆封时,mParcelledData将会被设置为null

void putBoolean(String key, boolean value){
    unparcel();//数据拆封,放到mMap中
    mMap.put(key,value);
}

void putString(String key, String value){
    unparcel();//数据拆封,放到mMap中
    mMap.put(key,value);
}
//...

其中,如果mParcelledData不为空,那么mMap将为空,并且数据存储为包含Bundle的Parcel。但数据被拆封时,mParcelledData将会被设置为null。

正常情况下,ArrayMap的存储容量只受堆大小影响。但如果将数据打包到Parcel中进行进程间通信,就需要考虑Binder的mmap映射内存空间的大小了,一般情况下,内存大小不能超过 1M - 8K。再大也不能超过 4M。

binder驱动给每个进程分配最多4M的buffer空间(一般从Zygote孵化出来的APP默认分配 1M-8K大小,servicemanager默认分配128K).

当然可以突破这个 1M-8K 的限制,可以自己手动调用open和mmap即可:

int main(int argc,char **argv){
...
bs = binder_open("/dev/binder",【自定义大小】);
}

但是还是无法突破 binder_mmap() 中 SM_4M 的限制

如果还要再深究,其实binder_mmap中害设置了最大值的另外设置:

static int binder_mmap(...){
proc->free_async_space = proc->buffer_size/2;
}

对于oneway和非oneway来说:

手写mmap初始化binder服务ProcessState初始化Binder服务
oneway4M/2(1M-8K)/2
非oneway4M1M-8K

一般情况下,Intent传输数据的上限是1M,因为 Intent 传输数据的机制中,用到了Binder。Intent 中的数据,会被作为 Parcel被存储在 Binder的事务缓冲区(Binder transaction buffer)中的对象进行传输。而 1M 并不是安全的上限,还是推荐不要通过Intent传递太大的数据。

解决办法:

  1. 减少传输数据量
  2. Intent 通过绑定一个 Bundle 来传输,这个可以超过 1M,不过也不能过大
  3. 通过内存共享
  4. 通过文件共享,如这里说到的 binder通信中进行传输文件句柄fd

这里不做 Bundle 的知识补充

7. Parcel在Binder通信中的表现

Parcel在Binder通信中,并不只序列化Java实例数据,还存了许多其他信息,包括但不限于Binder实体/远程引用:

binder_parcel_binder_object_format

如果要传递大量数据,只能通过传递文件句柄fd,通过共享文件的方式来传递大数据:

binder_fdobject_translate

那么Parcel写入数据的时候如何写入这些内容呢?显然入口是通过Parcel.java写入binder。对应的方法是nativeWriteStrongBinder(),来到native层:

//android_os_Parcel.cpp
static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        //交给Parcel对象来写入Binder
        const status_t err = parcel->writeStrongBinder(ibinderForJavaObject(env, object));
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}

native层的Parcel写入Binder,会将Binder“压扁打平”写入Parcel,这部分解析可以参考上图结构:

//Parcel.cpp
status_t Parcel::writeStrongBinder(const sp<IBinder>& val)
{
    return flatten_binder(ProcessState::self(), val, this);
}

status_t flatten_binder(const sp<ProcessState>& /*proc*/,
    const sp<IBinder>& binder, Parcel* out)
{
    //Binder数据被拆分放入flat_binder_object对象中
    flat_binder_object obj = {};
    if (binder != nullptr) {
        BHwBinder *local = binder->localBinder();
        if (!local) {
            //会进行一个判断,如果这个IBinder是远程服务,则会转换为Binder远程引用,也就是handle,存入Parcel中
            BpHwBinder *proxy = binder->remoteBinder();
            if (proxy == nullptr) {
                ALOGE("null proxy");
            }
            //生成一个int类型的handle句柄- binder远程引用句柄
            const int32_t handle = proxy ? proxy->handle() : 0;
            //设置类型为handle
            obj.hdr.type = BINDER_TYPE_HANDLE;
            //给出标记
            obj.flags = FLAT_BINDER_FLAG_ACCEPTS_FDS;
            obj.binder = 0; 
            //由于是handle,只设置handle的值
            obj.handle = handle;
            obj.cookie = 0;
        } else {
            //如果这个IBinder是本地服务,将会转换为Binder实体,存入Parcel中
            int policy = local->getMinSchedulingPolicy();
            int priority = local->getMinSchedulingPriority();
			//标志设置为本地服务
            obj.flags = priority & FLAT_BINDER_FLAG_PRIORITY_MASK;
            obj.flags |= FLAT_BINDER_FLAG_ACCEPTS_FDS | FLAT_BINDER_FLAG_INHERIT_RT;
            obj.flags |= (policy & 3) << FLAT_BINDER_FLAG_SCHED_POLICY_SHIFT;
            if (local->isRequestingSid()) {
                obj.flags |= FLAT_BINDER_FLAG_TXN_SECURITY_CTX;
            }
            //类型设为Binder实体
            obj.hdr.type = BINDER_TYPE_BINDER;
            //设置实体的引用,根据名字可以猜到使用弱引用
            obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());
            obj.cookie = reinterpret_cast<uintptr_t>(local);
        }
    } else {
        //如果根本就没有传递binder,我猜测传递的是ServiceManager这个handle为0的服务
        obj.hdr.type = BINDER_TYPE_BINDER;
        obj.binder = 0;
        obj.cookie = 0;
    }
	//将obj写入out这个Parcel中
    return finish_flatten_binder(binder, obj, out);
}

可以看到,flatten_binder的任务主要根据IBinder是本地服务还是远程引用,拼接 flat_binder_object。最后写入到Parcel中则是通过 finish_flatten_binder() 将这个 flat_binder_object 写入。

template <typename T>
status_t Parcel::writeObject(const T& val)
{
    const bool enoughData = (mDataPos+sizeof(val)) <= mDataCapacity;
    const bool enoughObjects = mObjectsSize < mObjectsCapacity;
    if (enoughData && enoughObjects) {
        //如果需要扩容,扩容后会根据这个标记goto到这里开始执行
restart_write:
        //写入数据到mData+mDataPos,也就是下一个可以写入的位置
        *reinterpret_cast<T*>(mData+mDataPos) = val;
        //根据接入的对象,强转成 binder_object_header对象
        const binder_object_header* hdr = reinterpret_cast<binder_object_header*>(mData+mDataPos);
        //根据头部中的type信息来判断接下来需要写入什么内容
        switch (hdr->type) {
                //如果类型是Binder类型
            case BINDER_TYPE_BINDER:
            case BINDER_TYPE_WEAK_BINDER:
            case BINDER_TYPE_HANDLE:
            case BINDER_TYPE_WEAK_HANDLE: {
                //强转回 flat_binder_object 类(就是刚传入的val)
                const flat_binder_object *fbo = reinterpret_cast<const flat_binder_object*>(hdr);
                //如果这是binder的实体
                if (fbo->binder != 0) {
                    //将偏移量记录
                    mObjects[mObjectsSize++] = mDataPos;
                    //将这个 flat_binder_object 记录到 ProcessState中
                    acquire_binder_object(ProcessState::self(), *fbo, this);
                }
                break;
            }
                //如果类型是文件描述符(共享文件)
            case BINDER_TYPE_FD: {
                // remember if it's a file descriptor
                if (!mAllowFds) {
                    // fail before modifying our object index
                    return FDS_NOT_ALLOWED;
                }
                mHasFds = mFdsKnown = true;
                mObjects[mObjectsSize++] = mDataPos;
                break;
            }
            case BINDER_TYPE_FDA:
                mObjects[mObjectsSize++] = mDataPos;
                break;
            case BINDER_TYPE_PTR: {
                const binder_buffer_object *buffer_obj = reinterpret_cast<
                    const binder_buffer_object*>(hdr);
                if ((void *)buffer_obj->buffer != nullptr) {
                    mObjects[mObjectsSize++] = mDataPos;
                }
                break;
            }
            default: {
                ALOGE("writeObject: unknown type %d", hdr->type);
                break;
            }
        }
        //完成写入,更新mDataPos
        return finishWrite(sizeof(val));
    }

    //如果空间不够,就进行扩容,最后通过 goto 回到写入数据的部分。
    if (!enoughData) {
        const status_t err = growData(sizeof(val));
        if (err != NO_ERROR) return err;
    }
    if (!enoughObjects) {
        if (mObjectsSize > SIZE_MAX - 2) return NO_MEMORY; // overflow
        if (mObjectsSize + 2 > SIZE_MAX / 3) return NO_MEMORY; // overflow
        size_t newSize = ((mObjectsSize+2)*3)/2;
        if (newSize > SIZE_MAX / sizeof(binder_size_t)) return NO_MEMORY; // overflow
        binder_size_t* objects = (binder_size_t*)realloc(mObjects, newSize*sizeof(binder_size_t));
        if (objects == nullptr) return NO_MEMORY;
        mObjects = objects;
        mObjectsCapacity = newSize;
    }

    goto restart_write;
}

8. 总结

Android为了实现进程间通信,传递Java对象数据,需要对数据进行序列化。现有许多序列化工具,常用的就是Serializable接口,Serializable序列化的优点就是适用范围广,不仅可以持久化存储对象到本地,也可以进行网络传输,但最大的缺点就是使用了反射和IO,性能不高。进场间通信是一个聚焦的功能,使用Parcelable接口直接将对象序列化到内存中,相比之下减少了反射和IO的时间损耗。当然,我们也知道Zygote的通信是通过socket的,如果在socket场景下要进行进程间通信,仍然需要使用Serializable进行序列化。

此外,虽然说Parcelable接口将对象序列化到内存中,这个“内存”仍然是进程私有的,不是共享内存。一个APP进程除了有JVM虚拟机的内存空间,还有本地内存(包含了元空间、直接内存)。JNI创建的C++对象是在本地内存的,它将数据直接写在内存块中。通过Binder通信,Binder驱动将这个内存块的数据直接拷贝到接收方进程的映射内存空间中,接收方访问这部分内存可以直接根据Parcelable的约定来反序列化出数据,实现了跨进程数据通信。

注:JNI创建的C++对象到底在JVM堆内存还是本地内存的哪个位置笔者还没探究清除。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/402064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

这几个群,程序员可千万不要进!

震惊&#xff01;某摸鱼网站惊现肾结石俱乐部&#xff01; &#xff08;图源V2EX&#xff09; 无关地域、无关性别&#xff0c;各位程序员们在肾结石这个病上面有着出奇一致的反应。诸如此类的各种职业病在我们的生活中更是十分常见。 也可能是到年纪了&#xff0c;在办公室…

ATTCK v12版本战术介绍——提权(一)

一、引言在前几期文章中我们介绍了ATT&CK中侦察、资源开发、初始访问、执行、持久化战术理论知识及实战研究&#xff0c;通过实战场景验证行之有效的检测规则、防御措施&#xff0c;本期我们为大家介绍ATT&CK 14项战术中提权战术&#xff08;一&#xff09;&#xff0c…

计算机图形学09:二维观察之点的裁剪

作者&#xff1a;非妃是公主 专栏&#xff1a;《计算机图形学》 博客地址&#xff1a;https://blog.csdn.net/myf_666 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录专栏推荐专栏系列文章序一、二维观察基本…

设计模式4——行为型模式

行为型模式用于描述程序在运行时复杂的流程控制&#xff0c;即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务&#xff0c;它设计算法与对象间职责的分配。 行为型模式分为类行为模式和对象行为模式&#xff0c;前者采用继承机制来在类间分派行为&…

[洛谷-P1272] 重建道路(树形背包DP)

[洛谷-P1272] 重建道路&#xff08;树形背包DP&#xff09;一、题目重建道路题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1提示样例解释限制与约定二、思路1、状态表示2、转移方程3、循环设计4、初末状态三、代码一、题目 重建道路 题目描述 一场可怕的地震后&…

计算机Ping命令使用详解

计算机Ping命令使用详解 在网络中 ping 是一个十分强大的 TCP/IP 工具。它的作用主要为: 1、用来检测网络的连通情况和分析网络速度 2、根据域名得到服务器 IP 3、根据 ping 返回的 TTL 值来判断对方所使用的操作系统及数据包经过路由器数量。 我们通常会用它来直接 ping ip 地…

uni-app api 获取系统信息(高、宽)用法及封装

uni-app提供了异步(uni.getSystemInfo)和同步(uni.getSystemInfoSync)的2个API获取系统信息 uniapp 官网解析地址 uni.getSystemInfo 异步获取系统信息 参数名类型必填说明successFunction是接口调用成功的回调failFunction否接口调用失败的回调函数completeFunction否接口…

腾讯混元AI大模型训练技术揭秘——太极AngelPTM

编者按&#xff1a;秉承“技术提效”理念&#xff0c;腾讯广告不断探索技术能力边界&#xff0c;全面升级广告系统&#xff0c;基于“一大平台、两大模型”持续精进创新研发&#xff0c;提升投放效率与投放效果&#xff0c;助力广告主实现高效的全域经营与生意增长。本篇文章是…

请介绍类加载过程,什么是双亲委派模型?

第23讲 | 请介绍类加载过程&#xff0c;什么是双亲委派模型&#xff1f; Java 通过引入字节码和 JVM 机制&#xff0c;提供了强大的跨平台能力&#xff0c;理解 Java 的类加载机制是深入 Java 开发的必要条件&#xff0c;也是个面试考察热点。 今天我要问你的问题是&#xff0…

ESP32驱动-红外寻迹传感器驱动

红外寻迹传感器驱动 1、红外寻迹传感器介绍 红外寻迹传感器具有一对红外线发射管与接收管,发射管发射出一定频率的红外线,当检测方向遇到障碍物(反射面)时,红外线反射回来被接收管接收,经过比较器电路处理之后,输出接口会输出一个数字信号(低电平或高电平,取决于电路…

JVM篇之内存及GC

目录一、JVM内存区域1.1程序计数器1.2虚拟机栈1.3本地方法栈1.4堆1.5方法区二、JVM运行时内存2.1新生代(轻量级GC)2.2老年代&#xff08;重量级GC&#xff09;一、JVM内存区域 JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区域【JAVA 堆、…

蓝桥杯嵌入式PWM_IN(打开中断)

1.原理图 2.配置 3.代码 关键函数 HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1) HAL_TIM_IC_CaptureCallback(TIM_HandTypeDef *htim)//回调函数 HAL_TIM_GET_COUNTER(&htim3) __HAL_TIM_SetCounter(&htim3,0)void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef …

spring5源码篇(4)——beanFactoryPostProcessor执行/注解bean的装配

spring-framework 版本&#xff1a;v5.3.19 前面研究了beanDefinition的注册&#xff0c;但也仅仅是注册这一动作。那么在spring容器启动的过程中&#xff0c;是何时/如何装配的&#xff1f;以及装配的bean是如何注入的&#xff1f; &#xff08;考虑到xml方式基本不用了以及篇…

探秘MySQL——全面了解索引、索引优化规则

文章目录0.什么是索引1.常用索引分类逻辑维度底层数据结构维度物理维度&#xff08;InnoDB&#xff09;2.为什么底层是B树平衡二叉查找树红黑树B树&#xff08;多叉&#xff09;B树&#xff08;多叉&#xff09;3.MySQL索引优化SQL性能分析之explainQ.MySQL如何查看查询是否用到…

fastdfs提高系统连接数

1.操作系统 vi /etc/systemd/system.conf 修改后重启系统。 ulimit -n 验证是否生效。 2.tracker ①docker exec -it trackerID bin/bash ②vi /etc/fdfs/tracker.conf 一般性能测试场景可配置10000 3.storage ①docker exec -it storageID bin/bash ②vi /etc/fdfs/st…

k8s-Pod域名学习总结

k8s-Pod域名学习总结 大纲 k8s内置DNS服务 配置Pod的域名服务 CornDNS配置 默认Pod的域名 自定义Pod的域名 实战需求 1 Pod有自己的域名 2 集群内部的Pod可以通过域名访问其他的Pod 基础准备&#xff1a; 1 k8s 集群版本1.17 k8s内置DNS服务 k8s1.17安装完成后自动创建…

保姆级使用PyTorch训练与评估自己的MixMIM网络教程

文章目录前言0. 环境搭建&快速开始1. 数据集制作1.1 标签文件制作1.2 数据集划分1.3 数据集信息文件制作2. 修改参数文件3. 训练4. 评估5. 其他教程前言 项目地址&#xff1a;https://github.com/Fafa-DL/Awesome-Backbones 操作教程&#xff1a;https://www.bilibili.co…

【python】如何用canvas在自己设计的软件上作画

文章目录前言Canvas组件Canvas画布界面画长方体画多边形PhotoImage组件展示gif的图片展示gif法2总结前言 python学习之路任重而道远&#xff0c;要想学完说容易也容易&#xff0c;说难也难。 很多人说python最好学了&#xff0c;但扪心自问&#xff0c;你会用python做什么了&a…

数据结构——线性数据结构(C语言实现顺序表详解)

1.什么线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串等… 在讲顺序表之前&#xff0c;我们先大致了解一下线性表。…

qt .pro文件 qmake编译过程

#&#xff1a;注释一行 QT&#xff1a;此项目中使用的Qt modules列表 CONFIG&#xff1a;此项目中使用的配置选项 TARGET&#xff1a;目标输出文件的名字 TEMPLATE&#xff1a;当生成二进制文件时项目的模版&#xff0c;例如app,lib 平台下使用 windows { SOURCES SysInf…