Parcelable原理分析以及读取数据异常的根源问题分析

本文深入解析了Android中Parcelable接口的工作原理,指出在实现序列化时成员变量的读写顺序必须一致,否则会导致数据异常。分析了Parcel的源码,揭示了数据在Java层与C层之间的交互,包括Parcel对象的创建、内存管理以及数据的读写操作。通过示例解释了错误的读写顺序如何导致数据错乱,强调了遵循读写顺序的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

着急的先看结论:Parcelable 的成员变量读写需要严格的遵循 先写先读、后写后读的原则。所以出现数据读取异常,大概率是因为读取顺序和写入顺序不一致。
想学东西的看后文分析
如下例子
public class PackageInfo implements Parcelable {

    public String packageName="com.exemple.aa";
    public int appId = 0;
    public int appMode = 0;
    public String apkPath="/data/data/com.eae.fgf/file/a.apk";

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.packageName);
        dest.writeInt(this.appId);
        dest.writeString(this.apkPath);
        dest.writeInt(this.appMode);  
    }

    PackageSetting(int version, Parcel in) {
        this.packageName = in.readString();
        this.appId = in.readInt();
        this.appMode = in.readInt();
        this.apkPath =  in.readString();
    }

}

写入 appMode的时候写入的是0 ,读取的时候却读到了65535,简直是一头雾水。

原因就是在写入的时候第三个写入的是 dest.writeString(this.apkPath);,而读取的时候第三个读取的却是 this.appMode = in.readInt(); 所以实际上this.appMode = in.readInt()读取到的是apkPath的值。

我们来看一下writeInt源码

public final void writeInt(int val) {

        nativeWriteInt(mNativePtr, val);

}

我们可以看到具体的写入是由native层的C代码实现的

private static native void nativeWriteInt(long nativePtr, int val);

第一个参数为 mNativePtr

第二个参数就是我们要写入的值

所以我们需要知道第一个参数是怎么来的,这得从Parcel创建说起

public static Parcel obtain() {
    final Parcel[] pool = sOwnedPool;
    synchronized (pool) {
        Parcel p;
        for (int i=0; i<POOL_SIZE; i++) {
            p = pool[i];
            if (p != null) {
                pool[i] = null;
                if (DEBUG_RECYCLE) {
                    p.mStack = new RuntimeException();
                }
                p.mReadWriteHelper = ReadWriteHelper.DEFAULT;
                return p;
            }
        }
    }
    return new Parcel(0);
}
private Parcel(long nativePtr) {
    if (DEBUG_RECYCLE) {
        mStack = new RuntimeException();
    }
    //Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack);
    init(nativePtr);
}
private void init(long nativePtr) {
    if (nativePtr != 0) {
        mNativePtr = nativePtr;
        mOwnsNativeParcelObject = false;
    } else {
        mNativePtr = nativeCreate();
        mOwnsNativeParcelObject = true;
    }
}

我们可以看到parcel池存列表存在 parcel对象的话就将该位置的parcel对象拿出来用,并且池的原位置置空。那样的话mNativePtr就沿用旧对象的。如果parcel池不存在parcel对象那就重新创建一个。那么就 mNativePtr = nativeCreate();

nativeCreate由native方法实现。让我们来看看C层代码

\frameworks\base\core\jni\android_os_Parcel.cpp

