Android源碼筆記——Camera系統架構

来源:http://www.cnblogs.com/younghao/archive/2016/04/01/5337058.html
-Advertisement-
Play Games

Camera的架構與Android系統的整體架構保持一致,如下圖所示,本文主要從以下四個方面對其進行說明。 Framework:Camera.java Android Runtime:android_hardware_Camera.cpp Library:Camera Client和Camera S... ...


Camera的架構與Android系統的整體架構保持一致,如下圖所示,本文主要從以下四個方面對其進行說明。

  1. Framework:Camera.java
  2. Android Runtime:android_hardware_Camera.cpp
  3. Library:Camera Client和Camera Service
  4. HAL:CameraHardwareInterface

14527792381330663_thumb9


 

一、Framework:Camera.java

Camera是應用層軟體直接使用的類,涵蓋了啟動、預覽、拍攝及關閉等操作攝像頭的全部介面。Camera.java在Android源碼中的路徑為:framework/base/core/java/android/hardware。為了說明整個Camera系統的架構,這裡暫不橫向分析Camera.java的功能,下麵從open()方法著手:

public static Camera open() {
    int numberOfCameras = getNumberOfCameras();
    CameraInfo cameraInfo = new CameraInfo();
    for (int i = 0; i < numberOfCameras; i++) {
        getCameraInfo(i, cameraInfo);
        if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
            return new Camera(i);
        }
    }
    return null;
}

open()方法需要註意以下幾點:

  • getNumberOfCameras為native方法,實現在android_hardware_Camera.cpp中;
  • CameraInfo是Camera定義的靜態內部類,包含facing、orientation、canDisableShutterSound;
  • getCameraInfo內部調用native方法_getCameraInfo獲取攝像頭信息;
  • open()預設啟動的是後置攝像頭(CAMERA_FACING_BACK)。
/** used by Camera#open, Camera#open(int) */
Camera(int cameraId) {
    int err = cameraInitNormal(cameraId);
    if (checkInitErrors(err)) {
        switch(err) {
            case EACCESS:
                throw new RuntimeException("Fail to connect to camera service");
            case ENODEV:
                throw new RuntimeException("Camera initialization failed");
            default:
                // Should never hit this.
                throw new RuntimeException("Unknown camera error");
        }
    }
}

Camera構造器的核心實現在cameraInitNormal中,cameraInitNormal調用cameraInitVersion,並傳入參數cameraId和CAMERA_HAL_API_VERSION_NORMAL_CONNECT,後者代表HAL的版本。

private int cameraInitVersion(int cameraId, int halVersion) {
    ……
    String packageName = ActivityThread.currentPackageName();
    return native_setup(new WeakReference<Camera>(this), cameraId, halVersion, packageName);
}

cameraInitNormal調用本地方法native_setup(),由此進入到android_hardware_Camera.cpp中,native_setup()的簽名如下:

private native final int native_setup(Object camera_this, int cameraId, int halVersion, String packageName);

 

二、Android Runtime:android_hardware_Camera.cpp

native_setup()被動態註冊到JNI,通過JNI調用android_hardware_Camera_native_setup()方法。

static JNINativeMethod camMethods[] = {
    ……
    { "native_setup",    "(Ljava/lang/Object;ILjava/lang/String;)V",
    (void*)android_hardware_Camera_native_setup }
    ……
};

JNI的重點是android_hardware_Camera_native_setup()方法的實現:

