Android Binder 驱动 - Media 服务启动流程

到现在为止我对Binder 的了解只是知道它很难和它是 Android 系统中最核心的跨进程通信机制,仅此而已,所以接下来几篇文章都是学习的Android Binder 驱动的。

以安卓 12 源码为基础分析

相关文件:

frameworks/av/media/mediaserver/mediaserver.rc

frameworks/av/media/mediaserver/main_mediaserver.cpp

frameworks/native/libs/binder/ProcessState.cpp

frameworks/native/libs/binder/IServiceManager.cpp

frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp

frameworks/native/libs/binder/BpBinder.cpp

frameworks/native/libs/binder/ndk/service_manager.cpp

frameworks/native/libs/binder/IPCThreadState.cpp

我们关注的是:一个系统级 Native 服务(mediaserver)如何被创建,并通过 Binder 机制向系统注册其提供的服务

主线:

markdown 复制代码
1. 内核启动 → 加载 Binder 驱动
   ↓
2. init 进程启动 → 解析 .rc 文件
   ↓
3. 启动 mediaserver 进程(fork + exec)
   ↓
4. mediaserver 初始化 ProcessState 和 IPCThreadState
   ↓
5. 实例化并注册媒体服务(如 MediaPlayerService)
   ↓
6. 服务通过 Binder 被 servicemanager 管理
   ↓
7. 其他进程可通过 Binder 获取服务代理

在开始之前我们要先简单了解下什么是Binder,首先我们都知道它可以用来跨进程通信,那么对跨进程该怎么理解呢:

每个 Android 应用通常运行在自己独立的进程中,拥有自己独立的内存空间。一个进程不能直接访问另一个进程的内存。当应用需要与其他应用或系统服务交互时,就必须通过 IPC 机制。

这个很简单,但是Binder 是如何实现跨进程通信的呢?

Binder 的架构基于客户端-服务器(Client-Server)模型 。它的核心思想是:虽然不能直接访问对方的内存,但可以通过内核空间(一个所有进程都能共享的区域)来间接地完成数据交换和方法调用。

这个也很好理解,Binder 就相当于做了各个进程之间的中转工作,和路由器的原理有点像,对客户端来说,你只需要调用一个简单的方法,Binder 就可以帮你完成跨进程的通信任务,我们对Binder 的理解先到这,先不去深究其实现。

另外 Binder 只需要一次数据拷贝(从用户空间到内核空间),相较于其他 IPC 机制性能更高,这也是安卓选它跨进程通信的原因之一!


Binder 驱动初始化(内核层)

  • 内核启动时加载 binder 模块。
  • 创建设备节点 /dev/binder
  • 提供 openmmapioctl 系统调用接口。

所有使用 Binder 的进程都必须先 open("/dev/binder")

mmap 映射一块内存用于 Binder 数据传输(减少拷贝)。

驱动已就绪!

init 解析 mediaserver.rc

frameworks/av/media/mediaserver/mediaserver.rc

csharp 复制代码
on property:init.svc.media=*
    setprop init.svc.mediadrm ${init.svc.media}

service media /system/bin/mediaserver
    class main
    user media
    group audio camera inet net_bt net_bt_admin net_bw_acct drmrpc mediadrm
    ioprio rt 4
    task_profiles ProcessCapacityHigh HighPerformance

init 进程 在解析 init.rc 时会启动一个名为 mediaserver 的守护进程。

/system/bin/mediaserver 入口

frameworks/av/media/mediaserver/main_mediaserver.cpp

