1. 程式人生 > >Android 5.0 Camera系統原始碼分析(3):Camera預覽流程控制流

Android 5.0 Camera系統原始碼分析(3):Camera預覽流程控制流

1. 前言

本文分析的是Android系統原始碼,從frameworks層到hal層,記錄了Camera進入預覽模式的重點程式碼,主要為控制流程的程式碼,有關影象buffer的傳遞暫不涉及,硬體平臺基於mt6735。由於某些函式比較複雜,在貼出程式碼時會適當對其進行簡化。

2. APP層

這裡將分析app層令Camera進入預覽模式的兩個重點api:setPreviewDisplay和startPreview

mCamera.setPreviewDisplay(mSurfaceHolder);
mCamera.startPreview();

3. setPreviewDisplay函式分析

預覽影象最終是要在lcd上顯示的,想要在lcd上顯示影象就需要用到Surface 。填充Surface有兩種方法,一種是註冊callback函式,預覽資料將在callback函式中返回,得到資料後再把它送到Surface裡面;另一種是在開始預覽之前就為底層設定好Surface,底層獲取資料後直接把資料送到Surface裡面,為底層設定好Surface就是setPreviewDisplay的作用,

3.1 frameworks層

先來看frameworks層的實現

public final void setPreviewDisplay(SurfaceHolder holder) throws
IOException { if (holder != null) { setPreviewSurface(holder.getSurface()); } else { setPreviewSurface((Surface)null); } }

setPreviewSurface是一個jni函式,它的實現在android_hardware_Camera.cpp中

static void android_hardware_Camera_setPreviewSurface(JNIEnv *env, jobject thiz, jobject jSurface)
{
    sp<Camera> camera
= get_native_camera(env, thiz, NULL); if (camera == 0) return; sp<IGraphicBufferProducer> gbp; sp<Surface> surface; if (jSurface) { surface = android_view_Surface_getSurface(env, jSurface); if (surface != NULL) { gbp = surface->getIGraphicBufferProducer(); } } if (camera->setPreviewTarget(gbp) != NO_ERROR) { jniThrowException(env, "java/io/IOException", "setPreviewTexture failed"); } }
// pass the buffered IGraphicBufferProducer to the camera service 
status_t Camera::setPreviewTarget(const sp<IGraphicBufferProducer>& bufferProducer) 
{ 
    sp <ICamera> c = mCamera; 
    return c->setPreviewTarget(bufferProducer); 
} 
// set the buffer consumer that the preview will use 
status_t CameraClient::setPreviewTarget( 
        const sp<IGraphicBufferProducer>& bufferProducer) { 
    sp<IBinder> binder; 
    sp<ANativeWindow> window; 
    if (bufferProducer != 0) { 
        binder = bufferProducer->asBinder(); 
        window = new Surface(bufferProducer, /*controlledByApp*/ true); 
    } 
    return setPreviewWindow(binder, window); 
}

ANativeWindow顧名思義“本地視窗”,Surface類繼承了ANativeWindow類。按照網上的說法,ANativeWindow類是連線OpenGL和Android視窗系統的橋樑,即OpenGL需要通過ANativeWindow類來間接地操作Android視窗系統。但我們接下來要操作ANativeWindow的不是OpenGL,而是CameraClient

status_t CameraClient::setPreviewWindow(const sp<IBinder>& binder, 
        const sp<ANativeWindow>& window) {
    if (window != 0) { 
        result = native_window_api_connect(window.get(), NATIVE_WINDOW_API_CAMERA); 
        if (result != NO_ERROR) { 
            ALOGE("native_window_api_connect failed: %s (%d)", strerror(-result), 
                    result); 
            return result; 
        } 
    } 

    // If preview has been already started, register preview buffers now. 
    if (mHardware->previewEnabled()) { 
        if (window != 0) { 
            native_window_set_scaling_mode(window.get(), 
                    NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW); 
            native_window_set_buffers_transform(window.get(), mOrientation); 
            result = mHardware->setPreviewWindow(window); 
        } 
    } 
    return result; 
}
/** Set the ANativeWindow to which preview frames are sent */ 
status_t setPreviewWindow(const sp<ANativeWindow>& buf) 
{ 
    mPreviewWindow = buf; 
    mHalPreviewWindow.user = this; 
    return mDevice->ops->set_preview_window(mDevice, 
               buf.get() ? &mHalPreviewWindow.nw : 0); 
}