// connect to camera service
static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz,
    jobject weak_this, jint cameraId, jint halVersion, jstring clientPackageName)
{
    // Convert jstring to String16
    const char16_t *rawClientName = env->GetStringChars(clientPackageName, NULL);
    jsize rawClientNameLen = env->GetStringLength(clientPackageName);
    String16 clientName(rawClientName, rawClientNameLen);
    env->ReleaseStringChars(clientPackageName, rawClientName);

    sp<Camera> camera;
    if (halVersion == CAMERA_HAL_API_VERSION_NORMAL_CONNECT) {
        // Default path: hal version is don't care, do normal camera connect.
        camera = Camera::connect(cameraId, clientName,
                Camera::USE_CALLING_UID);
    } else {
        jint status = Camera::connectLegacy(cameraId, halVersion, clientName,
                Camera::USE_CALLING_UID, camera);
        if (status != NO_ERROR) {
            return status;
        }
    }

    if (camera == NULL) {
        return -EACCES;
    }

    // make sure camera hardware is alive
    if (camera->getStatus() != NO_ERROR) {
        return NO_INIT;
    }

    jclass clazz = env->GetObjectClass(thiz);
    if (clazz == NULL) {
        // This should never happen
        jniThrowRuntimeException(env, "Can't find android/hardware/Camera");
        return INVALID_OPERATION;
    }

    // We use a weak reference so the Camera object can be garbage collected.
    // The reference is only used as a proxy for callbacks.
    sp<JNICameraContext> context = new JNICameraContext(env, weak_this, clazz, camera);
    context->incStrong((void*)android_hardware_Camera_native_setup);
    camera->setListener(context);

    // save context in opaque field
    env->SetLongField(thiz, fields.context, (jlong)context.get());
    return NO_ERROR;
}

android_hardware_Camera_native_setup()方法通過調用Camera::connect()方法請求連接CameraService服務。入參中:

  • clientName是通過將clientPackageName從jstring轉換為String16格式得到;
  • Camera::USE_CALLING_UID是定義在Camera.h中的枚舉類型,其值為ICameraService::USE_CALLING_UID(同樣為枚舉類型,值為-1)。

Camera::connect()位於Camera.cpp中,由此進入到Library層。

 

三、Library:Camera Client和Camera Service

如上述架構圖中所示,ICameraService.h、ICameraClient.h和ICamera.h三個類定義了Camera的介面和架構,ICameraService.cpp和Camera.cpp兩個文件用於Camera架構的實現,Camera的具體功能在下層調用硬體相關的介面來實現。Camera.h是Camera系統對上層的介面。

具體的,Camera類繼承模板類CameraBase,Camera::connect()調用了CameraBase.cpp中的connect()方法。

sp<Camera> Camera::connect(int cameraId, const String16& clientPackageName,
        int clientUid)
{
    return CameraBaseT::connect(cameraId, clientPackageName, clientUid);
}

CameraBase實際上又繼承了IBinder的DeathRecipient內部類,DeathRecipient虛擬繼承自RefBase。RefBase是Android中的引用計數基礎類,其中定義了incStrong、decStrong、incWeak和decWeak等涉及sp/wp的指針操作函數,當然這扯遠了。

template <typename TCam>
struct CameraTraits {
};

template <typename TCam, typename TCamTraits = CameraTraits<TCam> >
class CameraBase : public IBinder::DeathRecipient
{
public:
    
    static sp<TCam>      connect(int cameraId,
                                 const String16& clientPackageName,
                                 int clientUid);
    ……
}
class DeathRecipient : public virtual RefBase
{
public:
    virtual void binderDied(const wp<IBinder>& who) = 0;
};

回到Camera::connect()的實現上,其中,new TCam(cameraId)生成BnCameraClient對象,BnCameraClient定義在ICameraClient.h文件中,繼承自模板類BnInterface。getCameraService()方法返回CameraService的服務代理BpCameraService,BpCameraService同樣繼承自模板類BnInterface。然後通過Binder通信發送CONNECT命令,當BnCameraService收到CONNECT命令後調用CameraService的connect()成員函數來做相應的處理。