cpp 复制代码
int main(int argc __unused, char **argv __unused)
{
    signal(SIGPIPE, SIG_IGN);

    // 获取 ProcessState 单例(打开 /dev/binder,mmap)
    sp<ProcessState> proc(ProcessState::self());
    // 获取 ServiceManager 代理
    sp<IServiceManager> sm(defaultServiceManager());
    ALOGI("ServiceManager: %p", sm.get());
    // 实例化核心媒体服务
    MediaPlayerService::instantiate();
    // 媒体资源管理
    ResourceManagerService::instantiate();
    // 注册可选的媒体扩展服务(厂商定制或第三方插件)
    registerExtensions();
    ::android::hardware::configureRpcThreadpool(16, false);
    // 启动 Binder 线程池
    ProcessState::self()->startThreadPool();
    // 当前线程加入线程池,等待 Binder 请求
    IPCThreadState::self()->joinThreadPool();
    ::android::hardware::joinRpcThreadpool();
}

sp<...> 是 Android 的强引用智能指针(StrongPointer),用于自动管理 Binder 对象生命周期。

sp<ProcessState> proc(ProcessState::self());

执行 sp<ProcessState> proc(ProcessState::self());来到ProcessState.cpp 中:

frameworks/native/libs/binder/ProcessState.cpp

执行代码:

cpp 复制代码
sp<ProcessState> ProcessState::self() {
    return init(kDefaultDriver, false /*requireDefault*/);
}


sp<ProcessState> ProcessState::init(const char *driver, bool requireDefault) {
    static sp<ProcessState> gProcess;
    static std::mutex gProcessMutex;
    static std::once_flag gProcessOnce;

    if (driver == nullptr) {
        std::lock_guard<std::mutex> l(gProcessMutex);
        return gProcess;
    }

    std::call_once(gProcessOnce, [&](){
        if (access(driver, R_OK) == -1) {
            ALOGE("Binder driver %s is unavailable. Using /dev/binder instead.", driver);
            // 如果传入的 driver 设备文件不可访问,则退回 /dev/binder。
            driver = "/dev/binder";
        }
        std::lock_guard<std::mutex> l(gProcessMutex);
        // make 就是执行构造函数,等价于:new ProcessState("/dev/binder");
        // make 属于sp 作用域,具体可以看 system/core/libutils/include/utils/StrongPointer.h
        gProcess = sp<ProcessState>::make(driver);
    });

    if (requireDefault) {
        LOG_ALWAYS_FATAL_IF(gProcess->getDriverName() != driver,
            "ProcessState was already initialized with %s, can't initialize with %s.",
            gProcess->getDriverName().c_str(), driver);
    }

    return gProcess;
}

构造函数 ProcessState::ProcessState(const char* driver)

cpp 复制代码
ProcessState::ProcessState(const char *driver)
    : mDriverName(String8(driver)) // 保存驱动路径到 mDriverName
    , mDriverFD(open_driver(driver)) // 打开驱动 → 调用 open_driver(driver) 返回 mDriverFD
    , mVMStart(MAP_FAILED)
    , mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
    , mThreadPoolStarted(false)
    , mThreadPoolSeq(1)
    , mCallRestriction(CallRestriction::NONE)
{
    if (mDriverFD >= 0) {
        // 内存映射 (mmap) → 分配一块虚拟内存空间给 Binder 事务缓冲区
        mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ,
                        MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            ALOGE("Using %s failed: unable to mmap transaction memory.\n",
                   mDriverName.c_str());
            // 没有足够空间分配给 /dev/binder, 则关闭驱动
            close(mDriverFD);
            mDriverFD = -1;
            mDriverName.clear();
        }
    }

    LOG_ALWAYS_FATAL_IF(mDriverFD < 0,
        "Binder driver '%s' could not be opened.  Terminating.", driver);
}

mmap:

  • 作用:给 Binder 驱动分配一块进程虚拟内存,专门用来存放事务数据(Parcel 数据 buffer)。
  • Binder 驱动通过 copy_from_user/copy_to_user 在内核和用户空间之间交换数据。

打开驱动:

cpp 复制代码
static int open_driver(const char *driver)
{
    // 打开驱动,得到一个文件描述符 → Binder 驱动的通信入口
    int fd = open(driver, O_RDWR | O_CLOEXEC);
    if (fd >= 0) {
        int vers = 0;
        // 确认内核和用户空间协议版本一致
        status_t result = ioctl(fd, BINDER_VERSION, &vers);
        if (result == -1) {
            ALOGE("Binder ioctl to obtain version failed: %s", strerror(errno));
            close(fd);
            fd = -1;
        }
        if (result != 0 || vers != BINDER_CURRENT_PROTOCOL_VERSION) {
          ALOGE("Binder driver protocol(%d) does not match user space protocol(%d)! ioctl() return value: %d",
                vers, BINDER_CURRENT_PROTOCOL_VERSION, result);
            close(fd);
            fd = -1;
        }
        size_t maxThreads = DEFAULT_MAX_BINDER_THREADS;
        // 告诉内核这个进程最多可以有多少个 Binder 线程(默认 15)
        result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
        if (result == -1) {
            ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno));
        }
        uint32_t enable = DEFAULT_ENABLE_ONEWAY_SPAM_DETECTION;
        // 开启单向调用滥用检测(性能保护)
        result = ioctl(fd, BINDER_ENABLE_ONEWAY_SPAM_DETECTION, &enable);
        if (result == -1) {
            ALOGD("Binder ioctl to enable oneway spam detection failed: %s", strerror(errno));
        }
    } else {
        ALOGW("Opening '%s' failed: %s\n", driver, strerror(errno));
    }
    return fd;
}

open 是打开驱动、mmap 是映射驱动、ioctl 是操作驱动、close 是关闭驱动,分别对应以下函数:

  • binder_open():打开 Binder 设备(每个进程调用一次)
  • binder_mmap():映射内核缓冲区到用户空间(用于高效数据传输)
  • binder_ioctl():处理 Binder 通信命令(如 BC_TRANSACTION)
  • binder_colse:关闭驱动

sp<IServiceManager> sm(defaultServiceManager())

获取系统的 ServiceManager(服务的大管家),用来向 Binder 驱动注册/查询服务。

cpp 复制代码
sp<IServiceManager> defaultServiceManager()
{
    // call_once:确保里面的 lambda 函数 只执行一次,即使多线程调用 defaultServiceManager()
    // 这是 C++ 中实现单例的经典模式
    std::call_once(gSmOnce, []() {
        sp<AidlServiceManager> sm = nullptr;
        while (sm == nullptr) {
            // 获取 Binder 驱动为每个进程自动提供的 "上下文管理者"(context manager) 的 Binder 引用。
            // 这个"上下文管理者"其实就是 /dev/vndbinder 或 /dev/binder 上的 servicemanager 进程 的代理。
            // 将原始的 IBinder 接口(BpBinder(0))转换为 AidlServiceManager 接口
            sm = interface_cast<AidlServiceManager>(ProcessState::self()->getContextObject(nullptr));
            if (sm == nullptr) {
                ALOGE("Waiting 1s on context object on %s.", ProcessState::self()->getDriverName().c_str());
                // 因为 servicemanager 进程可能 还没有启动完成,等待一秒
                sleep(1);
            }
        }

        // 兼容层(Shim Layer)
        // 在 Android 10 及之前:servicemanager 使用 HIDL 定义
        // Android 11+ 开始:Google 将 servicemanager 重构为使用 AIDL
        gDefaultServiceManager = sp<ServiceManagerShim>::make(sm);
    });

    return gDefaultServiceManager;
}

getContextObject(nullptr):

