IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android Camera(四) getCameraIdList流程 (android P) -> 正文阅读

[移动开发]Android Camera(四) getCameraIdList流程 (android P)

getCameraIdList() 获取当前设备的相机设备列表 根据这个列表可以查询当前存在几个相机

本文将从当设备启动时主动初始化的列表和当设备存在拔插时的被动回调两条线路分析这个api


getCameraIdList

路径??frameworks/base/core/java/android/hardware/camera2/CameraManager.java

? ? ? ? 获取idlist,在下面代码可以看出调用的是CameraManagerGlobalgetCameraIdList

@NonNull
public String[] getCameraIdList() throws CameraAccessException {
    return CameraManagerGlobal.get().getCameraIdList();
}

CameraManagerGlobal::getCameraIdList

路径??frameworks/base/core/java/android/hardware/camera2/CameraManager.java

? ? ? ? 这里面主要是获取到cameraService

????????根据CameraManagermDeviceStatus里面的数据进行返回

????????private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();

public String[] getCameraIdList() {
    String[] cameraIds = null;
    synchronized(mLock) {
        // Try to make sure we have an up-to-date list of camera devices.
        //获取cameraService
        connectCameraServiceLocked();

        int idCount = 0;
        for (int i = 0; i < mDeviceStatus.size(); i++) {
            int status = mDeviceStatus.valueAt(i);
            if (status == ICameraServiceListener.STATUS_NOT_PRESENT ||
                    status == ICameraServiceListener.STATUS_ENUMERATING) continue;
            idCount++;
        }
        cameraIds = new String[idCount];
        idCount = 0;
        for (int i = 0; i < mDeviceStatus.size(); i++) {
            int status = mDeviceStatus.valueAt(i);
            if (status == ICameraServiceListener.STATUS_NOT_PRESENT ||
                    status == ICameraServiceListener.STATUS_ENUMERATING) continue;
            cameraIds[idCount] = mDeviceStatus.keyAt(i);
            idCount++;
        }
    }
    //...接下来是排序
    return cameraIds;
}

mDeviceStatus的添加? onStatusChangedLocked

路径??frameworks/base/core/java/android/hardware/camera2/CameraManager.java

? ? ? ? 可以明显看出这个数据结构中的值是受这个回调函数控制的当回调回来的是添加设备则添加移除则移除,这个回调是由函数connectCameraServiceLocked调用而来的

private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();


private void onStatusChangedLocked(int status, String id) {
    //...
    Integer oldStatus;
    if (status == ICameraServiceListener.STATUS_NOT_PRESENT) {
        oldStatus = mDeviceStatus.remove(id);
    } else {
        oldStatus = mDeviceStatus.put(id, status);
    }
    //...


    final int callbackCount = mCallbackMap.size();
    for (int i = 0; i < callbackCount; i++) {
        Executor executor = mCallbackMap.valueAt(i);
        final AvailabilityCallback callback = mCallbackMap.keyAt(i);

        postSingleUpdate(callback, executor, id, status);
    }
} // onStatusChangedLocked

connectCameraServiceLocked?

路径??frameworks/base/core/java/android/hardware/camera2/CameraManager.java

? ? ? ? 这个函数是在getCameraIdList函数中调用过的 主要的用途就是获取链接到cameraService,并将自己作为监听注册给cameraService

? ? ? ? 这样本文就从这开始介绍?主动:初始设备列表?被动:动态插拔设备列表?两种形式

主动:在调用注册函数后会获取到cameraStatuses的一个数组,这个里面存放了关于摄像头设备的挂载信息,遍历这个数组把他们放进mDeviceStatus中。

被动:当然这个类里面同样重写了onStatusChanged函数,并且这里面同样调用了onStatusChangedLocked,这样当cameraService想要通知当前的摄像设备的挂载状态时可以通过这个回调获取了