template <typename TCam, typename TCamTraits>
sp<TCam> CameraBase<TCam, TCamTraits>::connect(int cameraId,
                                               const String16& clientPackageName,
                                               int clientUid)
{
    ALOGV("%s: connect", __FUNCTION__);
    sp<TCam> c = new TCam(cameraId); // BnCameraClient 
    sp<TCamCallbacks> cl = c;
    status_t status = NO_ERROR;
    const sp<ICameraService>& cs = getCameraService(); // BpCameraService

    if (cs != 0) {
        TCamConnectService fnConnectService = TCamTraits::fnConnectService;
        status = (cs.get()->*fnConnectService)(cl, cameraId, clientPackageName, clientUid,
                                             /*out*/ c->mCamera);
    }
    if (status == OK && c->mCamera != 0) {
        c->mCamera->asBinder()->linkToDeath(c);
        c->mStatus = NO_ERROR;
    } else {
        ALOGW("An error occurred while connecting to camera: %d", cameraId);
        c.clear();
    }
    return c;
}
class BnCameraClient: public BnInterface<ICameraClient>
{
public:
    virtual status_t    onTransact( uint32_t code,
                                    const Parcel& data,
                                    Parcel* reply,
                                    uint32_t flags = 0);
};
class BpCameraService: public BpInterface<ICameraService>
{
public:
    BpCameraService(const sp<IBinder>& impl)
        : BpInterface<ICameraService>(impl)
    {
    }
    ……
}

註:connect()函數在BpCameraService和BnCameraService的父類ICameraService中聲明為純虛函數,在BpCameraService和CameraService中分別給出了實現,BpCameraService作為代理類,提供介面給客戶端,真正實現在BnCameraService的子類CameraService中。

在BpCameraService中,connect()函數實現如下:

// connect to camera service (android.hardware.Camera)
    virtual status_t connect(const sp<ICameraClient>& cameraClient, int cameraId,
                             const String16 &clientPackageName, int clientUid,
                             /*out*/
                             sp<ICamera>& device)
    {
        Parcel data, reply;
        data.writeInterfaceToken(ICameraService::getInterfaceDescriptor());
        data.writeStrongBinder(cameraClient->asBinder());
        data.writeInt32(cameraId);
        data.writeString16(clientPackageName);
        data.writeInt32(clientUid);
        remote()->transact(BnCameraService::CONNECT, data, &reply); // BpBinder的transact()函數向IPCThreadState實例發送消息,通知其有消息要發送給binder driver
        if (readExceptionCode(reply)) return -EPROTO;
        status_t status = reply.readInt32();
        if (reply.readInt32() != 0) {
            device = interface_cast<ICamera>(reply.readStrongBinder()); // client端讀出server返回的bind
        }
        return status;
    }

首先將傳遞過來的Camera對象cameraClient轉換成IBinder類型,將調用的參數寫到Parcel(可理解為Binder通信的管道)中,通過BpBinder的transact()函數發送消息,然後由BnCameraService去響應該連接,最後就是等待服務端返回,如果成功則生成一個BpCamera實例。

真正的服務端響應實現在BnCameraService的onTransact()函數中,其負責解包收到的Parcel並執行client端的請求的方法。

status_t BnCameraService::onTransact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    switch(code) {
    
    ……
     case CONNECT: {
            CHECK_INTERFACE(ICameraService, data, reply);
            sp<ICameraClient> cameraClient =
                    interface_cast<ICameraClient>(data.readStrongBinder()); // 使用Camera的Binder對象生成Camera客戶代理BpCameraClient實例
              int32_t cameraId = data.readInt32();
            const String16 clientName = data.readString16();
            int32_t clientUid = data.readInt32();
            sp<ICamera> camera;
            status_t status = connect(cameraClient, cameraId,
                    clientName, clientUid, /*out*/camera); // 將生成的BpCameraClient對象作為參數傳遞到CameraService的connect()函數中
              reply->writeNoException();
            reply->writeInt32(status); // 將BpCamera對象以IBinder的形式打包到Parcel中返回
              if (camera != NULL) {
                reply->writeInt32(1);
                reply->writeStrongBinder(camera->asBinder());
            } else {
                reply->writeInt32(0);
            }
            return NO_ERROR;
        } break;
    ……
    }
}

主要的處理包括:

  1. 通過data中Camera的Binder對象生成Camera客戶代理BpCameraClient實例;
  2. 將生成的BpCameraClient對象作為參數傳遞到CameraService(/frameworks/av/services/camera /libcameraservice/CameraService.cpp)的connect()函數中,該函數會返回一個BpCamera實例;
  3. 將在上述實例對象以IBinder的形式打包到Parcel中返回。

