首页 > 其他分享 >Android Media Framework(十七)ACodec - Ⅴ

Android Media Framework(十七)ACodec - Ⅴ

时间:2024-08-11 11:24:25浏览次数:9  
标签:info mCodec err buffer Media Android BufferInfo ACodec

本篇文章我们一起来分析Executing状态下的数据处理流程。

首先对上一篇文章做勘误:
实际在调用allocateOutputBuffersFromNativeWindow分配buffer时我们会看到,一开始确实是分配了nBufferCountActual个buffer,但是后面又调用cancelBufferToNativeWindow销毁掉了备用的。可能有人要问,前面调用了useBuffer把备用的buffer也共享给了组件,会不会有问题呢?这些被销毁的buffer是无法被使用的,实际能用的buffer数量就是nBufferCountMin个,至于为什么后续的文章会做了解。

画线部分修改为:实际能用的buffer数量是nBufferCountActual个,被cancel的buffer到后面会重新dequeue出来。

1、ExecutingState::resume

在 Android Media Framework(十五)ACodec - Ⅲ 一文最后,我们讲到组件正式切换到OMX_Executing状态后,ACodec会在IdleToExecutingState状态下先调用ExecutingState::resume方法,然后切换至ExecutingState,正式启动数据处理流程。

void ACodec::ExecutingState::resume() {
    if (mActive) {
        return;
    }

    submitOutputBuffers();

    if (mCodec->mBuffers[kPortIndexInput].size() == 0u) {
    }

    for (size_t i = 0; i < mCodec->mBuffers[kPortIndexInput].size(); i++) {
        BufferInfo *info = &mCodec->mBuffers[kPortIndexInput].editItemAt(i);
        if (info->mStatus == BufferInfo::OWNED_BY_US) {
            postFillThisBuffer(info);
        }
    }

    mActive = true;
}

组件刚启动时,除了Tunnel Mode下的output buffer和kPortModeDynamicANWBuffer下的output buffer不属于ACodec外,其他所有的buffer都归属于ACodec。resume会将ACodec持有的input buffer交给上层使用,将output buffer交给组件使用。

resume除了会在启动时调用,还会在flush之后的重启过程中用到。

先来看提交output buffer的过程:

void ACodec::ExecutingState::submitOutputBuffers() {
    submitRegularOutputBuffers();
    if (mCodec->storingMetadataInDecodedBuffers()) {
        submitOutputMetaBuffers();
    }
}

submitRegularOutputBuffers

从名字上来看submitRegularOutputBuffers这个方法,是用来提交常规的Output Buffer的,Regular在这里指的是所有归属于ACodec的output buffer。

void ACodec::ExecutingState::submitRegularOutputBuffers() {
    bool failed = false;
    for (size_t i = 0; i < mCodec->mBuffers[kPortIndexOutput].size(); ++i) {
        BufferInfo *info = &mCodec->mBuffers[kPortIndexOutput].editItemAt(i);

        if (mCodec->mNativeWindow != NULL) {
            // 1.
            if (info->mStatus != BufferInfo::OWNED_BY_US
                    && info->mStatus != BufferInfo::OWNED_BY_NATIVE_WINDOW) {
                failed = true;
                break;
            }
            // 2.
            if (info->mStatus == BufferInfo::OWNED_BY_NATIVE_WINDOW) {
                continue;
            }
        } else {
            // 3.
            if (info->mStatus != BufferInfo::OWNED_BY_US) {
                ALOGE("buffers should be owned by us");
                failed = true;
                break;
            }
        }
        // 4.
        info->checkWriteFence("submitRegularOutputBuffers");
        // 5.
        status_t err = mCodec->fillBuffer(info);
        if (err != OK) {
            failed = true;
            break;
        }
    }

    if (failed) {
        mCodec->signalError(OMX_ErrorUndefined, FAILED_TRANSACTION);
    }
}

简单分析一下submitRegularOutputBuffers,看看我们说的对不对:

  1. 有native window时,检查output buffer是属于ACodec还是属于native window,如果都不是就返回error;
  2. 检查output buffer是否属于native window,如果是则不会提交给组件使用;
  3. 没有native window时,如果output buffer不属于ACodec,就会返回error;
  4. 调用ACodec::BufferInfo的checkWriteFence方法,这个方法没有什么实质性作用,只做log提醒。提交buffer给组件使用,是想要组件做数据写入,因此graphic output buffer持有的是write fence。
  5. 调用ACodec::fillBuffer将buffer提交给组件。