static const JNINativeMethod gParcelMethods[] = {
        ......
        {"nativeCreate",              "()J", (void*)android_os_Parcel_create},
        ......
        {"nativeWriteInt",            "(JI)V", (void*)android_os_Parcel_writeInt},
        {"nativeWriteLong",           "(JJ)V", (void*)android_os_Parcel_writeLong},
        {"nativeWriteFloat",          "(JF)V", (void*)android_os_Parcel_writeFloat},
        {"nativeWriteDouble",         "(JD)V", (void*)android_os_Parcel_writeDouble},
        {"nativeWriteString",         "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString},
        ......
        {"nativeReadInt",             "(J)I", (void*)android_os_Parcel_readInt},
        {"nativeReadLong",            "(J)J", (void*)android_os_Parcel_readLong},
        {"nativeReadFloat",           "(J)F", (void*)android_os_Parcel_readFloat},
        {"nativeReadDouble",          "(J)D", (void*)android_os_Parcel_readDouble},
        {"nativeReadString",          "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString},
        ......
};

所以 native long nativeCreate() 的实现即

static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz) {
    // Create a Parcel object for the native layer
    Parcel* parcel = new Parcel();
    // Returns the long value of its pointer
    return reinterpret_cast<jlong>(parcel);
}

所以 Java 层的 Parcel 的 mNativePtr 成员变量,实际上是持有着 C层 Parcel 结构体指针地址的整型值。

我们回过头来再看 nativeWriteInt的实现 android_os_Parcel_writeInt

static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jlong nativePtr, jint val)
{
    // nativePtr is Android_ os_ Parcel_ The pointer value of the parcel object returned by create. Here, the parcel pointer is strongly returned
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        // Use the Parcel pointer to call its writeInt32() method to write data
        const status_t err = parcel->writeInt32(val);
    if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}
status_t Parcel::writeInt32(int32_t val)
{
    return writeAligned(val);
}
template<class T>
status_t Parcel::writeAligned(T val) {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
    // Determine whether the memory is sufficient
    if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
    // Move the pointer to the offset position (first address + pointer offset), and then write val to memory
    *reinterpret_cast<T*>(mData+mDataPos) = val;
    // Update memory offset
    return finishWrite(sizeof(val));
    }

    status_t err = growData(sizeof(val));
    if (err == NO_ERROR) goto restart_write;
        return err;
}

从源码分析可知 Java 层的 Parcel 创建时,实际上持有了 C层 Parcel 结构体的绝对地址指针。我们在Java层调用 dest.writeInt(), 系统会根据 Java 层 Parcel 持有的 C 层的 Parcel 结构体的指针,找到 C 层 Parcel 结构体的实际对象。并把数据写入到 Parcel 结构体中。

所以实际上数据的读写和存储是在 C层完成的,并且存储在内存中。 

那么C层Parcel 结构体是怎么运作的呢?

我们继续来看写入数据

template<class T>
status_t Parcel::writeAligned(T val) {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
    if ((mDataPos+sizeof(val)) <= mDataCapacity) {//已分配内存足够存储
restart_write:
        *reinterpret_cast<T*>(mData+mDataPos) = val;//直接将数据存储到内存中
        return finishWrite(sizeof(val));
    }
    status_t err = growData(sizeof(val));
    if (err == NO_ERROR) goto restart_write;
    return err;
}

status_t Parcel::finishWrite(size_t len)
{
    if (len > INT32_MAX) {//数据量大于int最大值,抛出异常
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }
    //printf("Finish write of %d\n", len);
    mDataPos += len;//对齐写入,直接按数据类型的长度后移指针,而不是实际数据长度
    ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos);
    if (mDataPos > mDataSize) {
        mDataSize = mDataPos;
        ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize);
    }
    //printf("New pos=%d, size=%d\n", mDataPos, mDataSize);
    return NO_ERROR;
}

status_t Parcel::growData(size_t len)
{
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }
    size_t newSize = ((mDataSize+len)*3)/2;  // 每次扩容都增加原来总长度的一半。防止频繁和过度扩容
    return (newSize <= mDataSize)
            ? (status_t) NO_MEMORY
            : continueWrite(newSize);
}