ANativeWindow最終儲存在mPreviewWindow變數中,而傳到Hal層的則是mHalPreviewWindow.nw 操作集,Hal層將通過它來間接的操作mPreviewWindow。

Created with Raphaël 2.1.0appappCamera.javaCamera.javaandroid_hardware_Camera.cppandroid_hardware_Camera.cppCamera.cppCamera.cppCameraClient.cppCameraClient.cppCameraHardwareInterface.hCameraHardwareInterface.hsetPreviewDisplaysetPreviewSurfacesetPreviewTargetsetPreviewTargetsetPreviewWindowsetPreviewWindow

mDevice就是上篇博文Camera開啟流程中最後講到的從Hal返回的mDevice物件,而它的ops指標指向的是gCameraDevOps結構體,從這裡開始進入Hal層

3.2 Hal層

gCameraDevOps就在Cam1Device.cpp中定義

static mtk_camera_device_ops const
gCameraDevOps = 
{
    #define OPS(name) name: camera_##name

    {   
        OPS(set_preview_window), 
        OPS(set_callbacks), 
        OPS(enable_msg_type), 
        OPS(disable_msg_type), 
        OPS(msg_type_enabled), 
        OPS(start_preview), 
        OPS(stop_preview), 
        OPS(preview_enabled), 
        OPS(store_meta_data_in_buffers), 
        OPS(start_recording), 
        OPS(stop_recording), 
        OPS(recording_enabled), 
        OPS(release_recording_frame), 
        OPS(auto_focus), 
        OPS(cancel_auto_focus), 
        OPS(take_picture), 
        OPS(cancel_picture), 
        OPS(set_parameters), 
        OPS(get_parameters), 
        OPS(put_parameters), 
        OPS(send_command), 
        OPS(release), 
        OPS(dump)
    },  
    OPS(mtk_set_callbacks),

    #undef  OPS
};

可以看到有關Camera的所有操作都這這裡,接著看函式set_preview_window的實現

//  Implementation of camera_device_ops
static int camera_set_preview_window(
    struct camera_device * device,
    struct preview_stream_ops *window
)   
{   
    int err = -EINVAL;

    Cam1Device*const pDev = Cam1Device::getDevice(device);
    if  ( pDev )
    {
        err = pDev->setPreviewWindow(window);
    }

    return  err;
}

Cam1Device::getDevice函式獲取到的將是DefaultCam1Device物件,而setPreviewWindow函式則在它的父類Cam1DeviceBase中實現

/******************************************************************************
 *  Set the preview_stream_ops to which preview frames are sent.
 ******************************************************************************/
status_t
Cam1DeviceBase::
setPreviewWindow(preview_stream_ops* window)
{   
    status_t status = initDisplayClient(window);
    if  ( OK == status && previewEnabled() && mpDisplayClient != 0 )
    {
        status = enableDisplayClient();
    }

    return  status;
}

第9行,初始化DisplayClient
第11行,通知DisplayClient開始工作
重點關注下函式initDisplayClient 的實現

status_t
Cam1DeviceBase::
initDisplayClient(preview_stream_ops* window)
{
    status_t status = OK;
    Size previewSize;

    //  [1] Check to see whether the passed window is NULL or not.
    if  ( ! window )
    {
        if  ( mpDisplayClient != 0 )
        {
            mpDisplayClient->uninit();
            mpDisplayClient.clear();
        }
        status = OK;
        goto lbExit;
    }

    //  [2] Get preview size.
    if  ( ! queryPreviewSize(previewSize.width, previewSize.height) )
    {
        status = DEAD_OBJECT;
        goto lbExit;
    }
    //  [3] Initialize Display Client.
    if  ( mpDisplayClient != 0 )
    {
        ......
    }
    //  [3.1] create a Display Client.
    mpDisplayClient = IDisplayClient::createInstance();
    if  ( mpDisplayClient == 0 )
    {
        MY_LOGE("Cannot create mpDisplayClient");
        status = NO_MEMORY;
        goto lbExit;
    }
    //  [3.2] initialize the newly-created Display Client.
    if  ( ! mpDisplayClient->init() )
    {
        MY_LOGE("mpDisplayClient init() failed");
        mpDisplayClient->uninit();
        mpDisplayClient.clear();
        status = NO_MEMORY;
        goto lbExit;
    }
    //  [3.3] set preview_stream_ops & related window info.
    if  ( ! mpDisplayClient->setWindow(window, previewSize.width, previewSize.height, queryDisplayBufCount()) )
    {
        status = INVALID_OPERATION;
        goto lbExit;
    }
    //  [3.4] set Image Buffer Provider Client if it exist.
    if  ( mpCamAdapter != 0 && ! mpDisplayClient->setImgBufProviderClient(mpCamAdapter) )
    {
        status = INVALID_OPERATION;
        goto lbExit;
    }

    status = OK;

lbExit:
    if  ( OK != status )
    {
        MY_LOGD("Cleanup...");
        ......
    }

    return  status;
}