private void connectCameraServiceLocked() {
    //...
    IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
    //...
    ICameraService cameraService = ICameraService.Stub.asInterface(cameraServiceBinder);

    try {
        CameraMetadataNative.setupGlobalVendorTagDescriptor();
    } catch (ServiceSpecificException e) {
        handleRecoverableSetupErrors(e);
    }

    try {
        CameraStatus[] cameraStatuses = cameraService.addListener(this);
        for (CameraStatus c : cameraStatuses) {
            onStatusChangedLocked(c.status, c.cameraId);
        }
        mCameraService = cameraService;
    } catch(ServiceSpecificException e) {
        // Unexpected failure
        throw new IllegalStateException("Failed to register a camera service listener", e);
    } catch (RemoteException e) {
        // Camera service is now down, leave mCameraService as null
    }
}
//####################################################################

@Override
public void onStatusChanged(int status, String cameraId) throws RemoteException {
    synchronized(mLock) {
        onStatusChangedLocked(status, cameraId);
    }
}


那当cameraManager被注册到cameraService后是如何首次返回摄像设备的挂载信息和在后续挂载信息改变时通知cameraManager的呢

首次获取

? ? ? ? 首次获取肯定是因为调用了注册监听的接口后返回的值

addListener? ? ?ICameraService.aidl

路径??frameworks/av/camera/aidl/android/hardware/ICameraService.aidl

CameraStatus[] addListener(ICameraServiceListener listener);

?动态插拔

? ? ? ? 动态插拔是调用观察者们的onStatusChanged函数

onStatusChanged? ??ICameraServiceListener.aidl

路径??frameworks/av/camera/aidl/android/hardware/ICameraServiceListener.aidl

oneway void onStatusChanged(int status, String cameraId);

CameraService::addListener

路径??frameworks/av/services/camera/libcameraservice/CameraService.cpp

????????emplace_back? 不需要构造添加进map

主动:cameraStatuses 是由vector结构保存的hardware::CameraStatus 而这个结构中的数据是由mCameraStates提供的

? ? ? ? ? ?mCameraStates?是一个map结构

在这个函数中明显能看出设备的挂载信息是由mCameraStates vector结构 提供的,而后利用这个vector结构 调用返回cameraStatuses,那么就应该查找mCameraStates结构是何时填充的

被动:CameraService收到设备挂载信息后,可以调用这个mListenerList结构中的观察者的onStatusChanged函数从而改变挂载设备数?

Status CameraService::addListener(const sp<ICameraServiceListener>& listener,
        /*out*/
        std::vector<hardware::CameraStatus> *cameraStatuses) {
    //...
    Mutex::Autolock lock(mServiceLock);
    {
        //...
        mListenerList.push_back(listener);
    }
    {
        Mutex::Autolock lock(mCameraStatesLock);
        for (auto& i : mCameraStates) {
            cameraStatuses->emplace_back(i.first, mapToInterface(i.second->getStatus()));
        }
    }
    //...
    return Status::ok();
}

// ####################################################################
//frameworks/av/services/camera/libcameraservice/CameraService.h
std::map<String8, std::shared_ptr<CameraState>> mCameraStates;

std::vector<sp<hardware::ICameraServiceListener>> mListenerList;

addStates? ? mCameraStates的填充? ?

路径??frameworks/av/services/camera/libcameraservice/CameraService.cpp

? ? ? ? 可以看出当调用CameraService::addStates?mCameraStates中的数据被更新

void CameraService::addStates(const String8 id) {
    std::string cameraId(id.c_str());
    hardware::camera::common::V1_0::CameraResourceCost cost;
    status_t res = mCameraProviderManager->getResourceCost(cameraId, &cost);
    //...
    std::set<String8> conflicting;
    for (size_t i = 0; i < cost.conflictingDevices.size(); i++) {
        conflicting.emplace(String8(cost.conflictingDevices[i].c_str()));
    }
    {
        Mutex::Autolock lock(mCameraStatesLock);
        mCameraStates.emplace(id, std::make_shared<CameraState>(id, cost.resourceCost,
                                                                conflicting));
    }
    //...
    updateCameraNumAndIds();
    logDeviceAdded(id, "Device added");
}