阅读上述流程我们可以知道,有native window的情况下,启动时:

  • PortMode为kPortModeDynamicANWBuffer时,output buffer归属于native window,所以没有buffer要提交;Tunnel Mode没有output buffer,因此也无需提交。
  • 其他情况下会把所有属于ACodec的buffer都提交。
status_t ACodec::fillBuffer(BufferInfo *info) {
    status_t err;
    if (!storingMetadataInDecodedBuffers() || !info->mNewGraphicBuffer) {
        err = mOMXNode->fillBuffer(
            info->mBufferID, OMXBuffer::sPreset, info->mFenceFd);
    } else {
        err = mOMXNode->fillBuffer(
            info->mBufferID, info->mGraphicBuffer, info->mFenceFd);
    }

    info->mNewGraphicBuffer = false;
    info->mFenceFd = -1;
    if (err == OK) {
        info->mStatus = BufferInfo::OWNED_BY_COMPONENT;
    }
    return err;
}

fillBuffer有一段注释,翻译出来是这样:“在dynamic ANW buffer mode下,如果graphic buffer没有变化,那么用sPreset代替相同的graphic buffer送给组件,组件端就不需要更新meta data。”

调用OMXNode的fillBuffer时第二个参数可能有两种:

  • OMXBuffer::sPreset:当PortMode不为kPortModeDynamicANWBuffer时,或者PortMode为kPortModeDynamicANWBuffer但是buffer没有被更新时,使用此case;
  • info->mGraphicBuffer:当PortMode为kPortModeDynamicANWBuffer且buffer被更新时,使用此case。
status_t OMXNodeInstance::fillBuffer(
        IOMX::buffer_id buffer, const OMXBuffer &omxBuffer, int fenceFd) {

    if (omxBuffer.mBufferType == OMXBuffer::kBufferTypeANWBuffer) {
        // 更新meta data
        status_t err = updateGraphicBufferInMeta_l(
                kPortIndexOutput, omxBuffer.mGraphicBuffer, buffer, header);
    } else if (omxBuffer.mBufferType != OMXBuffer::kBufferTypePreset) {
        return BAD_VALUE;
    }
    // 更新fence
    status_t res = storeFenceInMeta_l(header, fenceFd, kPortIndexOutput);
    if (res != OK) {
        return res;
    }

    OMX_ERRORTYPE err = OMX_FillThisBuffer(mHandle, header);

    return StatusFromOMXError(err);
}

调用fillBuffer时如果传入的是GraphicBuffer,将会调用updateGraphicBufferInMeta_l更新BufferHeader的pBuffer字段存储的meta data。如果kBufferTypePreset,说明meta data无需更新。

submitOutputMetaBuffers

调用submitOutputMetaBuffers有一个条件,需要PortMode为kPortModeDynamicANWBuffer。

void ACodec::ExecutingState::submitOutputMetaBuffers() {
    for (size_t i = 0; i < mCodec->mBuffers[kPortIndexInput].size(); ++i) {
        BufferInfo *info = &mCodec->mBuffers[kPortIndexInput].editItemAt(i);

        if (info->mStatus == BufferInfo::OWNED_BY_COMPONENT) {
            if (mCodec->submitOutputMetadataBuffer() != OK)
                break;
        }
    }
}

调用submitOutputMetadataBuffer又要有一个条件,需要有input buffer属于组件,是不是有点奇怪?起播时,所有的input buffer都不属于组件,因此并不会调用submitOutputMetadataBuffer。这个方法有什么用呢?它其实是用在OutputPortSettingsChangedState状态下的,我们后续再对此展开。

postFillThisBuffer

当input buffer归属于ACodec时,可以调用BaseState::postFillThisBuffer将它送给上层使用:

void ACodec::BaseState::postFillThisBuffer(BufferInfo *info) {
    // 1.
    if (mCodec->mPortEOS[kPortIndexInput]) {
        return;
    }

    CHECK_EQ((int)info->mStatus, (int)BufferInfo::OWNED_BY_US);
    // 2.
    info->mData->setFormat(mCodec->mInputFormat);
    // 3.
    mCodec->mBufferChannel->fillThisBuffer(info->mBufferID);
    // 4.
    info->mData.clear();
    // 5.
    info->mStatus = BufferInfo::OWNED_BY_UPSTREAM;
}
  1. 如果ACodec已经收到input eos,那么input buffer将不会再回传给上层;
  2. 将input format设定到mData中;
  3. 调用ACodecBufferChannel的fillThisBuffer方法,将可用buffer id送给上层;
  4. 清除ACodec对mData的引用;
  5. 将BufferInfo的归属设定为OWNED_BY_UPSTREAM。