initDisplayClient函式都做了些什麼事情註釋已經寫得很清楚
第31-47行,建立並初始化DisplayClient,其中DisplayClient是影象消費者,由它負責將影象資料送往Surface
第48-53行,DisplayClient想要操作Surface只能通過preview_stream_ops,也就是從上層傳下來mHalPreviewWindow.nw操作集,setWindow函式會通過preview_stream_ops對Surface設定一些引數,並把preview_stream_ops儲存在DisplayClient的mpStreamOps變數中,以後用到的時候才找得到。
第54-59行,DisplayClient作為消費者,那麼就會有生產者,也就是CamAdapter。由CamAdapter提供影象資料,再由DisplayClient將資料送往Surface。但由於這個時候的 mpCamAdapter 為空,所以這裡的setImgBufProviderClient函式暫時不會被呼叫。

setPreviewDisplay Hal

4. startPreview函式分析

app層通過呼叫startPreview函式來進入預覽模式,與setPreviewWindow的流程一樣,最終會調到Cam1DeviceBase的startPreview函式

4.1 Cam1DeviceBase::startPreview函式分析

/******************************************************************************
 *  Start preview mode.
 ******************************************************************************/
status_t
Cam1DeviceBase::
startPreview()
{
    status_t status = OK;

    if  ( ! onStartPreview() )
    {    
        MY_LOGE("onStartPreviewLocked() fail");
        status = INVALID_OPERATION;
        goto lbExit;
    }    

    if  ( mpDisplayClient == 0 )
    {    
        MY_LOGD("DisplayClient is not ready.");
    }
    else if ( OK != (status = enableDisplayClient()) )
    {
        goto lbExit;
    }

    ......

    //  startPreview in Camera Adapter.
    {
        status = mpCamAdapter->startPreview();
        if  ( OK != status )
        {
            MY_LOGE("startPreview() in CameraAdapter returns: [%s(%d)]", ::strerror(-status), -status);
            goto lbExit;
        }
    }

    ......

    status = OK;
lbExit:
    if  ( OK != status )
    {
        ......
    }

    MY_LOGI("- status(%d)", status);
    return  status;
}

第10行, onStartPreview函式主要就是建立並初始化 CameraAdapter
第21行, 通知DisplayClient開始工作
第30行, mpCamAdapter->startPreview函式工作量巨大,包含了初始化buffer、3A,設定ISP和sensor驅動進入預覽模式等工作。

先看CameraAdapter的初始化