onDeviceStatusChanged? addStates的调用? ?

路径??frameworks/av/services/camera/libcameraservice/CameraService.cpp

主动:这里认为当主动调用onDeviceStatusChanged函数时addStates函数则被调用

? ? ? ? 根据上一篇CameraService启动流程分析enumerateProviders流程中onDeviceStatusChanged被主动调用获取当前设备挂载状态,具体可参考Android Camera(三) CameraService启动流程 (androidP)_we1less的博客-CSDN博客

? ? ? ? 从这部分代码可以知道enumerateProviders流程中会向mCameraProviderManager请求deviceIds并遍历这个数据结构,调用onDeviceStatusChanged的PRESENT,这样相机列表就被添加进去了,接下来继续分析mCameraProviderManager->getCameraDeviceIds()

被动:同样的在该函数中存在一个updateStatus函数,这个函数就是上文中提到的,可以用来通知观察者们设备挂载信息改变了,从而完成被动调用流程,注意当调用enumerateProviders函数时,同样也调用了mCameraProviderManager->initialize(this);函数,将CameraService作为监听者注册到mCameraProviderManager中,这也是mCameraProviderManager能回调调用onDeviceStatusChanged函数并且调用updateStatus函数回调的一个基础。

void CameraService::onDeviceStatusChanged(const String8& id,
        CameraDeviceStatus newHalStatus) {
    ALOGI("%s: Status changed for cameraId=%s, newStatus=%d", __FUNCTION__,
            id.string(), newHalStatus);

    StatusInternal newStatus = mapToInternal(newHalStatus);

    std::shared_ptr<CameraState> state = getCameraState(id);

    if (state == nullptr) {
        if (newStatus == StatusInternal::PRESENT) {
            ALOGI("%s: Unknown camera ID %s, a new camera is added",
                    __FUNCTION__, id.string());

            // First add as absent to make sure clients are notified below
            addStates(id);

            updateStatus(newStatus, id);
        } else {
            ALOGE("%s: Bad camera ID %s", __FUNCTION__, id.string());
        }
        return;
    }
    //...
}
//####################################################

status_t CameraService::enumerateProviders() {    
     ...
     if (nullptr == mCameraProviderManager.get()) {
            mCameraProviderManager = new CameraProviderManager();
            //被动注册
            res = mCameraProviderManager->initialize(this);
            ...
        }
    std::vector<std::string> deviceIds;
        deviceIds = mCameraProviderManager->getCameraDeviceIds();
    ...
    for (auto& cameraId : deviceIds) {
        String8 id8 = String8(cameraId.c_str());
        onDeviceStatusChanged(id8, CameraDeviceStatus::PRESENT);
    }
    return OK;
}


//####################################################
void CameraService::updateStatus(StatusInternal status, const String8& cameraId,
        std::initializer_list<StatusInternal> rejectSourceStates) {

    auto state = getCameraState(cameraId);
    //...
            //回调的过程
            for (auto& listener : mListenerList) {
                listener->onStatusChanged(mapToInterface(status), String16(cameraId));
            }
        });
}

getCameraDeviceIds

路径??frameworks/av/services/camera/libcameraservice/common/CameraProviderManager.cpp

主动:这里可以看出先遍历mProviders这是一个由ProviderInfo对象组成的一个vector结构?并从其中的每个对象获取CameraId

????????mProviders在上一篇文章中已经介绍过了那么现在开始追溯provider->mUniqueCameraIds的填充

被动:在上一层的CameraService会在CameraProviderManager::initialize函数中被存放到CameraProviderManager::mListener中,在addProviderLocked函数中还会把CameraProviderManager对象作为mManager成员构造成ProviderInfo对象并添加到mProviders中,紧接着调用providerInfo->initialize(这个是回调的关键下面会说明),这样就可以通过ProviderInfo对象,获取其中的mManager成员(也就是CameraProviderManager对象这个mManager对象中保存着成员mListener也就是CameraService)。可以通过CameraProviderManager::getStatusListener函数获取到这个成员mListener。