最後,BpCamera實例是通過CameraService::connect()函數返回的。CameraService::connect()實現的核心是調用connectHelperLocked()函數根據HAL不同API的版本創建不同的client實例(早期版本中好像沒有connectHelperLocked()這個函數,但功能基本相似)。

status_t CameraService::connectHelperLocked(
        /*out*/
        sp<Client>& client,
        /*in*/
        const sp<ICameraClient>& cameraClient,
        int cameraId,
        const String16& clientPackageName,
        int clientUid,
        int callingPid,
        int halVersion,
        bool legacyMode) {

    int facing = -1;
    int deviceVersion = getDeviceVersion(cameraId, &facing);

    if (halVersion < 0 || halVersion == deviceVersion) {
        // Default path: HAL version is unspecified by caller, create CameraClient
        // based on device version reported by the HAL.
        switch(deviceVersion) {
          case CAMERA_DEVICE_API_VERSION_1_0:
            client = new CameraClient(this, cameraClient,
                    clientPackageName, cameraId,
                    facing, callingPid, clientUid, getpid(), legacyMode);
            break;
          case CAMERA_DEVICE_API_VERSION_2_0:
          case CAMERA_DEVICE_API_VERSION_2_1:
          case CAMERA_DEVICE_API_VERSION_3_0:
          case CAMERA_DEVICE_API_VERSION_3_1:
          case CAMERA_DEVICE_API_VERSION_3_2:
            client = new Camera2Client(this, cameraClient,
                    clientPackageName, cameraId,
                    facing, callingPid, clientUid, getpid(), legacyMode);
            break;
          case -1:
            ALOGE("Invalid camera id %d", cameraId);
            return BAD_VALUE;
          default:
            ALOGE("Unknown camera device HAL version: %d", deviceVersion);
            return INVALID_OPERATION;
        }
    } else {
        // A particular HAL version is requested by caller. Create CameraClient
        // based on the requested HAL version.
        if (deviceVersion > CAMERA_DEVICE_API_VERSION_1_0 &&
            halVersion == CAMERA_DEVICE_API_VERSION_1_0) {
            // Only support higher HAL version device opened as HAL1.0 device.
            client = new CameraClient(this, cameraClient,
                    clientPackageName, cameraId,
                    facing, callingPid, clientUid, getpid(), legacyMode);
        } else {
            // Other combinations (e.g. HAL3.x open as HAL2.x) are not supported yet.
            ALOGE("Invalid camera HAL version %x: HAL %x device can only be"
                    " opened as HAL %x device", halVersion, deviceVersion,
                    CAMERA_DEVICE_API_VERSION_1_0);
            return INVALID_OPERATION;
        }
    }

    status_t status = connectFinishUnsafe(client, client->getRemote());
    if (status != OK) {
        // this is probably not recoverable.. maybe the client can try again
        return status;
    }

    mClient[cameraId] = client;
    LOG1("CameraService::connect X (id %d, this pid is %d)", cameraId,
         getpid());

    return OK;
}

可見,在CAMERA_DEVICE_API_VERSION_2_0之前使用CameraClient進行實例化,之後則採用Camera2Client進行實例化。以CameraClient為例,其initialize()函數如下:

status_t CameraClient::initialize(camera_module_t *module) {
    int callingPid = getCallingPid();
    status_t res;

    LOG1("CameraClient::initialize E (pid %d, id %d)", callingPid, mCameraId);

    // Verify ops permissions
    res = startCameraOps();
    if (res != OK) {
        return res;
    }

    char camera_device_name[10];
    snprintf(camera_device_name, sizeof(camera_device_name), "%d", mCameraId);

    mHardware = new CameraHardwareInterface(camera_device_name);
    res = mHardware->initialize(&module->common);
    if (res != OK) {
        ALOGE("%s: Camera %d: unable to initialize device: %s (%d)",
                __FUNCTION__, mCameraId, strerror(-res), res);
        mHardware.clear();
        return res;
    }

    mHardware->setCallbacks(notifyCallback,
            dataCallback,
            dataCallbackTimestamp,
            (void *)(uintptr_t)mCameraId);

    // Enable zoom, error, focus, and metadata messages by default
    enableMsgType(CAMERA_MSG_ERROR | CAMERA_MSG_ZOOM | CAMERA_MSG_FOCUS |
                  CAMERA_MSG_PREVIEW_METADATA | CAMERA_MSG_FOCUS_MOVE);

    LOG1("CameraClient::initialize X (pid %d, id %d)", callingPid, mCameraId);
    return OK;
}