DefaultCam1Device::
onStartPreview()
{
    bool ret = false;

    ......

    //  (2) Initialize Camera Adapter.
    if  ( ! initCameraAdapter() )
    {       
        MY_LOGE("NULL Camera Adapter");
        goto lbExit;
    }
    //
    ret = true;
lbExit: 
    return ret;
}
bool    
Cam1DeviceBase::
initCameraAdapter()
{   
    bool ret = false;

    //  Create & init a new CamAdapter.
    mpCamAdapter = ICamAdapter::createInstance(mDevName, mi4OpenId, mpParamsMgr);
    if  ( mpCamAdapter != 0 && mpCamAdapter->init() )
    {
        //  (.1) init.
        mpCamAdapter->setCallbacks(mpCamMsgCbInfo);
        mpCamAdapter->enableMsgType(mpCamMsgCbInfo->mMsgEnabled);

        //  (.2) Invoke its setParameters
        if  ( OK != mpCamAdapter->setParameters() )
        {
            //  If fail, it should destroy instance before return.
            MY_LOGE("mpCamAdapter->setParameters() fail");
            goto lbExit;
        }

        //  (.3) Send to-do commands.
        {
            Mutex::Autolock _lock(mTodoCmdMapLock);
            for (size_t i = 0; i < mTodoCmdMap.size(); i++)
            {
                CommandInfo const& rCmdInfo = mTodoCmdMap.valueAt(i);
                MY_LOGD("send queued cmd(%#x),args(%d,%d)", rCmdInfo.cmd, rCmdInfo.arg1, rCmdInfo.arg2);
                mpCamAdapter->sendCommand(rCmdInfo.cmd, rCmdInfo.arg1, rCmdInfo.arg2);
            }
            mTodoCmdMap.clear();
        }

        //  (.4) [DisplayClient] set Image Buffer Provider Client if needed.
        if  ( mpDisplayClient != 0 && ! mpDisplayClient->setImgBufProviderClient(mpCamAdapter) )
        {
            MY_LOGE("mpDisplayClient->setImgBufProviderClient() fail");
            goto lbExit;
        }
    }

    ret = true;
lbExit:
    return ret;
}

建立CamAdapter例項並對它進行初始化。其中第35-40行,之前在setPreviewWindow裡沒機會呼叫的mpDisplayClient->setImgBufProviderClient函式將在這裡呼叫。DisplayClient和CamAdapter將會通過setImgBufProviderClient函式關聯起來,也就是告訴DisplayClient影象資料將由CamAdapter提供。至於CamAdpter如何獲取影象資料和DisplayClient如何將資料送往Surface將在以後解析。

startPreview Hal 1

4.2 mpCamAdapter->startPreview函式分析

既然資料由CamAdapter提供,那麼怎麼告訴它開始向DisplayClient提供資料呢,還的繼續分析mpCamAdapter->startPreview函式

status_t
CamAdapter::
startPreview()
{
    return  mpStateManager->getCurrentState()->onStartPreview(this);
}
status_t
StateIdle::
onStartPreview(IStateHandler* pHandler)
{
    ......
    status = pHandler->onHandleStartPreview();
    ......
    return  status;
}

mpStateManager->getCurrentState函式獲取到的是idle狀態,在上文提到 mpCamAdapter->init函式中設定。而 StateIdle::onStartPreview函式將會回撥CamAdapter的onHandleStartPreview函式,這個函式很長,非常長,相當長。

/******************************************************************************
*   CamAdapter::startPreview() -> IState::onStartPreview() -> 
*   IStateHandler::onHandleStartPreview() -> CamAdapter::onHandleStartPreview()
*******************************************************************************/
status_t
CamAdapter::
onHandleStartPreview()
{
    ......

    mpPass2Node = Pass2Node::createInstance(PASS2_FEATURE);
    mpCamGraph          = ICamGraph::createInstance(
                                        getOpenId(),
                                        mUserName.string());
    mpPass1Node         = Pass1Node::createInstance(p1NodeInitCfg);
    mpCamGraph->setBufferHandler(   PASS1_RESIZEDRAW,   mpAllocBufHdl);
    mpCamGraph->setBufferHandler(   PASS1_FULLRAW,      mpAllocBufHdl);
    mpCamGraph->connectData(    PASS1_RESIZEDRAW,   CONTROL_RESIZEDRAW, mpPass1Node,        mpDefaultCtrlNode);
    mpCamGraph->connectData(    CONTROL_PRV_SRC,    PASS2_PRV_SRC,      mpDefaultCtrlNode,  mpPass2Node); 
    mpCamGraph->connectNotify(  PASS1_START_ISP,    mpPass1Node,        mpDefaultCtrlNode);
    mpCamGraph->connectNotify(  PASS1_STOP_ISP,     mpPass1Node,        mpDefaultCtrlNode);
    mpCamGraph->connectNotify(  PASS1_EOF,          mpPass1Node,        mpDefaultCtrlNode);

    if ( !mpCamGraph->init() ) {
        ......
    }
    if ( !mpCamGraph->start() ) {
        ......
    }
lbExit:
    ......
    return ret;
}

暫時先把那些亂七八糟的引數設定的程式碼忽略掉,重點關注下 Pass1Node、 Pass2Node和DefaultCtlNode,以及作為各個Node通訊的橋樑的CamGraph。

