Android 存储设备管理框架

在android之VOLD进程启动源码分析一文中介绍了存储设备的管控中心Vold进程,Vold属于native后台进程,通过netlink方式接收kernel的uevent消息,并通过socket方式将uevent消息发送给MountService,同时实时接收MountService的命令消息,MountService,Vold,Kernel三者的关系如下图所示:

Android Service之MountService源码分析_第1张图片

android之VOLD进程启动源码分析一文中介绍了NetlinkManager模块在启动过程中,创建了一个socket监听线程,用于监听kernel发送过来的uevent消息;CommandListener模块在启动时同样创建了一个socket监听线程,不同的是该线程用于监听MountServcie的连接,接收MountService向Vold发送的命令消息;MountService要接收来自kernel的uevent消息,必定也需要创建一个socket监听线程,在接下来将对该socket监听线程进行详细讲解。

Android MountService框架设计

MountService作为Android的Java服务之一,在SystemServer进程启动的第二阶段创建并注册到ServiceManager中,同时长驻于SystemServer进程中,MountService创建及注册过程如下:

MountService mountService = null;if (!"0".equals(SystemProperties.get("system_init.startmountservice"))) {try {/* * NotificationManagerService is dependant on MountService, * so we must start MountService first. */Slog.i(TAG, "Mount Service");mountService = new MountService(context);//注册到ServiceManager中ServiceManager.addService("mount", mountService);} catch (Throwable e) {reportWtf("starting Mount Service", e);}}

MountService各个类关系图:

Android Service之MountService源码分析_第2张图片

构造MountService对象实例:

public MountService(Context context) {mContext = context; //从xml中读取存储设备列表readStorageList();if (mPrimaryVolume != null) {mExternalStoragePath = mPrimaryVolume.getPath();mEmulateExternalStorage = mPrimaryVolume.isEmulated();if (mEmulateExternalStorage) {Slog.d(TAG, "using emulated external storage");mVolumeStates.put(mExternalStoragePath, Environment.MEDIA_MOUNTED);}}//add mount state for inernal storage in NANDif (Environment.getSecondStorageType() == Environment.SECOND_STORAGE_TYPE_NAND) {mVolumeStates.put(Environment.getSecondStorageDirectory().getPath(), Environment.MEDIA_MOUNTED);}// 查询PackageManagerService服务mPms = (PackageManagerService) ServiceManager.getService("package");IntentFilter filter = new IntentFilter();filter.addAction(Intent.ACTION_BOOT_COMPLETED);// don't bother monitoring USB if mass storage is not supported on our primary volumeif (mPrimaryVolume != null && mPrimaryVolume.allowMassStorage()) {filter.addAction(UsbManager.ACTION_USB_STATE);}//注册开机完成及USB状态变化广播接收器mContext.registerReceiver(mBroadcastReceiver, filter, null, null);    //创建并启动一个带消息循环的MountService工作线程mHandlerThread = new HandlerThread("MountService");mHandlerThread.start();//为MountService工作线程创建一个HandlermHandler = new MountServiceHandler(mHandlerThread.getLooper());//为MountService工作线程创建一个ObbActionHandlermObbActionHandler = new ObbActionHandler(mHandlerThread.getLooper());/* * Create the connection to vold with a maximum queue of twice the * amount of containers we'd ever expect to have. This keeps an * "asec list" from blocking a thread repeatedly. */mConnector = new NativeDaemonConnector(this, "vold", MAX_CONTAINERS * 2, VOLD_TAG, 25);    //创建并启动一个socket连接监听线程Thread thread = new Thread(mConnector, VOLD_TAG);thread.start();// Add ourself to the Watchdog monitors if enabled.if (WATCHDOG_ENABLE) {Watchdog.getInstance().addMonitor(this);}}

在开始构造MountService前,首先读取frameworks/base/core/res/res/xml/storage_list.xml文件,该文件以XML方式保存了所有存储设备的参数,文件内容如下所示:

        

该文件的读取这里不在介绍,读者自行研究,使用XML解析器读取该XML的文件内容,根据读取到的存储设备参数来构造StorageVolume对象,并将构造的所有StorageVolume对象存放到列表mVolumes中。通过源码清晰地知道,在构造MountService时,注册了一个广播接收器,用于接收开机完成广播及USB状态广播,当开机完成时自动挂载存储设备,在大容量设备存储有效情况下,当USB状态变化也自动地挂载存储设备。创建了两个工作线程,MountService线程用于消息循环处理,为什么要开启一个异步消息处理线程呢?我们知道大量的Java Service驻留在SystemServer进程中,如果所有的服务消息都发送到SystemServer的主线程中处理的话,主线程的负荷很重,消息不能及时得到处理,因此需为每一个Service开启一个消息处理线程,专门处理本Service的消息。如下图所示:

Android Service之MountService源码分析_第3张图片


MountService服务的线程模型

Android Service之MountService源码分析_第4张图片
从上图可以清晰地看出SystemServer主线程启动MountService服务,该服务启动时会创建一个MountService带有消息循环的工作线程,用于处理MountServiceHandle和ObbActionHandler分发过来的消息;同时创建一个用于连接Vold服务端socket的VoldConnector线程,该线程在进入闭环运行前会创建一个带有消息循环的VoldConnector.CallbackHandler线程,用于处理native层的Vold进程发送过来的uevent事件消息;然后向服务端Vold发送连接请求,得到socket连接后,从该socket中循环读取数据以接收来之服务端Vold的uevent消息,当读取的数据长度为0时,向服务端重新发起连接,如此循环,保证客户端MountService与服务端Vold一直保持正常连接。当成功连接到服务端Vold时,VoldConnector线程会创建一个MountService#onDaemonConnected线程,用于处理本次连接请求响应。

1.MountService线程创建

        mHandlerThread = new HandlerThread("MountService");        mHandlerThread.start();        mHandler = new MountServiceHandler(mHandlerThread.getLooper());        // Add OBB Action Handler to MountService thread.        mObbActionHandler = new ObbActionHandler(mHandlerThread.getLooper());
MountServiceHandler消息处理:
public void handleMessage(Message msg) {    switch (msg.what) {case H_UNMOUNT_PM_UPDATE: {    UnmountCallBack ucb = (UnmountCallBack) msg.obj;    mForceUnmounts.add(ucb);    // Register only if needed.    if (!mUpdatingStatus) {if (DEBUG_UNMOUNT) Slog.i(TAG, "Updating external media status on PackageManager");mUpdatingStatus = true;mPms.updateExternalMediaStatus(false, true);    }    break;}case H_UNMOUNT_PM_DONE: {    mUpdatingStatus = false;    int size = mForceUnmounts.size();    int sizeArr[] = new int[size];    int sizeArrN = 0;    // Kill processes holding references first    ActivityManagerService ams = (ActivityManagerService)    ServiceManager.getService("activity");    for (int i = 0; i < size; i++) {UnmountCallBack ucb = mForceUnmounts.get(i);String path = ucb.path;boolean done = false;if (!ucb.force) {    done = true;} else {    int pids[] = getStorageUsers(path);    if (pids == null || pids.length == 0) {done = true;    } else {// Eliminate system process here?ams.killPids(pids, "unmount media", true);// Confirm if file references have been freed.pids = getStorageUsers(path);if (pids == null || pids.length == 0) {    done = true;}    }}if (!done && (ucb.retries < MAX_UNMOUNT_RETRIES)) {    // Retry again    Slog.i(TAG, "Retrying to kill storage users again");    mHandler.sendMessageDelayed(mHandler.obtainMessage(H_UNMOUNT_PM_DONE,ucb.retries++),RETRY_UNMOUNT_DELAY);} else {    if (ucb.retries >= MAX_UNMOUNT_RETRIES) {Slog.i(TAG, "Failed to unmount media inspite of " +MAX_UNMOUNT_RETRIES + " retries. Forcibly killing processes now");    }    sizeArr[sizeArrN++] = i;    mHandler.sendMessage(mHandler.obtainMessage(H_UNMOUNT_MS,ucb));}    }    // Remove already processed elements from list.    for (int i = (sizeArrN-1); i >= 0; i--) {mForceUnmounts.remove(sizeArr[i]);    }    break;}case H_UNMOUNT_MS: {    UnmountCallBack ucb = (UnmountCallBack) msg.obj;    ucb.handleFinished();    break;}    }}


MountServiceHandler分别对H_UNMOUNT_PM_UPDATE,H_UNMOUNT_PM_DONE,H_UNMOUNT_MS
public void handleMessage(Message msg) {    switch (msg.what) {case OBB_RUN_ACTION: {    final ObbAction action = (ObbAction) msg.obj;    // If a bind was already initiated we don't really    // need to do anything. The pending install    // will be processed later on.    if (!mBound) {// If this is the only one pending we might// have to bind to the service again.if (!connectToService()) {    Slog.e(TAG, "Failed to bind to media container service");    action.handleError();    return;}    }    mActions.add(action);    break;}case OBB_MCS_BOUND: {    if (msg.obj != null) {mContainerService = (IMediaContainerService) msg.obj;    }    if (mContainerService == null) {for (ObbAction action : mActions) {    // Indicate service bind error    action.handleError();}mActions.clear();    } else if (mActions.size() > 0) {final ObbAction action = mActions.get(0);if (action != null) {    action.execute(this);}    } else {// Should never happen ideally.Slog.w(TAG, "Empty queue");    }    break;}case OBB_MCS_RECONNECT: {    if (mActions.size() > 0) {if (mBound) {    disconnectService();}if (!connectToService()) {    Slog.e(TAG, "Failed to bind to media container service");    for (ObbAction action : mActions) {// Indicate service bind erroraction.handleError();    }    mActions.clear();}    }    break;}case OBB_MCS_UNBIND: {    // Delete pending install    if (mActions.size() > 0) {mActions.remove(0);    }    if (mActions.size() == 0) {if (mBound) {    disconnectService();}    } else {// There are more pending requests in queue.// Just post MCS_BOUND message to trigger processing// of next pending install.mObbActionHandler.sendEmptyMessage(OBB_MCS_BOUND);    }    break;}case OBB_FLUSH_MOUNT_STATE: {    final String path = (String) msg.obj;    synchronized (mObbMounts) {final List obbStatesToRemove = new LinkedList();final Iterator> i =mObbPathToStateMap.entrySet().iterator();while (i.hasNext()) {    final Entry obbEntry = i.next();    if (obbEntry.getKey().startsWith(path)) {obbStatesToRemove.add(obbEntry.getValue());    }}for (final ObbState obbState : obbStatesToRemove) {    removeObbStateLocked(obbState);    try {obbState.token.onObbResult(obbState.filename, obbState.nonce,OnObbStateChangeListener.UNMOUNTED);    } catch (RemoteException e) {Slog.i(TAG, "Couldn't send unmount notification for  OBB: "+ obbState.filename);    }}    }    break;}    }}



MountService命令下发流程

Vold作为存储设备的管控中心,需要接收来自上层MountService的操作命令,MountService驻留在SystemServer进程中,和Vold作为两个不同的进程,它们之间的通信方式采用的是socket通信,在 android之VOLD进程启动源码分析一文中介绍了 CommandListener模块启动了一个socket监听线程,用于专门接收来之上层MountService的连接请求。而在MountService这端,同样启动了VoldConnector socket连接线程,用于循环连接服务端,保证连接不被中断,当成功连接Vold时,循环从服务端读取数据。MountService按照指定格式向Vold发送命令,由于发送的命令比较多,这里不做一一接收,只对其中的mount命令的发送流程进行介绍: Android Service之MountService源码分析_第5张图片
从以上的时序图可以看出,MountService对命令的发送首先是调用makeCommand来组合成指定格式的命令,然后直接写入到Vold socket即可,整个命令发送流程比较简单。
public int mountVolume(String path) {        //权限检验validatePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);waitForReady();return doMountVolume(path);}
调用doMountVolume函数来挂载存储设备:
private int doMountVolume(String path) {int rc = StorageResultCode.OperationSucceeded;try {    //命令交给NativeDaemonConnector去发送    mConnector.execute("volume", "mount", path);} catch (NativeDaemonConnectorException e) {    //捕获命令发送的异常,根据异常码来决定发送失败的原因    String action = null;    int code = e.getCode();    if (code == VoldResponseCode.OpFailedNoMedia) {/* * Attempt to mount but no media inserted */rc = StorageResultCode.OperationFailedNoMedia;    } else if (code == VoldResponseCode.OpFailedMediaBlank) {if (DEBUG_EVENTS) Slog.i(TAG, " updating volume state :: media nofs");/* * Media is blank or does not contain a supported filesystem */updatePublicVolumeState(path, Environment.MEDIA_NOFS);action = Intent.ACTION_MEDIA_NOFS;rc = StorageResultCode.OperationFailedMediaBlank;    } else if (code == VoldResponseCode.OpFailedMediaCorrupt) {if (DEBUG_EVENTS) Slog.i(TAG, "updating volume state media corrupt");/* * Volume consistency check failed */updatePublicVolumeState(path, Environment.MEDIA_UNMOUNTABLE);action = Intent.ACTION_MEDIA_UNMOUNTABLE;rc = StorageResultCode.OperationFailedMediaCorrupt;    } else {rc = StorageResultCode.OperationFailedInternalError;    }    /*     * Send broadcast intent (if required for the failure)     */    if (action != null) {sendStorageIntent(action, path);    }}return rc;}
NativeDaemonConnector命令发送:
public NativeDaemonEvent execute(String cmd, Object... args)    throws NativeDaemonConnectorException {        //使用executeForList函数来发送命令和命令参数,并返回一组NativeDaemonEvent事件final NativeDaemonEvent[] events = executeForList(cmd, args);if (events.length != 1) {    throw new NativeDaemonConnectorException("Expected exactly one response, but received " + events.length);}return events[0];}
调用executeForList来发送命令和命令参数,并在这里设置超时时间:
public NativeDaemonEvent[] executeForList(String cmd, Object... args)    throws NativeDaemonConnectorException {    //设置超时时间:DEFAULT_TIMEOUT = 1 * 60 * 1000    return execute(DEFAULT_TIMEOUT, cmd, args);}
真正命令发送:
public NativeDaemonEvent[] execute(int timeout, String cmd, Object... args)    throws NativeDaemonConnectorException {final ArrayList events = Lists.newArrayList();final int sequenceNumber = mSequenceNumber.incrementAndGet();final StringBuilder cmdBuilder = new StringBuilder(Integer.toString(sequenceNumber)).append(' ');//发送起始时间final long startTime = SystemClock.elapsedRealtime();//命令组合makeCommand(cmdBuilder, cmd, args);final String logCmd = cmdBuilder.toString(); /* includes cmdNum, cmd, args */log("SND -> {" + logCmd + "}"); //SND -> {8 volume mount /storage/sdcard1}cmdBuilder.append('\0');final String sentCmd = cmdBuilder.toString(); /* logCmd + \0 */synchronized (mDaemonLock) {    if (mOutputStream == null) {throw new NativeDaemonConnectorException("missing output stream");    } else {try {    //向socket中写入命令    mOutputStream.write(sentCmd.getBytes(Charsets.UTF_8));} catch (IOException e) {    throw new NativeDaemonConnectorException("problem sending command", e);}    }}NativeDaemonEvent event = null;do {    event = mResponseQueue.remove(sequenceNumber, timeout, sentCmd);    if (event == null) {loge("timed-out waiting for response to " + logCmd);throw new NativeDaemonFailureException(logCmd, event);    }    log("RMV <- {" + event + "}");    events.add(event);} while (event.isClassContinue());        //发送结束时间final long endTime = SystemClock.elapsedRealtime();if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {    loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");}if (event.isClassClientError()) {    throw new NativeDaemonArgumentException(logCmd, event);}if (event.isClassServerError()) {    throw new NativeDaemonFailureException(logCmd, event);}return events.toArray(new NativeDaemonEvent[events.size()]);}


MountService消息接收流程

MountService需要接收两种类型的消息: 1)当外部存储设备发生热插拔时,kernel将通过netlink方式通知Vold,Vold进程经过一系列处理后最终还是要叫uevent事件消息发送给MountService,Vold发送uevent的过程已经在 android之VOLD进程启动源码分析一文中详细介绍了; 2)当MountService向Vold发送命令后,将接收到Vold的响应消息; 无论是何种类型的消息,MountService都是通过VoldConnector线程来循环接收Vold的请求,整个消息接收流程如下: Android Service之MountService源码分析_第6张图片

1)socket连接

public void run() {//创建并启动VoldConnector.CallbackHandler线程,用于处理native层的Vold进程发送过来的uevent事件消息HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");thread.start();//为VoldConnector.CallbackHandler线程创建一个Handler,用于向该线程分发消息mCallbackHandler = new Handler(thread.getLooper(), this);        //进入闭环socket连接模式while (true) {try {listenToSocket();} catch (Exception e) {loge("Error in NativeDaemonConnector: " + e);SystemClock.sleep(5000);}}}
连接服务端Socket,并读取数据:
private void listenToSocket() throws IOException {LocalSocket socket = null;try {//创建Vold socketsocket = new LocalSocket();LocalSocketAddress address = new LocalSocketAddress(mSocket,LocalSocketAddress.Namespace.RESERVED);                //向服务端发起连接请求socket.connect(address);                //从连接的socket中得到输入输出流InputStream inputStream = socket.getInputStream();synchronized (mDaemonLock) {mOutputStream = socket.getOutputStream();}                //对本次连接请求做一些回调处理mCallbacks.onDaemonConnected();                //定义bufferbyte[] buffer = new byte[BUFFER_SIZE];int start = 0;                //进入闭环数据读取模式while (true) {int count = inputStream.read(buffer, start, BUFFER_SIZE - start);//当读取的数据长度小于0时,表示连接已断开,跳出循环,重新向服务端发起新的连接请求if (count < 0) {loge("got " + count + " reading with start = " + start);break;}// Add our starting point to the count and reset the start.count += start;start = 0;                        //解析读取到的数据,得到NativeDaemonEventfor (int i = 0; i < count; i++) {if (buffer[i] == 0) {final String rawEvent = new String(buffer, start, i - start, Charsets.UTF_8);//RCV <- {632 Volume sdcard /storage/sdcard1 bad removal (179:1)}log("RCV <- {" + rawEvent + "}");try {final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent(rawEvent);//如果命令码code >= 600 && code < 700if (event.isClassUnsolicited()) {//将读取到的事件发送到VoldConnector.CallbackHandler线程中处理mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(event.getCode(), event.getRawEvent()));//否则将改事件添加到响应队列中} else {mResponseQueue.add(event.getCmdNumber(), event);}} catch (IllegalArgumentException e) {log("Problem parsing message: " + rawEvent + " - " + e);}start = i + 1;}}if (start == 0) {final String rawEvent = new String(buffer, start, count, Charsets.UTF_8);log("RCV incomplete <- {" + rawEvent + "}");}// We should end at the amount we read. If not, compact then// buffer and read again.if (start != count) {final int remaining = BUFFER_SIZE - start;System.arraycopy(buffer, start, buffer, 0, remaining);start = remaining;} else {start = 0;}}} catch (IOException ex) {loge("Communications error: " + ex);throw ex;} finally {synchronized (mDaemonLock) {if (mOutputStream != null) {try {loge("closing stream for " + mSocket);mOutputStream.close();} catch (IOException e) {loge("Failed closing output stream: " + e);}mOutputStream = null;}}try {if (socket != null) {socket.close();}} catch (IOException ex) {loge("Failed closing socket: " + ex);}}}

2)连接成功回调处理

public void onDaemonConnected() {    //创建一个工作线程new Thread("MountService#onDaemonConnected") {@Overridepublic void run() {/** * Determine media state and UMS detection status */try {//向vold查询所有的存储设备final String[] vols = NativeDaemonEvent.filterMessageList(mConnector.executeForList("volume", "list"),VoldResponseCode.VolumeListResult);        //判断存储设备状态for (String volstr : vols) {String[] tok = volstr.split(" ");// FMT: 
存储设备状态更新:
private boolean updatePublicVolumeState(String path, String state) {String oldState;synchronized(mVolumeStates) {oldState = mVolumeStates.put(path, state);}if (state.equals(oldState)) {Slog.w(TAG, String.format("Duplicate state transition (%s -> %s) for %s",state, state, path));return false;}Slog.d(TAG, "volume state changed for " + path + " (" + oldState + " -> " + state + ")");if (path.equals(mExternalStoragePath)) {// Update state on PackageManager, but only of real eventsif (!mEmulateExternalStorage) {if (Environment.MEDIA_UNMOUNTED.equals(state)) {mPms.updateExternalMediaStatus(false, false);/* * Some OBBs might have been unmounted when this volume was * unmounted, so send a message to the handler to let it know to * remove those from the list of mounted OBBS. */mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_FLUSH_MOUNT_STATE, path));} else if (Environment.MEDIA_MOUNTED.equals(state)) {mPms.updateExternalMediaStatus(true, false);}}}synchronized (mListeners) {for (int i = mListeners.size() -1; i >= 0; i--) {MountServiceBinderListener bl = mListeners.get(i);try {//调用已注册的MountServiceBinderListener来通知存储设备状态改变bl.mListener.onStorageStateChanged(path, oldState, state);} catch (RemoteException rex) {Slog.e(TAG, "Listener dead");mListeners.remove(i);} catch (Exception ex) {Slog.e(TAG, "Listener failed", ex);}}}return true;}


MountServiceBinderListener的注册过程:
public void registerListener(IMountServiceListener listener) {synchronized (mListeners) {MountServiceBinderListener bl = new MountServiceBinderListener(listener);try {listener.asBinder().linkToDeath(bl, 0);mListeners.add(bl);} catch (RemoteException rex) {Slog.e(TAG, "Failed to link to listener death");}}}
使用StorageManager的内部类MountServiceBinderListener对象来构造MountService的内部类MountServiceBinderListener对象,并添加到MountService的成员变量mListeners列表中。StorageManager的内部类MountServiceBinderListener定义如下:
private class MountServiceBinderListener extends IMountServiceListener.Stub {public void onUsbMassStorageConnectionChanged(boolean available) {final int size = mListeners.size();for (int i = 0; i < size; i++) {mListeners.get(i).sendShareAvailabilityChanged(available);}}public void onStorageStateChanged(String path, String oldState, String newState) {final int size = mListeners.size();for (int i = 0; i < size; i++) {mListeners.get(i).sendStorageStateChanged(path, oldState, newState);}}}
最后调用ListenerDelegate的sendStorageStateChanged来实现

3)事件处理

mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(event.getCode(), event.getRawEvent())); 将事件已消息的方式发送到VoldConnector.CallbackHandler线程中处理:
public boolean handleMessage(Message msg) {String event = (String) msg.obj;try {//回调MountService的onEvent函数进行处理if (!mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))) {log(String.format("Unhandled event '%s'", event));}} catch (Exception e) {loge("Error handling '" + event + "': " + e);}return true;}

public boolean onEvent(int code, String raw, String[] cooked) {if (DEBUG_EVENTS) {StringBuilder builder = new StringBuilder();builder.append("onEvent::");builder.append(" raw= " + raw);if (cooked != null) {builder.append(" cooked = " );for (String str : cooked) {builder.append(" " + str);}}Slog.i(TAG, builder.toString());}if (code == VoldResponseCode.VolumeStateChange) {/* * One of the volumes we're managing has changed state. * Format: "NNN Volume 
整个MountService与Vold的通信到此就介绍完了。


更多相关文章

  1. android消息推送-XMPP
  2. 使用GCM服务(Google Cloud Messaging)实现Android消息推送
  3. Android 事件处理基于Handler 消息处理
  4. Android学习笔记Android线程模型解析
  5. [置顶] Android消息异步机制(ThreadLocal、MessageQueue、Looper
  6. [Android] [ Android启动流程 ] [ 下 ] [ Zygote、SystemServer
  7. Android Handler 消息机制

随机推荐

  1. Android: Service中创建窗口Dialog
  2. android快速集成高德地图
  3. 2010-02-27 传智播客—Android(二)数据存储
  4. 从Android发展看Meego
  5. Android中要让一个程序的界面始终保持一
  6. Android(安卓)4.0 Launcher2源码分析——
  7. Android推送通知指南
  8. Android(安卓)Activity生命周期详解
  9. Android之TextView属性列表
  10. Android最基本的异步网络请求框架