上述函數中,主要註意以下流程:

  1. 加粗的代碼CameraHardwareInterface新建了了一個Camera硬體介面,當然,camera_device_name為攝像頭設備名;
  2. mHardware->initialize(&module->common)調用底層硬體的初始化方法;
  3. mHardware->setCallbacks將CamerService處的回調函數註冊到HAL處。

CameraHardwareInterface定義了Camera的硬體抽象特征,由此進入到HAL。

 

四、HAL:CameraHardwareInterface

CameraHardwareInterface的作用在於鏈接Camera Server和V4L2,通過實現CameraHardwareInterface可以屏蔽不同的driver對Camera Server的影響。CameraHardwareInterface同樣虛擬繼承自RefBase。

class CameraHardwareInterface : public virtual RefBase {
public:
    CameraHardwareInterface(const char *name)
    {
        mDevice = 0;
        mName = name;
    }
    ……
}

CameraHardwareInterface中包含了控制通道和數據通道,控制通道用於處理預覽和視頻獲取的開始/停止、拍攝照片、自動對焦等功能,數據通道通過回調函數來獲得預覽、視頻錄製、自動對焦等數據。當需要支持新的硬體時就需要繼承於CameraHardwareInterface ,來實現對應的功能。CameraHardwareInterface提供的public方法如下:

1258.tmp

在前一節中,initialize()函數調用了mHardware->initialize和mHardware->setCallbacks,下麵來看下CameraHardwareInterface.h對其的實現。

status_t initialize(hw_module_t *module)
    {
        ALOGI("Opening camera %s", mName.string());
        camera_module_t *cameraModule = reinterpret_cast<camera_module_t *>(module);
        camera_info info;
        status_t res = cameraModule->get_camera_info(atoi(mName.string()), &info);
        if (res != OK) return res;

        int rc = OK;
        if (module->module_api_version >= CAMERA_MODULE_API_VERSION_2_3 &&
            info.device_version > CAMERA_DEVICE_API_VERSION_1_0) {
            // Open higher version camera device as HAL1.0 device.
            rc = cameraModule->open_legacy(module, mName.string(),
                                               CAMERA_DEVICE_API_VERSION_1_0,
                                               (hw_device_t **)&mDevice);
        } else {
            rc = CameraService::filterOpenErrorCode(module->methods->open(
                module, mName.string(), (hw_device_t **)&mDevice));
        }
        if (rc != OK) {
            ALOGE("Could not open camera %s: %d", mName.string(), rc);
            return rc;
        }
        initHalPreviewWindow();
        return rc;
    }

在initialize()方法中,通過cameraModule->open_legacy打開攝像頭模組,initHalPreviewWindow()用於初始化Preview的相關流opspreview_stream_ops,初始化hal的預覽視窗。

void initHalPreviewWindow()
    {
        mHalPreviewWindow.nw.cancel_buffer = __cancel_buffer;
        mHalPreviewWindow.nw.lock_buffer = __lock_buffer;
        mHalPreviewWindow.nw.dequeue_buffer = __dequeue_buffer;
        mHalPreviewWindow.nw.enqueue_buffer = __enqueue_buffer;
        mHalPreviewWindow.nw.set_buffer_count = __set_buffer_count;
        mHalPreviewWindow.nw.set_buffers_geometry = __set_buffers_geometry;
        mHalPreviewWindow.nw.set_crop = __set_crop;
        mHalPreviewWindow.nw.set_timestamp = __set_timestamp;
        mHalPreviewWindow.nw.set_usage = __set_usage;
        mHalPreviewWindow.nw.set_swap_interval = __set_swap_interval;

        mHalPreviewWindow.nw.get_min_undequeued_buffer_count =
                __get_min_undequeued_buffer_count;
    }