std::vector<std::string> CameraProviderManager::getCameraDeviceIds() const {
    std::lock_guard<std::mutex> lock(mInterfaceMutex);
    std::vector<std::string> deviceIds;
    for (auto& provider : mProviders) {
        for (auto& id : provider->mUniqueCameraIds) {
            deviceIds.push_back(id);
        }
    }
    return deviceIds;
}

//########################################################################
//frameworks/av/services/camera/libcameraservice/common/CameraProviderManager.h
std::vector<sp<ProviderInfo>> mProviders;

//########################################################################
status_t CameraProviderManager::initialize(wp<CameraProviderManager::StatusListener> listener,
        ServiceInteractionProxy* proxy) {
    //...
    mListener = listener;
    mServiceProxy = proxy;
    //...
    addProviderLocked(kLegacyProviderName, /*expected*/ false);
    addProviderLocked(kExternalProviderName, /*expected*/ false);

    return OK;
}

//########################################################################
//frameworks/av/services/camera/libcameraservice/common/CameraProviderManager.h
wp<StatusListener> mListener;

//########################################################################
//frameworks/av/services/camera/libcameraservice/common/CameraProviderManager.cpp
status_t CameraProviderManager::addProviderLocked(const std::string& newProvider, bool expected) {
    //...
    sp<ProviderInfo> providerInfo =
            new ProviderInfo(newProvider, interface, this);
    status_t res = providerInfo->initialize();
    //...
    mProviders.push_back(providerInfo);
    return OK;
}

CameraProviderManager::ProviderInfo::addDevice? ?mUniqueCameraIds的填充

路径??frameworks/av/services/camera/libcameraservice/common/CameraProviderManager.cpp

主动:当调用了ProviderInfo::addDevice后,才会向mUniqueCameraIds中添加数据

上一篇文章中也提到addDevice是由ProviderInfo::initialize函数调用的,这个函数执行期间还调用了mInterface->getCameraIdList 而ProviderInfo::mInterface是CameraProvider的代理 参考

Android Camera(三) CameraService启动流程 (androidP)_we1less的博客-CSDN博客

? ? ? ? 也就是说代码会调用到CameraProvidergetCameraIdList

被动:这里注意ProviderInfo::initialize这里会调用setCallbackProviderInfo作为CameraProvider的监听对象注册进去了,这一步在上一层已经说明了调用流程。

此时的ProviderInfo中存在着mManager成员(也就是CameraProviderManager对象),调用该对象的getStatusListener函数可以获取CameraProviderManager对象mListener成员(CameraService),此时调用这个listener(CameraService)的onDeviceStatusChanged方法可以完成回调流程。那么对于CameraProvider来讲就可以调用cameraDeviceStatusChange完成对上层的回调

status_t CameraProviderManager::ProviderInfo::addDevice(const std::string& name,
        CameraDeviceStatus initialStatus, /*out*/ std::string* parsedId) {
    //...
    status_t res = parseDeviceName(name, &major, &minor, &type, &id);
    //...
    std::unique_ptr<DeviceInfo> deviceInfo;
    //...
    deviceInfo->mStatus = initialStatus;
    bool isAPI1Compatible = deviceInfo->isAPI1Compatible();

    mDevices.push_back(std::move(deviceInfo));

    mUniqueCameraIds.insert(id);
    if (isAPI1Compatible) {
        mUniqueAPI1CompatibleCameraIds.push_back(id);
    }
    //...
    return OK;
}
//###############################################################################
//frameworks/av/services/camera/libcameraservice/common/CameraProviderManager.cpp
status_t CameraProviderManager::ProviderInfo::initialize() {
    //...
    hardware::Return<Status> status = mInterface->setCallback(this);
    //...
    std::vector<std::string> devices;
    hardware::Return<void> ret = mInterface->getCameraIdList([&status, &devices](
            Status idStatus,
            const hardware::hidl_vec<hardware::hidl_string>& cameraDeviceNames) {
        status = idStatus;
        if (status == Status::OK) {
            for (size_t i = 0; i < cameraDeviceNames.size(); i++) {
                //将返回的设备填装到devices中
                devices.push_back(cameraDeviceNames[i]);
            }
        } });
    ...
 
    sp<StatusListener> listener = mManager->getStatusListener();
    for (auto& device : devices) {
        std::string id;
        status_t res = addDevice(device,
                hardware::camera::common::V1_0::CameraDeviceStatus::PRESENT, &id);
        ...
    }
    ...
    mInitialized = true;
    return OK;
}