2、BaseState::onInputBufferFilled

上层向input buffer写完数据后会调用ACodecBufferChannel的queueInputBuffer或者queueSecureInputBuffer向ACodec发送一条kWhatInputBufferFilled消息。由于ExecutingState没有实现对该消息的处理逻辑,因此最终调用到BaseState::onInputBufferFilled。

onInputBufferFilled方法很长,我将代码全部贴出并且补上注释,可以先从上到下阅读:

void ACodec::BaseState::onInputBufferFilled(const sp<AMessage> &msg) {
    IOMX::buffer_id bufferID;
    CHECK(msg->findInt32("buffer-id", (int32_t*)&bufferID));
    sp<MediaCodecBuffer> buffer;
    int32_t err = OK;
    bool eos = false;
    // 获取当前对buffer的处理方式
    PortMode mode = getPortMode(kPortIndexInput);
    int32_t discarded = 0;
    // 检查是否调用的是discardBuffer
    if (msg->findInt32("discarded", &discarded) && discarded) {
        // 如果是将mode切换为KEEP_BUFFERS
        mode = KEEP_BUFFERS;
    }
    sp<RefBase> obj;
    CHECK(msg->findObject("buffer", &obj));
    buffer = static_cast<MediaCodecBuffer *>(obj.get());

    int32_t tmp;
    if (buffer != NULL && buffer->meta()->findInt32("eos", &tmp) && tmp) {
        eos = true;
        err = ERROR_END_OF_STREAM;
    }

    BufferInfo *info = mCodec->findBufferByID(kPortIndexInput, bufferID);
    BufferInfo::Status status = BufferInfo::getSafeStatus(info);
    // 检查bufferInfo存储的状态
    if (status != BufferInfo::OWNED_BY_UPSTREAM) {
        mCodec->dumpBuffers(kPortIndexInput);
        mCodec->signalError(OMX_ErrorUndefined, FAILED_TRANSACTION);
        return;
    }

    // 修改bufferInfo的状态,并且重新添加mData对buffer的引用
    info->mStatus = BufferInfo::OWNED_BY_US;
    info->mData = buffer;
    // 根据mode对buffer做不同的处理
    switch (mode) {
        case KEEP_BUFFERS:
        {
            // 如果是keep buffers,那么ACodec会持有buffer,什么都不做
            if (eos) {
                if (!mCodec->mPortEOS[kPortIndexInput]) {
                    mCodec->mPortEOS[kPortIndexInput] = true;
                    mCodec->mInputEOSResult = err;
                }
            }
            break;
        }
        // 如果是resubmit buffer就将buffer提交给组件使用
        case RESUBMIT_BUFFERS:
        {
            // 如果buffer不为NULL且没有收到input eos
            if (buffer != NULL && !mCodec->mPortEOS[kPortIndexInput]) {
                // 如果buffer数据为空,并且不是eos,那么重新将buffer送给上层填充
                if (buffer->size() == 0 && !eos) {
                    postFillThisBuffer(info);
                    break;
                }

                int64_t timeUs;
                CHECK(buffer->meta()->findInt64("timeUs", &timeUs));
                // 设定eof,表示当前是完整的一帧
                OMX_U32 flags = OMX_BUFFERFLAG_ENDOFFRAME;

                int32_t isCSD = 0;
                if (buffer->meta()->findInt32("csd", &isCSD) && isCSD != 0) {
                    if (mCodec->mIsLegacyVP9Decoder) {
                        ALOGV("[%s] is legacy VP9 decoder. Ignore %u codec specific data",
                            mCodec->mComponentName.c_str(), bufferID);
                        postFillThisBuffer(info);
                        break;
                    }
                    // 如果包含csd buffer,拉起对应的标志位
                    flags |= OMX_BUFFERFLAG_CODECCONFIG;
                }
                // 如果是eos,那么拉起eos flag
                if (eos) {
                    flags |= OMX_BUFFERFLAG_EOS;
                }

                size_t size = buffer->size();
                size_t offset = buffer->offset();
                // 如果需要做数据转换,那么就把mData中的数据转换到mCodecData中
                if (buffer->base() != info->mCodecData->base()) {
                    sp<DataConverter> converter = mCodec->mConverter[kPortIndexInput];
                    if (converter == NULL || isCSD) {
                        converter = getCopyConverter();
                    }
                    status_t err = converter->convert(buffer, info->mCodecData);
                    if (err != OK) {
                        mCodec->signalError(OMX_ErrorUndefined, err);
                        return;
                    }
                    size = info->mCodecData->size();
                } else {
                    // 设定buffer信息
                    info->mCodecData->setRange(offset, size);
                }

                if (flags & OMX_BUFFERFLAG_CODECCONFIG) {
                    ALOGV("[%s] calling emptyBuffer %u w/ codec specific data",
                         mCodec->mComponentName.c_str(), bufferID);
                } else if (flags & OMX_BUFFERFLAG_EOS) {
                    ALOGV("[%s] calling emptyBuffer %u w/ EOS",
                         mCodec->mComponentName.c_str(), bufferID);
                } else {
#if TRACK_BUFFER_TIMING
                    ALOGI("[%s] calling emptyBuffer %u w/ time %lld us",
                         mCodec->mComponentName.c_str(), bufferID, (long long)timeUs);
#else
                    ALOGV("[%s] calling emptyBuffer %u w/ time %lld us",
                         mCodec->mComponentName.c_str(), bufferID, (long long)timeUs);
#endif
                }

#if TRACK_BUFFER_TIMING
                // 记录buffer写入时间
                ACodec::BufferStats stats;
                stats.mEmptyBufferTimeUs = ALooper::GetNowUs();
                stats.mFillBufferDoneTimeUs = -1ll;
                mCodec->mBufferStats.add(timeUs, stats);
#endif

                if (mCodec->storingMetadataInDecodedBuffers()) {
                    // try to submit an output buffer for each input buffer
                    // 获取输出端口的buffer处理方式,和输入端口的处理方式相同
                    PortMode outputMode = getPortMode(kPortIndexOutput);

                    ALOGV("MetadataBuffersToSubmit=%u portMode=%s",
                            mCodec->mMetadataBuffersToSubmit,
                            (outputMode == FREE_BUFFERS ? "FREE" :
                             outputMode == KEEP_BUFFERS ? "KEEP" : "RESUBMIT"));
                    if (outputMode == RESUBMIT_BUFFERS) {
                        // 调用submitOutputMetadataBuffer
                        status_t err = mCodec->submitOutputMetadataBuffer();
                        if (mCodec->mIsLowLatency
                                && err == OK
                                && mCodec->mMetadataBuffersToSubmit > 0) {
                            maybePostExtraOutputMetadataBufferRequest();
                        }
                    }
                }
                info->checkReadFence("onInputBufferFilled");

                // 调用OMXNode的emptyBuffer,将数据交给组件
                status_t err2 = OK;
                switch (mCodec->mPortMode[kPortIndexInput]) {
                case IOMX::kPortModePresetByteBuffer:
                case IOMX::kPortModePresetANWBuffer:
                case IOMX::kPortModePresetSecureBuffer:
                    {
                        err2 = mCodec->mOMXNode->emptyBuffer(
                            bufferID, info->mCodecData, flags, timeUs, info->mFenceFd);
                    }
                    break;
#ifndef OMX_ANDROID_COMPILE_AS_32BIT_ON_64BIT_PLATFORMS
                case IOMX::kPortModeDynamicNativeHandle:
                    if (info->mCodecData->size() >= sizeof(VideoNativeHandleMetadata)) {
                        VideoNativeHandleMetadata *vnhmd =
                            (VideoNativeHandleMetadata*)info->mCodecData->base();
                        sp<NativeHandle> handle = NativeHandle::create(
                                vnhmd->pHandle, false /* ownsHandle */);
                        err2 = mCodec->mOMXNode->emptyBuffer(
                            bufferID, handle, flags, timeUs, info->mFenceFd);
                    }
                    break;
                case IOMX::kPortModeDynamicANWBuffer:
                    if (info->mCodecData->size() >= sizeof(VideoNativeMetadata)) {
                        VideoNativeMetadata *vnmd = (VideoNativeMetadata*)info->mCodecData->base();
                        sp<GraphicBuffer> graphicBuffer = GraphicBuffer::from(vnmd->pBuffer);
                        err2 = mCodec->mOMXNode->emptyBuffer(
                            bufferID, graphicBuffer, flags, timeUs, info->mFenceFd);
                    }
                    break;
#endif
                default:
                    ALOGW("Can't marshall %s data in %zu sized buffers in %zu-bit mode",
                            asString(mCodec->mPortMode[kPortIndexInput]),
                            info->mCodecData->size(),
                            sizeof(buffer_handle_t) * 8);
                    err2 = ERROR_UNSUPPORTED;
                    break;
                }

                info->mFenceFd = -1;
                if (err2 != OK) {
                    mCodec->signalError(OMX_ErrorUndefined, makeNoSideEffectStatus(err2));
                    return;
                }
                // 切换BufferInfo的状态
                info->mStatus = BufferInfo::OWNED_BY_COMPONENT;
                info->mData = buffer;
                // 如果没有到input eos,那么尝试将ACodec持有的一个input buffer送给上层填充
                if (!eos && err == OK) {
                    getMoreInputDataIfPossible();
                } else {
                    ALOGV("[%s] Signalled EOS (%d) on the input port",
                         mCodec->mComponentName.c_str(), err);
                    // 如果收到eos,那么就将mPortEOS置为true
                    mCodec->mPortEOS[kPortIndexInput] = true;
                    mCodec->mInputEOSResult = err;
                }
            } else if (!mCodec->mPortEOS[kPortIndexInput]) {
                // 还有一种情况,buffer等于NULL,且还未收到Input EOS
                if (err != OK && err != ERROR_END_OF_STREAM) {
                    ALOGV("[%s] Signalling EOS on the input port due to error %d",
                         mCodec->mComponentName.c_str(), err);
                } else {
                    ALOGV("[%s] Signalling EOS on the input port",
                         mCodec->mComponentName.c_str());
                }

                ALOGV("[%s] calling emptyBuffer %u signalling EOS",
                     mCodec->mComponentName.c_str(), bufferID);

                info->checkReadFence("onInputBufferFilled");
                // 单独给组件送一个input eos
                status_t err2 = mCodec->mOMXNode->emptyBuffer(
                        bufferID, OMXBuffer::sPreset, OMX_BUFFERFLAG_EOS, 0, info->mFenceFd);
                info->mFenceFd = -1;
                if (err2 != OK) {
                    mCodec->signalError(OMX_ErrorUndefined, makeNoSideEffectStatus(err2));
                    return;
                }
                // 切换BufferInfo状态,并且将mPortEOS置为true。
                info->mStatus = BufferInfo::OWNED_BY_COMPONENT;

                mCodec->mPortEOS[kPortIndexInput] = true;
                mCodec->mInputEOSResult = err;
            }
            break;
        }
        // BaseState不会处理FREE_BUFFERS。
        case FREE_BUFFERS:
            break;

        default:
            ALOGE("invalid port mode: %d", mode);
            break;
    }
}