cpp 复制代码
sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
    // 传入 handle = 0,这是整个 Binder 架构中唯一硬编码的句柄
    // handle=0 固定代表 ServiceManager
    // 所有进程都知道:要找 ServiceManager,就去拿 handle=0 的代理。
    sp<IBinder> context = getStrongProxyForHandle(0);

    if (context) {
        // The root object is special since we get it directly from the driver, it is never
        // written by Parcell::writeStrongBinder.
        internal::Stability::markCompilationUnit(context.get());
    } else {
        ALOGW("Not able to get context object on %s.", mDriverName.c_str());
    }

    return context;
}
cpp 复制代码
sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;

    // mLock 是 ProcessState 的互斥锁
    AutoMutex _l(mLock);

    // struct handle_entry {
    // IBinder* binder;        // 对应的代理对象(BpBinder)
    // weakref_type* refs;     // 弱引用计数器
    // };

    //lookupHandleLocked 维护一个数组:mHandleToObject(Vector<handle_entry>)
    // 所有 handle 的映射都集中管理,避免重复创建代理。
    handle_entry* e = lookupHandleLocked(handle);

    if (e != nullptr) {

        IBinder* b = e->binder;
        if (b == nullptr || !e->refs->attemptIncWeak(this)) {
             // 代理不存在 或 弱引用已失效 → 需要重建
            if (handle == 0) {

                IPCThreadState* ipc = IPCThreadState::self();

                CallRestriction originalCallRestriction = ipc->getCallRestriction();
                ipc->setCallRestriction(CallRestriction::NONE);

                Parcel data;
                // 即使 handle=0 存在,也不能保证 servicemanager 进程还活着
                // 发送一个 PING_TRANSACTION 事务,如果返回 DEAD_OBJECT:说明 servicemanager 死了,不能返回代理
                status_t status = ipc->transact(
                        0, IBinder::PING_TRANSACTION, data, nullptr, 0);

                ipc->setCallRestriction(originalCallRestriction);

                if (status == DEAD_OBJECT)
                   return nullptr;
            }

            // 创建一个 BpBinder 对象,封装 handle
            // BpBinder 是 Binder 代理的基类
            // 所有跨进程调用最终都通过 BpBinder::transact() 发送到内核
            sp<BpBinder> b = BpBinder::create(handle);
            e->binder = b.get();
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
            // 代理存在且有效 → 直接返回强引用
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }

    return result;
}

流程图:

scss 复制代码
getStrongProxyForHandle(handle)
  ↓
加锁 mLock
  ↓
lookupHandleLocked(handle) → 获取 handle_entry
  ↓
是否有有效代理?
  ├─ 是 → attemptIncWeak → 成功? → 返回强引用
  └─ 否 → 
        handle == 0? → 是 → 发送 PING_TRANSACTION 检查存活
                             → 失败? → 返回 nullptr
        创建 BpBinder(handle)
        保存到 handle_entry
        返回 sp<BpBinder>

这样就拿到了 ServiceManager 的 Binder 代理对象。


MediaPlayerService::instantiate();

启动并向 ServiceManager 注册 MediaPlayerService

frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp

cpp 复制代码
void MediaPlayerService::instantiate() {
    defaultServiceManager()->addService(
            String16("media.player"), new MediaPlayerService());
}

因为 defaultServiceManager() 返回的是 sp<IServiceManager> 对象, 所以可以在IServiceManager 中找到下面方法:

cpp 复制代码
using AidlServiceManager = android::os::IServiceManager;
//...
class ServiceManagerShim : public IServiceManager

//...

status_t ServiceManagerShim::addService(const String16& name, const sp<IBinder>& service,
                                        bool allowIsolated, int dumpsysPriority)
{
    Status status = mTheRealServiceManager->addService(
        String8(name).c_str(), service, allowIsolated, dumpsysPriority);
    return status.exceptionCode();
}

mTheRealServiceManager 是一个 AIDL 接口的客户端代理 ,指向真正的 AIDL 版 servicemanager 服务。

此处的 addService 就对应了 frameworks/base/core/java/android/os/ServiceManager.java 中的 addService 方法,代表着 Java 和 Native 都可以独立地向 servicemanager 注册服务:

java 复制代码
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static void addService(String name, IBinder service, boolean allowIsolated,
            int dumpPriority) {
        try {
            getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
        } catch (RemoteException e) {
            Log.e(TAG, "error in addService", e);
        }
    }

ResourceManagerService::instantiate();

启动并注册 ResourceManagerService,用于统一管理多媒体硬件资源(解码器、音视频硬件编解码能力)