//###############################################################################
hardware::Return<void> CameraProviderManager::ProviderInfo::cameraDeviceStatusChange(
        const hardware::hidl_string& cameraDeviceName,
        CameraDeviceStatus newStatus) {
    sp<StatusListener> listener;
    std::string id;
    bool initialized = false;
    {
        std::lock_guard<std::mutex> lock(mLock);
        bool known = false;
        //...
        if (!known) {
            //...
            addDevice(cameraDeviceName, newStatus, &id);
        } else if (newStatus == CameraDeviceStatus::NOT_PRESENT) {
            removeDevice(id);
        }
        listener = mManager->getStatusListener();
        initialized = mInitialized;
    }
    if (listener != nullptr && initialized) {
        listener->onDeviceStatusChanged(String8(id.c_str()), newStatus);
    }
    return hardware::Void();
}

CameraProvider::getCameraIdList (主动)

CameraProvider::setCallback (被动)

路径??hardware/interfaces/camera/provider/2.4/default/CameraProvider.cpp

主动:这里面可知返回的是由mCameraDeviceNames (Vector)数组里面的deviceNamePair.second组成的一个(Vector)数组,接下来看看mCameraDeviceNames是何时被填充的

被动:可知上一层的ProviderInfo作为这一层的mCallbacks成员存在,这样就可以在本层调用ProviderInfo::cameraDeviceStatusChange函数,完成对上层的回调。

Return<void> CameraProvider::getCameraIdList(getCameraIdList_cb _hidl_cb)  {
    std::vector<hidl_string> deviceNameList;
    for (auto const& deviceNamePair : mCameraDeviceNames) {
        if (mCameraStatusMap[deviceNamePair.first] == CAMERA_DEVICE_STATUS_PRESENT) {
            deviceNameList.push_back(deviceNamePair.second);
        }
    }
    hidl_vec<hidl_string> hidlDeviceNameList(deviceNameList);
    _hidl_cb(Status::OK, hidlDeviceNameList);
    return Void();
}

//#####################################################################
//hardware/interfaces/camera/provider/2.4/default/CameraProvider.h
SortedVector<std::pair<std::string, std::string>> mCameraDeviceNames;

//#####################################################################
Return<Status> CameraProvider::setCallback(const sp<ICameraProviderCallback>& callback)  {
    Mutex::Autolock _l(mCbLock);
    mCallbacks = callback;
    return Status::OK;
}

CameraProvider::addDeviceNames

路径??hardware/interfaces/camera/provider/2.4/default/CameraProvider.cpp

主动:在这个CameraProvider::addDeviceNames函数中mCameraDeviceNames被填充的

被动:在这个addDeviceNames环节中会调用mCallbackscameraDeviceStatusChange进行回调,由于CameraProvider是继承自结构体camera_module_callbacks_t的,所以可以向so中提供sCameraDeviceStatusChange指针提其在挂载变动时调用。在sCameraDeviceStatusChange函数中会调用mCallbackscameraDeviceStatusChange进行回调流程。

void CameraProvider::addDeviceNames(int camera_id, CameraDeviceStatus status, bool cam_new)
{
    //...
    int deviceVersion = mModule->getDeviceVersion(camera_id);
    auto deviceNamePair = std::make_pair(cameraIdStr,
                                         getHidlDeviceName(cameraIdStr, deviceVersion));
    mCameraDeviceNames.add(deviceNamePair);
    if (cam_new) {
        mCallbacks->cameraDeviceStatusChange(deviceNamePair.second, status);
    }
    //...
}