status_t Parcel::continueWrite(size_t desired)
{
    if (desired > INT32_MAX) {//扩容失败
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return BAD_VALUE;
    }
    // If shrinking, first adjust for any objects that appear
    // after the new data size.
    size_t objectsSize = mObjectsSize;
    if (desired < mDataSize) {
        if (desired == 0) {
            objectsSize = 0;
        } else {
            while (objectsSize > 0) {
                if (mObjects[objectsSize-1] < desired)
                    break;
                objectsSize--;
            }
        }
    }
    if (mOwner) {
        // If the size is going to zero, just release the owner's data.
        if (desired == 0) {
            freeData();
            return NO_ERROR;
        }
        // If there is a different owner, we need to take
        // posession.
        uint8_t* data = (uint8_t*)malloc(desired);
        if (!data) {
            mError = NO_MEMORY;
            return NO_MEMORY;
        }
        binder_size_t* objects = nullptr;
        if (objectsSize) {
            objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
            if (!objects) {
                free(data);
                mError = NO_MEMORY;
                return NO_MEMORY;
            }
            // Little hack to only acquire references on objects
            // we will be keeping.
            size_t oldObjectsSize = mObjectsSize;
            mObjectsSize = objectsSize;
            acquireObjects();
            mObjectsSize = oldObjectsSize;
        }
        if (mData) {
            memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
        }
        if (objects && mObjects) {
            memcpy(objects, mObjects, objectsSize*sizeof(binder_size_t));
        }
        //ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
        mOwner(this, mData, mDataSize, mObjects, mObjectsSize, mOwnerCookie);
        mOwner = nullptr;
        LOG_ALLOC("Parcel %p: taking ownership of %zu capacity", this, desired);
        pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
        gParcelGlobalAllocSize += desired;
        gParcelGlobalAllocCount++;
        pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);
        mData = data;
        mObjects = objects;
        mDataSize = (mDataSize < desired) ? mDataSize : desired;
        ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
        mDataCapacity = desired;
        mObjectsSize = mObjectsCapacity = objectsSize;
        mNextObjectHint = 0;
        mObjectsSorted = false;
    } else if (mData) {
        if (objectsSize < mObjectsSize) {
            // Need to release refs on any objects we are dropping.
            const sp<ProcessState> proc(ProcessState::self());
            for (size_t i=objectsSize; i<mObjectsSize; i++) {
                const flat_binder_object* flat
                    = reinterpret_cast<flat_binder_object*>(mData+mObjects[i]);
                if (flat->hdr.type == BINDER_TYPE_FD) {
                    // will need to rescan because we may have lopped off the only FDs
                    mFdsKnown = false;
                }
                release_object(proc, *flat, this, &mOpenAshmemSize);
            }
            if (objectsSize == 0) {
                free(mObjects);
                mObjects = nullptr;
                mObjectsCapacity = 0;
            } else {
                binder_size_t* objects =
                    (binder_size_t*)realloc(mObjects, objectsSize*sizeof(binder_size_t));
                if (objects) {
                    mObjects = objects;
                    mObjectsCapacity = objectsSize;
                }
            }
            mObjectsSize = objectsSize;
            mNextObjectHint = 0;
            mObjectsSorted = false;
        }
        // We own the data, so we can just do a realloc().
        if (desired > mDataCapacity) {
            uint8_t* data = (uint8_t*)realloc(mData, desired);
            if (data) {
                LOG_ALLOC("Parcel %p: continue from %zu to %zu capacity", this, mDataCapacity,
                        desired);
                pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
                gParcelGlobalAllocSize += desired;
                gParcelGlobalAllocSize -= mDataCapacity;
                pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);
                mData = data;
                mDataCapacity = desired;
            } else {
                mError = NO_MEMORY;
                return NO_MEMORY;
            }
        } else {
            if (mDataSize > desired) {
                mDataSize = desired;
                ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
            }
            if (mDataPos > desired) {
                mDataPos = desired;
                ALOGV("continueWrite Setting data pos of %p to %zu", this, mDataPos);
            }
        }
    } else {
        // This is the first data.  Easy!
        uint8_t* data = (uint8_t*)malloc(desired);
        if (!data) {
            mError = NO_MEMORY;
            return NO_MEMORY;
        }
        if(!(mDataCapacity == 0 && mObjects == nullptr
             && mObjectsCapacity == 0)) {
            ALOGE("continueWrite: %zu/%p/%zu/%zu", mDataCapacity, mObjects, mObjectsCapacity, desired);
        }
        LOG_ALLOC("Parcel %p: allocating with %zu capacity", this, desired);
        pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
        gParcelGlobalAllocSize += desired;
        gParcelGlobalAllocCount++;
        pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);
        mData = data;
        mDataSize = mDataPos = 0;
        ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
        ALOGV("continueWrite Setting data pos of %p to %zu", this, mDataPos);
        mDataCapacity = desired;
    }
    return NO_ERROR;
}