/** Set the notification and data callbacks */
    void setCallbacks(notify_callback notify_cb,
                      data_callback data_cb,
                      data_callback_timestamp data_cb_timestamp,
                      void* user)
    {
        mNotifyCb = notify_cb;
        mDataCb = data_cb;
        mDataCbTimestamp = data_cb_timestamp;
        mCbUser = user;

        ALOGV("%s(%s)", __FUNCTION__, mName.string());

        if (mDevice->ops->set_callbacks) {
            mDevice->ops->set_callbacks(mDevice,
                                   __notify_cb,
                                   __data_cb,
                                   __data_cb_timestamp,
                                   __get_memory,
                                   this);
        }
    }
set_callbacks中,__notify_cb、__data_cb、__data_cb_timestamp和__get_memory分別消息回調,數據回調,時間戳回調,以及記憶體相關操作的回調。

 

以上通過簡略分析應用層調用Camera.open()之後在Framework、ART、Library以及HAL層的響應,來說明Android中Camera系統的整體架構,希望對讀者能有一定的幫助,後續將在理解Camera整體架構的基礎,探索更加高效的Preview方式,敬請期待!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • GitHub:https://github.com/samvermette/SVProgressHUDSVProgressHUD和MBProgressHUD效果差不多,不過不需要使用協議,同時也不需要聲明實例。直接通過類方法進行調用即可: 可以使用以下方法來顯示狀態: 如果需要明確的進度,則使用以下 ...
  • 完整的開發一個android移動App需要經過從分解需求、架構設計到開發調試、測試、上線發佈等多個階段,在發佈後還會有產品功能上的迭代演進,此外還會面對性能、安全、無線網路質量等多方面的問題。 移動App的產品形態各不相同,有的是內容類,有的是工具類,有的是社交類,所以它們的業務邏輯所偏重的核心技術 ...
  • 源碼如下: 運行結果如下,在屏幕最右邊有一個紅色的P: 源碼解析: 1.首先程式跳轉至LABEL_BEGIN處,jmp LABEL_BEGIN。將ds、es、ss段寄存器全部初始化為當前代碼段。 2.初始化32位代碼段描述符 在實模式下,也就是8086的16位的CPU的定址方式是段x16+偏移,而在 ...
  • activity的佈局 佈局主要有五種: 1.LinearLayout(線性佈局) 可以利用orientation屬性來設置線性佈局是水平還是垂直 2.TableLayout(表格佈局) 3.RelativeLayout(相對佈局) 相對佈局是比較好的佈局方式,它是根據控制項的相對位置來佈局的,這個的 ...
  • 今天愚人節,小伙們,愚人節快樂! 實現一個小功能,滑動菜單,顯示隱藏的功能菜單, 先上圖: 這裡嘗試用了下使用三個方式來實現了這個功能: 1、使用自定義UITableViewCell + UISwipeGestureRecognizer + 代理 實現; 2、使用自定義UITableViewCell ...
  • 本文轉自 :http://www.cnblogs.com/wendingding/p/3761730.html ios開發UI篇—使用純代碼自定義UItableviewcell實現一個簡單的微博界面佈局 一、實現效果 二、使用純代碼自定義一個tableview的步驟 1.新建一個繼承自UITable ...
  • android中activity的生命周期 主要有七種周期 onCreate(); onStart(); onResume(); onPause(); onStop(); onRestart(); onDestory(); 當activity第一次運行時調用onCreate(),然後界面能被看見是調 ...
  • 我為什麼選擇android? 我基本上前一年的時間都是在學習java的語法和線程之類的,沒有註意java的分類,所以到現在慢慢接觸到深處的時候我瞭解到,java的優勢主要在web,而我不是特別喜歡網頁的設計開發,相比較而言,更喜歡軟體之類的app式的軟體,所以現在記錄下我為什麼選擇android,j ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...