//#######################################################################
void CameraProvider::sCameraDeviceStatusChange(
        const struct camera_module_callbacks* callbacks,
        int camera_id,
        int new_status) {
    CameraProvider* cp = const_cast<CameraProvider*>(
            static_cast<const CameraProvider*>(callbacks));
    bool found = false;

    if (cp == nullptr) {
        ALOGE("%s: callback ops is null", __FUNCTION__);
        return;
    }

    Mutex::Autolock _l(cp->mCbLock);
    char cameraId[kMaxCameraIdLen];
    snprintf(cameraId, sizeof(cameraId), "%d", camera_id);
    std::string cameraIdStr(cameraId);
    cp->mCameraStatusMap[cameraIdStr] = (camera_device_status_t) new_status;
    if (cp->mCallbacks != nullptr) {
        CameraDeviceStatus status = (CameraDeviceStatus) new_status;
        for (auto const& deviceNamePair : cp->mCameraDeviceNames) {
            if (cameraIdStr.compare(deviceNamePair.first) == 0) {
                cp->mCallbacks->cameraDeviceStatusChange(
                        deviceNamePair.second, status);
                found = true;
            }
        }

        switch (status) {
        case CameraDeviceStatus::PRESENT:
        case CameraDeviceStatus::ENUMERATING:
            if (!found) {
                cp->addDeviceNames(camera_id, status, true);
            }
            break;
        case CameraDeviceStatus::NOT_PRESENT:
            if (found) {
                cp->removeDeviceNames(camera_id);
            }
        }
    }
}

//#######################################################################
CameraProvider::CameraProvider() :
        camera_module_callbacks_t({sCameraDeviceStatusChange,
                                   sTorchModeStatusChange}) {
    mInitFailed = initialize();
}

//#######################################################################
//hardware/interfaces/camera/provider/2.4/default/CameraProvider.h
struct CameraProvider : public ICameraProvider, public camera_module_callbacks_t

CameraProvider::initialize

路径? hardware/interfaces/camera/provider/2.4/default/CameraProvider.cpp

主动:最后是在?CameraProvider初始化的时候向so中调用获取的

被动:会调用mModule->setCallbacks(this);CameraProvider自己注册进去,这样so中就可以利用这个结构体中sCameraDeviceStatusChange指针进行回调,的一旦出现设备挂载变动就调用回调向上传递了。

bool CameraProvider::initialize() {
    camera_module_t *rawModule;
    int err = hw_get_module(CAMERA_HARDWARE_MODULE_ID,
            (const hw_module_t **)&rawModule);
    //...
    mModule = new CameraModule(rawModule);
    err = mModule->init();
    //...
    err = mModule->setCallbacks(this);
    //...
    mNumberOfLegacyCameras = mModule->getNumberOfCameras();
    for (int i = 0; i < mNumberOfLegacyCameras; i++) {
        //...
        addDeviceNames(i);
    }
    return false; // mInitFailed
}

//#####################################################################
//hardware/interfaces/camera/common/1.0/default/CameraModule.cpp
int CameraModule::getNumberOfCameras() {
    int numCameras;
    ATRACE_BEGIN("camera_module->get_number_of_cameras");
    numCameras = mModule->get_number_of_cameras();
    ATRACE_END();
    return numCameras;
}

int CameraModule::setCallbacks(const camera_module_callbacks_t *callbacks) {
    int res = OK;
    ATRACE_BEGIN("camera_module->set_callbacks");
    if (getModuleApiVersion() >= CAMERA_MODULE_API_VERSION_2_1) {
        res = mModule->set_callbacks(callbacks);
    }
    ATRACE_END();
    return res;
}

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-11-18 11:17:50  更:2021-11-18 11:19:34 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:54:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码