cpp 复制代码
void ResourceManagerService::instantiate() {
    // 创建 ResourceManagerService 实例(使用 NDK AIDL 模式)
    std::shared_ptr<ResourceManagerService> service =
            ::ndk::SharedRefBase::make<ResourceManagerService>();
    // 将服务注册到 servicemanager
    binder_status_t status =
            AServiceManager_addService(service->asBinder().get(), getServiceName());
    // 如果失败就直接返回,不继续初始化
    if (status != STATUS_OK) {
        return;
    }

    // 创建并设置观察者服务
    std::shared_ptr<ResourceObserverService> observerService =
            ResourceObserverService::instantiate();

    // 使用观察者模式解耦资源管理和状态通知。
    if (observerService != nullptr) {
        service->setObserverService(observerService);
    }
    // TODO: mediaserver main() is already starting the thread pool,
    // move this to mediaserver main() when other services in mediaserver
    // are converted to ndk-platform aidl.
    //ABinderProcess_startThreadPool();
}

这段代码的作用是: 使用现代 NDK AIDL 架构,创建并注册 ResourceManagerService 服务到 servicemanager,同时设置其依赖的观察者服务,为 mediaserver 提供资源管理能力。

其中AServiceManager_addService 代码实现在 service_manager.cpp :

frameworks/native/libs/binder/ndk/service_manager.cpp

cpp 复制代码
binder_exception_t AServiceManager_addService(AIBinder* binder, const char* instance) {
    if (binder == nullptr || instance == nullptr) {
        return EX_ILLEGAL_ARGUMENT;
    }

    sp<IServiceManager> sm = defaultServiceManager();
    status_t exception = sm->addService(String16(instance), binder->getBinder());
    return PruneException(exception);
}

IPCThreadState::self()->joinThreadPool();

将当前线程变成一个可处理传入和传出 Binder 请求的"Binder 线程",并进入一个无限循环,等待并执行来自其他进程的 Binder 调用。

关键源码:

cpp 复制代码
void IPCThreadState::joinThreadPool(bool isMain)
{
    status_t result;
    mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);

    do {
        processPendingDerefs();
        // 等待并处理来自 Binder 驱动的消息
        result = getAndExecuteCommand();
    } while (result != -ECONNREFUSED && result != -EBADF);

    mOut.writeInt32(BC_EXIT_LOOPER);
    talkWithDriver(false);
}

与驱动进行通信:

cpp 复制代码
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    if (mProcess->mDriverFD < 0) {
        return -EBADF;
    }

    binder_write_read bwr;

    const bool needRead = mIn.dataPosition() >= mIn.dataSize();

    const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;

    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();

    // This is what we'll read.
    if (doReceive && needRead) {
        // 接收数据缓冲区信息的填充。如果以后收到数据,就直接填在mIn中了。
        bwr.read_size = mIn.dataCapacity();
        bwr.read_buffer = (uintptr_t)mIn.data();
    } else {
        bwr.read_size = 0;
        bwr.read_buffer = 0;
    }

    IF_LOG_COMMANDS() {
        TextOutput::Bundle _b(alog);
        if (outAvail != 0) {
            alog << "Sending commands to driver: " << indent;
            const void* cmds = (const void*)bwr.write_buffer;
            const void* end = ((const uint8_t*)cmds)+bwr.write_size;
            alog << HexDump(cmds, bwr.write_size) << endl;
            while (cmds < end) cmds = printCommand(alog, cmds);
            alog << dedent;
        }
        alog << "Size of receive buffer: " << bwr.read_size
            << ", needRead: " << needRead << ", doReceive: " << doReceive << endl;
    }

    // Return immediately if there is nothing to do.
    if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR;

    bwr.write_consumed = 0;
    bwr.read_consumed = 0;
    status_t err;
    do {
        IF_LOG_COMMANDS() {
            alog << "About to read/write, write size = " << mOut.dataSize() << endl;
        }
#if defined(__ANDROID__)
        // 通过ioctl不停的读写操作,跟Binder Driver进行通信
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
#else
        err = INVALID_OPERATION;
#endif
        if (mProcess->mDriverFD < 0) {
            err = -EBADF;
        }
        IF_LOG_COMMANDS() {
            alog << "Finished read/write, write size = " << mOut.dataSize() << endl;
        }
    } while (err == -EINTR);

    //...
    return err;
}