CamNode

CamGraph代表了整個系統,而使用不同的Node來描述不同的buffer處理, 所有的Node都需要連線到CamGraph。各個Node之間的通訊就需要用到 connectData和 connectNotify函式, connectData為兩個node之間buffer傳輸的連線,而 connectNotify為兩個node之間訊息傳輸的連線。

例如第18行呼叫了connectData(PASS1_RESIZEDRAW, CONTROL_RESIZEDRAW, mpPass1Node,mpDefaultCtrlNode)之後Pass1Node和DefaultCtrlNode就連線在一起,事件是 PASS1_RESIZEDRAW,也就是說當Pass1Node呼叫handlePostBuffer(PASS1_RESIZEDRAW, buffer)的時候,DefaultCtrlNode裡面的onPostBuffer函式將會接受到Pass1Node的buffer。

同理第20行呼叫了connectNotify( PASS1_START_ISP, mpPass1Node, mpDefaultCtrlNode),事件是 PASS1_START_ISP,當Pass1Node呼叫handleNotify(PASS1_START_ISP)的時候,DefaultCtrlNode裡面的onNotify函式將會接收到 PASS1_START_ISP訊息。

connectData和connectNotify的不同之處在於,一個可以傳輸整個buffer,但只能一對一連線,一個只能傳輸訊息,但可以一對多連線,這兩個函式的實現這裡就不解析了,裡面各種子類、父類的關係比較複雜,整理起來比較麻煩。需要關注的是 mpCamGraph->init和 mpCamGraph->start這兩個函式,先來看看init

MBOOL
ICamGraph::
init()
{
    return mpImpl->init();
}

這裡的 mpImpl指的是ICamGraphImpl

MBOOL
ICamGraphImpl::
init()
{
    Mutex::Autolock _l(mLock);
    MY_LOGD("init +");
    MY_ASSERT_STATE( mState == State_Connected, mState );

    MBOOL ret = MTRUE;
    vector< ICamNodeImpl* >::const_iterator iter;
    for( iter = mvNodeImpls.begin(); iter != mvNodeImpls.end(); iter++ )
    {
        MY_ASSERT_NODE_OP( ret, (*iter), init );
    }

lbExit:
    if( !ret )
    {
        ......
    }
    else
    {
        mState = State_Initiated;
    }
    MY_LOGD("init -");
    return ret;
}

mvNodeImpls裡儲存的是ICamThreadImpl物件, 每一個ICamThreadImpl代表一個CamNode,例如Pass1Node。這個函式所做的事情就是迴圈遍歷所有的ICamThreadImpl,並且呼叫它們的init函式

MBOOL
ICamThreadImpl::
init()
{
    Mutex::Autolock _l(mLock);
    MY_ASSERT_STATE( mState == State_Connected, mState );

    MY_LOGV("init");
    MY_ASSERT( mpSelf->onInit() );
    MY_ASSERT( mpThread->createThread() 
            && mpThread->sendThreadCmd(TCmd_Sync)
            && mpThread->sendThreadCmd(TCmd_Init)
            && mpThread->sendThreadCmd(TCmd_Sync));

    mState = State_Initiated;
    return MTRUE;
}

ICamThreadImpl裡的mySelf成員就指向了它所代表的CamNode,例如Pass1Node。也就是說接下來所有儲存在 mvNodeImpls裡面的CamNode的onInit函式都會被呼叫。儲存在mvNodeImpls裡面的CamNode有很多,例如Pass1Node、Pass2Node、DefaultCtrlNode等。Pass1Node負責和Sensor Driver、ISP Driver打交道,進入預覽模式的重點工作都由它來完成,所以這裡只分析Pass1Node,來看看Pass1Node的onInit函式

MBOOL
Pass1NodeImpl::
onInit()
{
    ......
    mpIspSyncCtrlHw = IspSyncControlHw::createInstance(getSensorIdx());
    mpIspSyncCtrlHw->setIspEnquePeriod(mIspEnquePeriod);
    mpIspSyncCtrlHw->setSensorInfo(
            mInitCfg.muScenario,
            sensorSize.w,
            sensorSize.h,
            mSensorInfo.sensorType);
    ......
    mpCamIO = (IHalCamIO*)INormalPipe::createInstance(getSensorIdx(), getName(), mIspEnquePeriod);
    if( !mpCamIO )
    {
        MY_LOGE("create NormalPipe failed");
        goto lbExit;
    }
    if( !mpCamIO->init() )
    {
        MY_LOGE("camio init failed");
        goto lbExit;
    }
    ret = MTRUE;
lbExit:
    return ret;
}