整体来说,BaseState::onInputBufferFilled的逻辑比较简单,提取并重新组织msg携带的信息,然后调用emptyBuffer将数据送入组件。里面有几点需要再提一下:

  • 如果PortMode为kPortModeDynamicANWBuffer,并且是在起播阶段,我们知道这时候Graphic Output Buffer是没有分配的。在onInputBufferFilled中会尝试调用submitOutputMetadataBuffer为每个input buffer提交一个对应的output buffer;
  • 数据写入成功后ACodec还会尝试将ACodec持有的一个input buffer送给上层填充,理想状态下每写入一个input buffer,就要向上层回传一个input buffer;
  • 在ExecutingState下,input和output PortMode都是RESUBMIT_BUFFERS;
  • 有两种方式通知组件Input EOS,一种是随着有效数据添加eos flag,还有一种是单独调用emptyBuffer通知组件,此时Buffer为NULL;
  • PortMode为kPortModeDynamicNativeHandle和kPortModeDynamicANWBuffer时,Metadata会写在mData中,传输数据时需要将buffer强转为对应类型的Metadata,然后从中获取handle/graphic buffer,再调用emptyBuffer通知组件更新Metadata。

3、submitOutputMetadataBuffer

status_t ACodec::submitOutputMetadataBuffer() {
    CHECK(storingMetadataInDecodedBuffers());
    if (mMetadataBuffersToSubmit == 0)
        return OK;

    BufferInfo *info = dequeueBufferFromNativeWindow();
    if (info == NULL) {
        return ERROR_IO;
    }

    ALOGV("[%s] submitting output meta buffer ID %u for graphic buffer %p",
          mComponentName.c_str(), info->mBufferID, info->mGraphicBuffer->handle);

    --mMetadataBuffersToSubmit;
    info->checkWriteFence("submitOutputMetadataBuffer");
    return fillBuffer(info);
}