这里提一下 binder_write_read 可以把它理解为一个 双向信封

  • 一边装着 我要告诉驱动什么(write)
  • 一边留着 驱动要告诉我什么(read)
write_size/buffer 用户 → 内核 发送命令(BC_TRANSACTION等)
read_size/buffer 内核 → 用户 接收事件(BR_TRANSACTION等)
write/read_consumed 内核 → 用户 告知处理进度

上面我们说过 所有跨进程调用最终都通过 BpBinder::transact() 发送到内核

cpp 复制代码
sp<BpBinder> b = BpBinder::create(handle);

BpBinder 在 sp ProcessState::getStrongProxyForHandle 中被创建

cpp 复制代码
status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    LOG_ALWAYS_FATAL_IF(data.isForRpc(), "Parcel constructed for RPC, but being used with binder.");

    status_t err;

    // ...
    waitForResponse(nullptr, nullptr);

    //...
    return err;
}

waitForResponse:

cpp 复制代码
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    uint32_t cmd;
    int32_t err;

    while (1) {
        // 调用 talkWithDriver 
        if ((err=talkWithDriver()) < NO_ERROR) break;
        err = mIn.errorCheck();
        if (err < NO_ERROR) break;
        if (mIn.dataAvail() == 0) continue;

        cmd = (uint32_t)mIn.readInt32();

        IF_LOG_COMMANDS() {
            alog << "Processing waitForResponse Command: "
                << getReturnString(cmd) << endl;
        }

        switch (cmd) {
        case BR_ONEWAY_SPAM_SUSPECT:
            //..
        case BR_TRANSACTION_COMPLETE:
             //..
        case BR_DEAD_REPLY:
             //..
        case BR_FAILED_REPLY:
            err = FAILED_TRANSACTION;
            goto finish;

        case BR_FROZEN_REPLY:
            err = FAILED_TRANSACTION;
            goto finish;

        case BR_ACQUIRE_RESULT:
             //..
        case BR_REPLY:
             //..
        default:
            err = executeCommand(cmd);
            if (err != NO_ERROR) goto finish;
            break;
        }
    }

finish:
    if (err != NO_ERROR) {
        if (acquireResult) *acquireResult = err;
        if (reply) reply->setError(err);
        mLastError = err;
    }

    return err;
}

客户端通过 transact 发送消息, 调用 talkWithDriver 监听返回消息,至此形成闭环。


最后

本人能力有限,关于具体函数的介绍只能停留在表面上,分析不到之处还请不吝赐教,啊,头皮发麻...

相关推荐
Jerry1 小时前
Compose 为元素赋予动画特效
android
Jeled2 小时前
协程工具类
android·android studio
阿兰哥5 小时前
【调试篇5】TransactionTooLargeException 原理解析
android·性能优化·源码
爱吃水蜜桃的奥特曼6 小时前
玩Android Flutter版本,通过项目了解Flutter项目快速搭建开发
android·flutter
太过平凡的小蚂蚁6 小时前
Android 版本特性完全解析:从6.0到16.0的实用指南
android
杨筱毅6 小时前
【底层机制】【Android】深入理解UI体系与绘制机制
android·底层机制
介一安全7 小时前
【Frida Android】基础篇8:Java层Hook基础——调用带对象参数的方法
android·网络安全·逆向·安全性测试·frida
puyaCheer7 小时前
Android 13 启动的时候会显示一下logo,很不友好
android·gitee
long_hai_d8 小时前
Aosp14桌面壁纸和锁屏壁纸的设置和加载分析
android
2501_916007479 小时前
iOS 26 软件性能测试 新版系统下评估全流程 + 多工具辅助方案
android·macos·ios·小程序·uni-app·cocoa·iphone