主要就是對IspSyncCtrl和CamIO進行初始化,一個用來和ISP打交道,另一個用來和驅動打交道

回到onHandleStartPreview函式,在執行完mpCamGraph->init函式之後就到 mpCamGraph->start函數了。和mpCamGraph->init的流程一樣,mpCamGraph->start所做的事情就是迴圈遍歷所有的CamNode,並且回撥它們的onStart函式,直接看Pass1Node的onStart函式

MBOOL
Pass1NodeImpl::
onStart()
{
    list<HwPortConfig_t> lHwPortCfg;
    if( !getHwPortConfig(&lHwPortCfg) )
    {
        MY_LOGE("getHwPortConfig failed");
        goto lbExit;
    }

    if( !startHw(lHwPortCfg) )
    {
        MY_LOGE("startHw failed");
        goto lbExit;
    }
    ret = MTRUE;
lbExit:
    FUNC_END;
    return ret;
}

接著看startHw函式的實現

MBOOL
Pass1NodeImpl::
startHw(list<HwPortConfig_t> & plPortCfg)
{
    // 1. Allocated ring buffers.
    if( pthread_create(&mThreadHandle, NULL, doThreadAllocBuf, &th_data) != 0 )
    {
        MY_LOGE("pthread create failed");
        goto lbExit;
    }

    // 2. Lock Pass1 HW
    if( !mpIspSyncCtrlHw->lockHw(IspSyncControlHw::HW_PASS1) )
    {
        MY_LOGE("isp sync lock pass1 failed");
        goto lbExit;
    }

    ......
    // 3. Configure RRZO and IMGO
    if( !mpCamIO->configPipe(halCamIOinitParam) ) {
        MY_LOGE("configPipe failed");
        goto lbExit;
    }

    newMagicNum = mpIspSyncCtrlHw->getMagicNum(MTRUE);
    if( !configFrame(newMagicNum) ) {
        MY_LOGE("configFrame failed");
        goto lbExit;
    }

    // 4. Send PASS1_START_ISP event
    handleNotify(PASS1_START_ISP, newMagicNum, 0);

    ......
    // 5. Enque buffer
    if( !mpCamIO->enque(halCamIOQBuf) ) {
        MY_LOGE("enque failed");
        goto lbExit;
    }

    // 6. Start ISP
    if( !mpCamIO->start() ) {
        MY_LOGE("start failed");
        goto lbExit;
    }

    ret = MTRUE;
lbExit:
    if( !ret ) {
        ......
    }
    return ret;
}

這個函式做的事情比較多,上面標記的每個步驟都很複雜
第5-10行:建立一個執行緒來分配ring buffers,用於存放從驅動獲取到的影象資料
第20-24行:配置ISP和Sensor驅動預覽相關的引數,記得sensor驅動中(例如imx214mipiraw_Sensor.c)的preview_setting函式嗎,就是在這個時候被呼叫的
preview_setting
第33行:傳送PASS1_START_ISP事件,其它的CamNode接收到該事件後會做相應的處理,例如DefaultCtlNode,會通知Hal3A進入CameraPreview狀態
第42-46行:讓ISP開始工作,到這裡準備工作都已經完成,Camera已經進入了預覽模式,接下來就是不斷獲取影象資料,並將它送到顯示器了。

startPreview Hal 2

5. 總結

setPreviewWindow函式就是為hal層準備好Surface,hal層只能通過上層傳下來的mHalPreviewWindow.nw來間接的操作Surface,而mHalPreviewWindow.nw儲存在DisplayClient裡面,也就是說DisplayClient是lcd顯示影象的關鍵

startPreview函式的工作重點在CamAdapter,它代表Camera硬體,由它提供影象資料給DisplayClient。CamAdapter包含了多個CamNode,不同的CamNode用來描述不同的buffer處理,例如Pass1Node,它負責和驅動打交道,進入預覽模式的重點工作都在它的startHw函式裡面完成。