美文网首页
[008]Cursor引发的一系列思考

[008]Cursor引发的一系列思考

作者: 王小二的技术栈 | 来源:发表于2019-05-29 10:07 被阅读0次

    前言

    [007]一次Binder通信最大可以传输多大的数据?这个文章,我得到了一个结论,就是正常情况下一次Binder通信最大可以传输的数据的大小是1MB-8KB。突然想到我们在通过ContentResolver对象调用ContentProvider的调用query返回Cursor的时候,本质上这是一次Binder通信,那这个Cursor对象大小有没有限制呢?是不是也要小于1MB-8KB?

    Cursor的实现原理

    rameworks/base/libs/androidfw/CursorWindow.cpp

    status_t CursorWindow::create(const String8& name, size_t size, CursorWindow** outCursorWindow) {
        String8 ashmemName("CursorWindow: ");
        ashmemName.append(name);
    
        status_t result;
        //调用ashmem_create_region创建一个共享内存
        int ashmemFd = ashmem_create_region(ashmemName.string(), size);
        if (ashmemFd < 0) {
            result = -errno;
        } else {
            //设置匿名共享内存可读可写
            result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE);
            if (result >= 0) {
                //调用mmap进行内存映射
                void* data = ::mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0);
                if (data == MAP_FAILED) {
                    result = -errno;
                } else {
                    result = ashmem_set_prot_region(ashmemFd, PROT_READ);
                    if (result >= 0) {
                        CursorWindow* window = new CursorWindow(name, ashmemFd,
                                data, size, false /*readOnly*/);
                      //省略部分代码
        return result;
    }
    
    //序列化只传递两个数据1.mName 2. mAshmemFd
    status_t CursorWindow::writeToParcel(Parcel* parcel) {
        status_t status = parcel->writeString8(mName);//一个名字
        if (!status) {
            status = parcel->writeDupFileDescriptor(mAshmemFd);//匿名共享内存的文件描述FD
        }
        return status;
    }
    

    从代码来看Cursor的真实实现应该是CursorWindow.cpp,CursorWindow的Data数据真实实现是匿名共享内存,在序列化到Parcel的时候,只需要传递String和匿名共享内存的FD就好了。
    这样子看来Cursor的大小是不受限制的,不懂匿名共享内存的可以先看一下[006]匿名共享内存(Ashmem)的使用

    其实在Android Framework中对此有一定的限制,请注意在CursorWindow::create会传递一个size的参数。
    frameworks/base/core/java/android/database/CursorWindow.java

        public CursorWindow(String name) {
            this(name, getCursorWindowSize());
        }
    
        public CursorWindow(String name, @BytesLong long windowSizeBytes) {
            mStartPos = 0;
            mName = name != null && name.length() != 0 ? name : "<unnamed>";
            //windowSizeBytes默认是getCursorWindowSize的返回值就是2048*1024
            mWindowPtr = nativeCreate(mName, (int) windowSizeBytes);
            if (mWindowPtr == 0) {
                throw new CursorWindowAllocationException("Cursor window allocation of " +
                        windowSizeBytes + " bytes failed. " + printStats());
            }
            mCloseGuard.open("close");
            recordNewWindow(Binder.getCallingPid(), mWindowPtr);
        }
    
        private static int getCursorWindowSize() {
            if (sCursorWindowSize < 0) {
                //<integer name="config_cursorWindowSize">2048</integer>
                sCursorWindowSize = Resources.getSystem().getInteger(
                        com.android.internal.R.integer.config_cursorWindowSize) * 1024;
            }
            return sCursorWindowSize;
        }
    

    总结:

    Cursor的Data区域是基于匿名共享内存实现的,所以Binder进程传递的Cursor对象,本质上就是一个String和FD(根本不用担心超出Binder的1MB-8KB的限制导致异常),但是这个匿名共享内存的大小是有限制的,安卓系统中Cursor的data匿名共享内存的大小限制是2MB。当然可以通过调用public CursorWindow(String name, @BytesLong long windowSizeBytes)来设置Cursor的Data区域大小

    意外发现Parcel.cpp中的writeBlob方法

    frameworks/native/libs/binder/Parcel.cpp

    // Maximum size of a blob to transfer in-place.
    static const size_t BLOB_INPLACE_LIMIT = 16 * 1024;
    
    status_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob* outBlob)
    {
        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;
        }
    
        status_t status;
        //如果mAllowFds为false或者len小于等于16kb,使用普通的方式保存数据
        if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {
            ALOGV("writeBlob: write in place");
            status = writeInt32(BLOB_INPLACE);
            if (status) return status;
    
            void* ptr = writeInplace(len);
            if (!ptr) return NO_MEMORY;
    
            outBlob->init(-1, ptr, len, false);
            return NO_ERROR;
        }
        //使用匿名共享内存的方式保存数据
        ALOGV("writeBlob: write to ashmem");
        int fd = ashmem_create_region("Parcel Blob", len);
        if (fd < 0) return NO_MEMORY;
        //创建可读可写的匿名共享内存fd
        int result = ashmem_set_prot_region(fd, PROT_READ | PROT_WRITE);
        if (result < 0) {
            status = result;
        } else {
            //申请物理内存
            void* ptr = ::mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
            if (ptr == MAP_FAILED) {
                status = -errno;
            } else {
                if (!mutableCopy) {
                    result = ashmem_set_prot_region(fd, PROT_READ);
                }
                if (result < 0) {
                    status = result;
                } else {
                    //把数据写到申请的匿名共享内存上
                    status = writeInt32(mutableCopy ? BLOB_ASHMEM_MUTABLE : BLOB_ASHMEM_IMMUTABLE);
                    if (!status) {
                        status = writeFileDescriptor(fd, true /*takeOwnership*/);
                        if (!status) {
                            outBlob->init(fd, ptr, len, mutableCopy);
                            return NO_ERROR;
                        }
                    }
                }
            }
            ::munmap(ptr, len);
        }
        ::close(fd);
        return status;
    }
    

    frameworks/base/core/java/android/os/Parcel.java

        /** @hide */
        public final void restoreAllowFds(boolean lastValue) {
            nativeRestoreAllowFds(mNativePtr, lastValue);
        }
    
        /**
         * Write a blob of data into the parcel at the current {@link #dataPosition},
         * growing {@link #dataCapacity} if needed.
         * @param b Bytes to place into the parcel.
         * {@hide}
         * {@SystemApi}
         */
        public final void writeBlob(byte[] b) {
            writeBlob(b, 0, (b != null) ? b.length : 0);
        }
    
        /**
         * Write a blob of data into the parcel at the current {@link #dataPosition},
         * growing {@link #dataCapacity} if needed.
         * @param b Bytes to place into the parcel.
         * @param offset Index of first byte to be written.
         * @param len Number of bytes to write.
         * {@hide}
         * {@SystemApi}
         */
        public final void writeBlob(byte[] b, int offset, int len) {
            if (b == null) {
                writeInt(-1);
                return;
            }
            Arrays.checkOffsetAndCount(b.length, offset, len);
            nativeWriteBlob(mNativePtr, b, offset, len);
        }
    
    发现其实Parcel有隐藏接口,可以通过restoreAllowFds来让writeBlob的接口内部以匿名共享内存的方式存储数据。
    猜想:在Intent跳转另一个页面的时候,能否通过调用隐藏接口来进行大数据的传输?

    常规的操作

        private void test1() {
            Intent intent = new Intent(this, Main2Activity.class);
            byte[] bytes = new byte[1024*1024];//创建一个1M的数据
            intent.putExtra("kobe", bytes);
            this.startActivity(intent);
        }
    

    出现异常

    E AndroidRuntime: Caused by: android.os.TransactionTooLargeException: data parcel size 1048944 bytes
    E AndroidRuntime:   at android.os.BinderProxy.transactNative(Native Method)
    E AndroidRuntime:   at android.os.BinderProxy.transact(Binder.java:1127)
    E AndroidRuntime:   at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:3754)
    E AndroidRuntime:   at android.app.Instrumentation.execStartActivity(Instrumentation.java:1669)
    

    因为Binder调用的限制是1M-8K,我们传递一个1M的数据,肯定报错了。

    特殊操作:调用Parcel的隐藏接口

        private void test2() {
            Intent intent = new Intent(this, Main2Activity.class);
            Data data = new Data();
            data.bytes = new byte[1024*1024];//创建一个1M的数据,并保存在Data中的bytes
            intent.putExtra("kobe", data);//把一个Parcelable对象传进去
            this.startActivity(intent);
        }
    
    //Data实现了Parcelable接口
    public class Data implements Parcelable {
    
        public byte[] bytes;
    
        @Override
        public int describeContents() {
            return 0;
        }
        //序列化
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.restoreAllowFds(true);//设置Parcel的AllowFds为true
            dest.writeBlob(bytes);//把数据传递写到Parcel里面,存储方式是匿名共享内存
        }
    
        public Data() {
        }
        //反序列化
        protected Data(Parcel in) {
            this.bytes = in.readBlob();//把数据从Parcel中读出来
        }
    
        public static final Parcelable.Creator<Data> CREATOR = new Parcelable.Creator<Data>() {
            @Override
            public Data createFromParcel(Parcel source) {
                return new Data(source);
            }
    
            @Override
            public Data[] newArray(int size) {
                return new Data[size];
            }
        };
    }
    
    写完代码的我感觉非常好,程序跑起来,正准备宣布我的新发现,结果发现还是我太年轻了
    E AndroidRuntime: Caused by: java.lang.IllegalArgumentException: File descriptors passed in Intent
    E AndroidRuntime:   at android.os.Parcel.readException(Parcel.java:2009)
    E AndroidRuntime:   at android.os.Parcel.readException(Parcel.java:1951)
    E AndroidRuntime:   at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:4425)
    E AndroidRuntime:   at android.app.Instrumentation.execStartActivity(Instrumentation.java:1613)
    E AndroidRuntime:   at android.app.Activity.startActivityForResult(Activity.java:4501)
    E AndroidRuntime:   at android.app.Activity.startActivityForResult(Activity.java:4459)
    E AndroidRuntime:   at android.app.Activity.startActivity(Activity.java:4820)
    E AndroidRuntime:   at android.app.Activity.startActivity(Activity.java:4788)
    E AndroidRuntime:   at com.tct.blobtest.MainActivity.test2(MainActivity.java:28)
    E AndroidRuntime:   at com.tct.blobtest.MainActivity.onCreate(MainActivity.java:14)
    E AndroidRuntime:   at android.app.Activity.performCreate(Activity.java:7023)
    E AndroidRuntime:   at android.app.Activity.performCreate(Activity.java:7014)
    E AndroidRuntime:   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1215)
    E AndroidRuntime:   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2861)
    
    

    总结

    java.lang.IllegalArgumentException: File descriptors passed in Intent
    说明Intent中是无法传递FD的,当然无法通过匿名共享内存的方式,来通过Intent传递大的数据。其实android为什么要做这个限制,想想也很简单,如果可以通过Intent来传递FD,会导致程序中的FD泛滥成灾,一个应用的FD上限是1024,这样子应用太容易crash了。

    More

    通过最近的几次研究,发现匿名共享内存在android中使用还是非常多的,只是android的SDK基本帮我们封装好了,不希望我们直接操作匿名功能内存,毕竟一旦操作不好就容易FD泄露,匿名共享内存泄露。

    相关文章

      网友评论

          本文标题:[008]Cursor引发的一系列思考

          本文链接:https://www.haomeiwen.com/subject/umustctx.html