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

前言

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

推荐阅读更多精彩内容