我们再来看看读取数据

status_t Parcel::readAligned(T *pArg) const {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
    if ((mDataPos+sizeof(T)) <= mDataSize) {//
数据大小正常
        if (mObjectsSize > 0) {
            status_t err = validateReadData(mDataPos + sizeof(T));//
校验读取的数据
            if(err != NO_ERROR) {
                // Still increment the data position by the expected length
                mDataPos += sizeof(T);//
读取错误数据照常指针后移,不妨碍下一个读取
                return err;
            }
        }
        const void* data = mData+mDataPos;//
读取数据的指针
        mDataPos += sizeof(T);
        *pArg =  *reinterpret_cast<const T*>(data);//
reinterpret_cast会逐个比特的复制T数据类型同等长度的数据,超长部分不会读取。
        return NO_ERROR;
    } else {
        return NOT_ENOUGH_DATA;
    }
}

status_t Parcel::validateReadData(size_t upperBound) const
{
    // Don't allow non-object reads on object data
    if (mObjectsSorted || mObjectsSize <= 1) {
data_sorted:
        // Expect to check only against the next object
        if (mNextObjectHint < mObjectsSize && upperBound > mObjects[mNextObjectHint]) {
            // For some reason the current read position is greater than the next object
            // hint. Iterate until we find the right object
            size_t nextObject = mNextObjectHint;
            do {
                if (mDataPos < mObjects[nextObject] + sizeof(flat_binder_object)) {
                    // Requested info overlaps with an object
                    ALOGE("Attempt to read from protected data in Parcel %p", this);
                    return PERMISSION_DENIED;
                }
                nextObject++;
            } while (nextObject < mObjectsSize && upperBound > mObjects[nextObject]);
            mNextObjectHint = nextObject;
        }
        return NO_ERROR;
    }
    // Quickly determine if mObjects is sorted.
    binder_size_t* currObj = mObjects + mObjectsSize - 1;
    binder_size_t* prevObj = currObj;
    while (currObj > mObjects) {
        prevObj--;
        if(*prevObj > *currObj) {
            goto data_unsorted;
        }
        currObj--;
    }
    mObjectsSorted = true;
    goto data_sorted;
data_unsorted:
    // Insertion Sort mObjects
    // Great for mostly sorted lists. If randomly sorted or reverse ordered mObjects become common,
    // switch to std::sort(mObjects, mObjects + mObjectsSize);
    for (binder_size_t* iter0 = mObjects + 1; iter0 < mObjects + mObjectsSize; iter0++) {
        binder_size_t temp = *iter0;
        binder_size_t* iter1 = iter0 - 1;
        while (iter1 >= mObjects && *iter1 > temp) {
            *(iter1 + 1) = *iter1;
            iter1--;
        }
        *(iter1 + 1) = temp;
    }
    mNextObjectHint = 0;
    mObjectsSorted = true;
    goto data_sorted;
}

结论:

我们大致可以知道 C层里面 Parcel 结构体 的 mData用来实际存储数据,是实际存储数据内存的指针。mDataPos是当前已经存储的数据量,mDataCapacity是当前已经动态分配的内存容量。

所以实际上 Parcel 通过 创建Parcel 结构体来 赋予 Java 层 Parcel 一个可以调用结构体的指针。并且封装了一系列的Java 方法来操控 mData上存储的数据。

当写入数据的时候,根据当前调用的方法类型写入数据,指针向后偏移该类型的数据长度(对齐写入)。所以写入是按调用顺序逐个存入当前调用数据类型的长度。而读取也是一样,按照读取方法的调用顺序,逐个读取当前数据类型长度值个数据,指针往后偏移。

所以例子中的错误可以用示意图表示为

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闽农qq:994955138

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值