4、dequeueBufferFromNativeWindow

5、BaseState::onOMXEmptyBufferDone

6、BaseState::onOMXFillBufferDone

7、BaseState::onOutputBufferDrained

关注公众号《青山渺渺》阅读全文
请添加图片描述

标签:info,mCodec,err,buffer,Media,Android,BufferInfo,ACodec
From: https://blog.csdn.net/qq_41828351/article/details/141103118

相关文章

  • 腾讯地图SDK Android版开发 3 地图图层
    腾讯地图SDKAndroid版开发3地图图层前言腾讯地图图层地图底图类型地图类图层类型常量接口路况图层接口示例代码地图风格类地图底图类型实时路况页面布局控件响应事件地图底图类型实时路况运行效果图前言本文主要介绍腾讯地图图层相关的功能和接口,以及使用方......
  • Java计算机毕业设计基于Android的校园网上拍卖平台(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在数字化校园建设的浪潮中,学生们对于便捷、高效的二手商品交易需求日益增长。传统的校园跳蚤市场受限于时间、空间等因素,难以满足学生群体对于多样化......
  • Java计算机毕业设计基于Android的生活记账小助手APP的设计与实现(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在快节奏的现代生活中,个人财务管理成为了许多人面临的一大挑战。随着智能手机的普及和移动互联网技术的飞速发展,移动应用成为辅助个人财务管理的得力......
  • 秒开WebView?Android性能优化全攻略
    在Android应用中,WebView组件被广泛用于显示网页内容。然而,由于WebView的一些固有限制和资源消耗,它可能会导致应用启动变慢或响应速度下降。下面是一些优化WebView性能的策略,以帮助你实现“秒开”效果:1.减少初始化时间• 延迟加载:不要在应用启动时立即初始化WebView,......
  • Java计算机毕业设计基于Android的校园网上拍卖平台(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,特别是移动互联网的普及,校园生活也日益数字化、便捷化。在传统校园市场中,二手物品的交换与拍卖往往受限于时间、空间和信息......
  • Android ndk string处理
    1.AndroidNDKNDK开发过程中常用的库定义在android-ndk-r25c/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android如libc++_shared.solibc++_static.alibstdc++.a库ndk工具链下载:./bin/sdkmanager--install"ndk;25.0.8775105"2.链接问......
  • CF1999F.Expected Median 题解
    一.前言这是一道很有趣的数学题目,用到了逆元和组合数学,非常适合新手练手。二.题目大意:给出一个长度为\(n\)的\(01\)数组。找出所有长度为\(k\)的子序列的中位数,求出其和。妥妥的数学题~三.分析首先考虑对答案的贡献。很明显,只有\(1\)作为中位数时才会做出贡献,于是......
  • Android开发基础08-掌握kotlin语言
    Kotlin是一种现代化的编程语言,作为Android开发的官方支持语言,越来越多的开发者选择使用Kotlin进行Android应用开发。在开始学习Android开发之前,掌握Kotlin语言的基础知识至关重要。1.基础知识a.开发环境设置安装JDK(JavaDevelopmentKit):Kotlin运行于JVM之上,因此需要先安......
  • Android开发基础07-掌握Java语言
    Android开发广泛使用Java作为编程语言,熟练掌握Java语言是十分必要的。1.基础入门知识a.设置开发环境安装JDK(JavaDevelopmentKit):JDK是进行Java开发的必备工具,务必下载安装并配置相应的环境变量。安装IDE(IntegratedDevelopmentEnvironment):推荐使用IntelliJIDEA、E......
  • Android 13 移植EthernetSettings/Ethernet更新
    移植EthernetSettingsAndroid13在Settings搜索没有发现以太网设置,应该是移除了,但是客户的设备需要,所以移植Android11的.以太网相关的功能在Android13中进行模块化,提取到packages/modules/Connectivity/中,EthernetManager相关代码从framework移到packages/modules/Conne......