HarmonyOS NEXT实战开发:NDK接口接入ArkTS页面实例

占位组件

使用NDK接口构建UI界面时,需要在ArkTS页面创建用于挂载NDK接口创建组件的占位组件。占位组件类型为ContentSlot,ContentSlot能够绑定一个NodeContent对象,该对象可通过Node-API传递到Native侧挂载显示Native组件。

  • 占位组件和其他ArkTS内置组件使用方法相同。

    import { NodeContent } from '@ohos.arkui.node';
    
    @Entry
    @Component
    struct Index {
      // 初始化NodeContent对象。
      private rootSlot = new NodeContent();
      @State @Watch('changeNativeFlag') showNative: boolean = false;
    
      changeNativeFlag(): void {
        if (this.showNative) {
          // 传递NodeContent对象用于Native创建组件的挂载显示
          nativeNode.createNativeRoot(this.rootSlot)
        } else {
          // 销毁NativeModule组件
          nativeNode.destroyNativeRoot()
        }
      }
    
      build() {
        Column() {
          Button(this.showNative ? "HideNativeUI" : "ShowNativeUI").onClick(() => {
            this.showNative = !this.showNative
          })
          Row() {
            // 将NodeContent和ContentSlot占位组件绑定。
            ContentSlot(this.rootSlot)
          }.layoutWeight(1)
        }
        .width('100%')
        .height('100%')
      }
    }
  • 占位组件可以通过相关接口在Native侧转化为挂载对象。

    ArkUI_NodeContentHandle contentHandle;
    OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &contentHandle);
  • 挂载对象提供了相关挂载和卸载组件接口。

    OH_ArkUI_NodeContent_AddNode(handle_, myNativeNode);
    OH_ArkUI_NodeContent_RemoveNode(handle_, myNativeNode);

NDK组件模块

NDK提供的UI组件能力如组件创建、树操作、属性设置、事件注册等是通过函数指针结构体(如ArkUI_NativeNodeAPI_1)进行暴露,该函数指针结构体可以通过模块查询接口获取。

ArkUI_NativeNodeAPI_1* arkUINativeNodeApi = nullptr;
OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, arkUINativeNodeApi);

在获取到函数指针结构体后,可以使用该结构体内的函数实现相关UI组件操作。

  • 组件创建和销毁。

    auto listNode = arkUINativeNodeApi->createNode(ARKUI_NODE_LIST);
    arkUINativeNodeApi->disposeNode(listNode);

    获取NDK接口支持的组件范围可以通过查询ArkUI_NodeType枚举值。

  • 组件树操作。

    auto parent = arkUINativeNodeApi->createNode(ARKUI_NODE_STACK);
    auto child = arkUINativeNodeApi->createNode(ARKUI_NODE_STACK);
    arkUINativeNodeApi->addChild(parent, child);
    arkUINativeNodeApi->removeChild(parent, child);
  • 属性设置。

    auto stack = arkUINativeNodeApi->createNode(ARKUI_NODE_STACK);
    ArkUI_NumberValue value[] = {{.f32 = 100}};
    ArkUI_AttributeItem item = {value, 1};
    arkUINativeNodeApi->setAttribute(stack, NODE_WIDTH, &item);
    ArkUI_NumberValue value[] = {{.u32 = 0xff112233}};
    ArkUI_AttributeItem item = {value, 1};
    arkUINativeNodeApi->setAttribute(stack, NODE_BACKGROUND_COLOR, &item);

    获取NDK接口支持的属性范围可以通过查询ArkUI_NodeAttributeType枚举值。

  • 事件注册。

    auto stack = arkUINativeNodeApi->createNode(ARKUI_NODE_STACK);
    arkUINativeNodeApi->addNodeEventReceiver(stack, [](ArkUI_NodeEvent* event){
        // process event
    });
    arkUINativeNodeApi->registerNodeEvent(stack, NODE_ON_CLICK, 0, nullptr);

    获取NDK接口支持的事件范围可以通过查询ArkUI_NodeEventType枚举值。

示例

下面的示例展示了如何使用ContentSlot挂载Native侧的文本列表。

图1 Native文本列表

  1. 在ArkTS页面上声明用于Native页面挂载的占位组件,并在页面创建时通知Native侧创建文本列表。

    import nativeNode from 'libentry.so';
    import { NodeContent } from '@ohos.arkui.node';
    
    @Entry
    @Component
    struct Index {
      // 初始化NodeContent对象。
      private rootSlot = new NodeContent();
      @State @Watch('changeNativeFlag') showNative: boolean = false;
    
      changeNativeFlag(): void {
        if (this.showNative) {
          // 传递NodeContent对象用于Native创建组件的挂载显示
          nativeNode.createNativeRoot(this.rootSlot)
        } else {
          // 销毁NativeModule组件
          nativeNode.destroyNativeRoot()
        }
      }
    
      build() {
        Column() {
          Button(this.showNative ? "HideNativeUI" : "ShowNativeUI").onClick(() => {
            this.showNative = !this.showNative
          })
          Row() {
            // 将NodeContent和ContentSlot占位组件绑定。
            ContentSlot(this.rootSlot)
          }.layoutWeight(1)
        }
        .width('100%')
        .height('100%')
      }
    }
  2. 使用Native模板创建工程,并在Native侧提供Node-API的桥接方法,实现ArkTS侧的NativeNode模块接口。

    接口声明。

    // entry/src/main/cpp/types/libentry/Index.d.ts
    
    export const createNativeRoot: (content: Object) => void;
    export const destroyNativeRoot: () => void;

    Native实现。

    // entry/src/main/cpp/napi_init.cpp
    
    #include "NativeEntry.h"
    #include "napi/native_api.h"
    
    EXTERN_C_START
    static napi_value Init(napi_env env, napi_value exports) {
        // 绑定Native侧的创建组件和销毁组件。
        napi_property_descriptor desc[] = {
            {"createNativeRoot", nullptr, NativeModule::CreateNativeRoot, nullptr, nullptr, nullptr, napi_default, nullptr},
            {"destroyNativeRoot", nullptr, NativeModule::DestroyNativeRoot, nullptr, nullptr, nullptr, napi_default, nullptr}};
        napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
        return exports;
    }
    EXTERN_C_END
    
    static napi_module demoModule = {
        .nm_version = 1,
        .nm_flags = 0,
        .nm_filename = nullptr,
        .nm_register_func = Init,
        .nm_modname = "entry",
        .nm_priv = ((void *)0),
        .reserved = {0},
    };
    
    extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); }
  3. 在NativeEntry.h文件中创建Native界面。

    // NativeEntry.h
    
    #ifndef MYAPPLICATION_NATIVEENTRY_H
    #define MYAPPLICATION_NATIVEENTRY_H
    
    #include <js_native_api_types.h>
    
    namespace NativeModule {
    
    napi_value CreateNativeRoot(napi_env env, napi_callback_info info);
    
    napi_value DestroyNativeRoot(napi_env env, napi_callback_info info);
    
    // 管理Native组件的生命周期和内存。
    class NativeEntry {
    public:
        static NativeEntry *GetInstance() {
            static NativeEntry nativeEntry;
            return &nativeEntry;
        }
    
        void SetContentHandle(ArkUI_NodeContentHandle handle) {
            handle_ = handle;
        }
    
        void SetRootNode(const std::shared_ptr<ArkUIBaseNode> &baseNode) {
            root_ = baseNode;
            // 添加Native组件到NodeContent上用于挂载显示。
            OH_ArkUI_NodeContent_AddNode(handle_, root_->GetHandle());
        }
        void DisposeRootNode() {
            // 从NodeContent上卸载组件并销毁Native组件。
            OH_ArkUI_NodeContent_RemoveNode(handle_, root_->GetHandle());
            root_.reset();
        }
    
    private:
        std::shared_ptr<ArkUIBaseNode> root_;
        ArkUI_NodeContentHandle handle_;
    };
    
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_NATIVEENTRY_H

    对应实现文件。

    // NativeEntry.cpp
    #include "NativeEntry.h"
    
    #include <arkui/native_node_napi.h>
    #include <hilog/log.h>
    #include <js_native_api.h>
    
    namespace NativeModule {
    
    napi_value CreateNativeRoot(napi_env env, napi_callback_info info) {
        size_t argc = 1;
        napi_value args[1] = {nullptr};
    
        napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
    
        // 获取NodeContent
        ArkUI_NodeContentHandle contentHandle;
        OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &contentHandle);
        NativeEntry::GetInstance()->SetContentHandle(contentHandle);
    
        // 创建文本列表
        auto list = CreateTextListExample();
    
        // 保持Native侧对象到管理类中,维护生命周期。
        NativeEntry::GetInstance()->SetRootNode(list);
        return nullptr;
    }
    
    napi_value DestroyNativeRoot(napi_env env, napi_callback_info info) {
        // 从管理类中释放Native侧对象。
        NativeEntry::GetInstance()->DisposeRootNode();
        return nullptr;
    }
    
    } // namespace NativeModule

    使用NDK 提供的C接口需要在CMakeLists.txt 中增加libace_ndk.z.so 的引用,如下所示,其中entry为工程导出的动态库名称,如当前示例使用的是默认的名称 libentry.so。

    target_link_libraries(entry PUBLIC libace_napi.z.so libace_ndk.z.so)
  4. 由于NDK接口提供的是C接口,为了使用面向对象的方式简化编程和工程管理,这里建议使用C++进行二次封装,下面示例代码展示了示例界面中所需的列表,文本组件封装类。

    1)获取ArkUI在NDK接口的入口模块ArkUI_NativeNodeAPI_1,该结构体模块提供了一系列组件创建、树构建、属性设置和事件注册等函数指针。

    // NativeModule.h
    // 提供获取ArkUI在Native侧模块的封装接口
    
    #ifndef MYAPPLICATION_NATIVEMODULE_H
    #define MYAPPLICATION_NATIVEMODULE_H
    
    #include <arkui/native_node.h>
    #include <functional>
    #include <cassert>
    
    #include <arkui/native_interface.h>
    
    namespace NativeModule {
    
    class NativeModuleInstance {
    public:
        static NativeModuleInstance *GetInstance() {
            static NativeModuleInstance instance;
            return &instance;
        }
    
        NativeModuleInstance() {
            // 获取NDK接口的函数指针结构体对象,用于后续操作。
            OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, arkUINativeNodeApi_);
            assert(arkUINativeNodeApi_);
        }
        // 暴露给其他模块使用。
        ArkUI_NativeNodeAPI_1 *GetNativeNodeAPI() { return arkUINativeNodeApi_; }
    
    private:
        ArkUI_NativeNodeAPI_1 *arkUINativeNodeApi_ = nullptr;
    };
    
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_NATIVEMODULE_H

    2)提供列表,文本组件的基类对象,用于封装通用属性和事件。

    // ArkUIBaseNode.h
    // 提供组件树操作的基类。
    
    #ifndef MYAPPLICATION_ARKUIBASENODE_H
    #define MYAPPLICATION_ARKUIBASENODE_H
    
    #include <arkui/native_type.h>
    #include <list>
    #include <memory>
    
    #include "NativeModule.h"
    
    namespace NativeModule {
    
    class ArkUIBaseNode {
    public:
        explicit ArkUIBaseNode(ArkUI_NodeHandle handle)
            : handle_(handle), nativeModule_(NativeModuleInstance::GetInstance()->GetNativeNodeAPI()) {}
    
        virtual ~ArkUIBaseNode() {
            // 封装析构函数,实现子节点移除功能。
            if (!children_.empty()) {
                for (const auto& child : children_) {
                    nativeModule_->removeChild(handle_, child->GetHandle());
                }
                children_.clear();
            }
            // 封装析构函数,统一回收节点资源。 
            nativeModule_->disposeNode(handle_);
        }
    
        void AddChild(const std::shared_ptr<ArkUIBaseNode> &child) {
            children_.emplace_back(child);
            OnAddChild(child);
        }
    
        void RemoveChild(const std::shared_ptr<ArkUIBaseNode> &child) {
            children_.remove(child);
            OnRemoveChild(child);
        }
    
        void InsertChild(const std::shared_ptr<ArkUIBaseNode> &child, int32_t index) {
            if (index >= children_.size()) {
                AddChild(child);
            } else {
                auto iter = children_.begin();
                std::advance(iter, index);
                children_.insert(iter, child);
                OnInsertChild(child, index);
            }
        }
    
        ArkUI_NodeHandle GetHandle() const { return handle_; }
    
    protected:
        // 针对父容器子类需要重载下面的函数,实现组件挂载和卸载。
        virtual void OnAddChild(const std::shared_ptr<ArkUIBaseNode> &child) {}
        virtual void OnRemoveChild(const std::shared_ptr<ArkUIBaseNode> &child) {}
        virtual void OnInsertChild(const std::shared_ptr<ArkUIBaseNode> &child, int32_t index) {}
    
        ArkUI_NodeHandle handle_;
        ArkUI_NativeNodeAPI_1 *nativeModule_ = nullptr;
    
    private:
        std::list<std::shared_ptr<ArkUIBaseNode>> children_;
    };
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_ARKUIBASENODE_H
    // ArkUINode.h
    // 提供通用属性和事件的封装。
    
    #ifndef MYAPPLICATION_ARKUINODE_H
    #define MYAPPLICATION_ARKUINODE_H
    
    #include "ArkUIBaseNode.h"
    #include "NativeModule.h"
    #include <arkui/native_node.h>
    #include <arkui/native_type.h>
    
    namespace NativeModule {
    
    class ArkUINode : public ArkUIBaseNode {
    public:
        explicit ArkUINode(ArkUI_NodeHandle handle) : ArkUIBaseNode(handle) {}
    
        ~ArkUINode() override {}
    
        // NDK相关通用属性调用封装
        void SetWidth(float width) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.f32 = width}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_WIDTH, &item);
        }
        void SetPercentWidth(float percent) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.f32 = percent}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_WIDTH_PERCENT, &item);
        }
        void SetHeight(float height) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.f32 = height}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_HEIGHT, &item);
        }
        void SetPercentHeight(float percent) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.f32 = percent}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_HEIGHT_PERCENT, &item);
        }
        void SetBackgroundColor(uint32_t color) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.u32 = color}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_BACKGROUND_COLOR, &item);
        }
    
    protected:
        // 组件树操作的实现类对接。
        void OnAddChild(const std::shared_ptr<ArkUIBaseNode> &child) override {
            nativeModule_->addChild(handle_, child->GetHandle());
        }
        void OnRemoveChild(const std::shared_ptr<ArkUIBaseNode> &child) override {
            nativeModule_->removeChild(handle_, child->GetHandle());
        }
        void OnInsertChild(const std::shared_ptr<ArkUIBaseNode> &child, int32_t index) override {
            nativeModule_->insertChildAt(handle_, child->GetHandle(), index);
        }
    };
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_ARKUINODE_H

    3)实现列表组件。

    // ArkUIListNode.h
    // 提供列表组件的封装。
    
    #ifndef MYAPPLICATION_ARKUILISTNODE_H
    #define MYAPPLICATION_ARKUILISTNODE_H
    
    #include "ArkUINode.h"
    
    namespace NativeModule {
    class ArkUIListNode : public ArkUINode {
    public:
        ArkUIListNode()
            : ArkUINode((NativeModuleInstance::GetInstance()->GetNativeNodeAPI())->createNode(ARKUI_NODE_LIST)) {} // 创建ArkUI的列表组件。
    
        ~ArkUIListNode() override {} 
        // List组件的属性NDK接口封装。
        void SetScrollBarState(bool isShow) {
            assert(handle_);
            ArkUI_ScrollBarDisplayMode displayMode =
                isShow ? ARKUI_SCROLL_BAR_DISPLAY_MODE_ON : ARKUI_SCROLL_BAR_DISPLAY_MODE_OFF;
            ArkUI_NumberValue value[] = {{.i32 = displayMode}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_SCROLL_BAR_DISPLAY_MODE, &item);
        }
    };
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_ARKUILISTNODE_H

    4)实现列表项组件。

    // ArkUIListItemNode.h
    // 提供列表项的封装类。
    
    #ifndef MYAPPLICATION_ARKUISTACKNODE_H
    #define MYAPPLICATION_ARKUISTACKNODE_H
    
    #include "ArkUINode.h"
    
    namespace NativeModule {
    class ArkUIListItemNode : public ArkUINode {
    public:
        ArkUIListItemNode()
            : ArkUINode((NativeModuleInstance::GetInstance()->GetNativeNodeAPI())->createNode(ARKUI_NODE_LIST_ITEM)) {}
    };
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_ARKUISTACKNODE_H

    5)实现文本组件。

    // ArkUITextNode.h
    // 实现文本组件的封装类。
    
    #ifndef MYAPPLICATION_ARKUITEXTNODE_H
    #define MYAPPLICATION_ARKUITEXTNODE_H
    
    #include "ArkUINode.h"
    
    #include <string>
    
    namespace NativeModule {
    class ArkUITextNode : public ArkUINode {
    public:
        ArkUITextNode()
            : ArkUINode((NativeModuleInstance::GetInstance()->GetNativeNodeAPI())->createNode(ARKUI_NODE_TEXT)) {}
        // 文本属性NDK接口封装。
        void SetFontSize(float fontSize) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.f32 = fontSize}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_FONT_SIZE, &item);
        }
        void SetFontColor(uint32_t color) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.u32 = color}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_FONT_COLOR, &item);
        }
        void SetTextContent(const std::string &content) {
            assert(handle_);
            ArkUI_AttributeItem item = {nullptr, 0, content.c_str()};
            nativeModule_->setAttribute(handle_, NODE_TEXT_CONTENT, &item);
        }
        void SetTextAlign(ArkUI_TextAlignment align) {
            assert(handle_);
            ArkUI_NumberValue value[] = {{.i32 = align}};
            ArkUI_AttributeItem item = {value, 1};
            nativeModule_->setAttribute(handle_, NODE_TEXT_ALIGN, &item);
        }
    };
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_ARKUITEXTNODE_H
  5. 完善步骤3的CreateTextListExample函数,实现Native文本列表的创建和挂载显示。

    // NativeEntry.h
    // 自定义NDK接口入口函数。
    
    #ifndef MYAPPLICATION_NORMALTEXTLISTEXAMPLE_H
    #define MYAPPLICATION_NORMALTEXTLISTEXAMPLE_H
    
    #include "ArkUIBaseNode.h"
    #include "ArkUIListItemNode.h"
    #include "ArkUIListNode.h"
    #include "ArkUITextNode.h"
    #include <hilog/log.h>
    
    namespace NativeModule {
    
    std::shared_ptr<ArkUIBaseNode> CreateTextListExample() {
        // 创建组件并挂载
        // 1:使用智能指针创建List组件。
        auto list = std::make_shared<ArkUIListNode>();
        list->SetPercentWidth(1);
        list->SetPercentHeight(1);
        // 2:创建ListItem子组件并挂载到List上。
        for (int32_t i = 0; i < 30; ++i) {
            auto listItem = std::make_shared<ArkUIListItemNode>();
            auto textNode = std::make_shared<ArkUITextNode>();
            textNode->SetTextContent(std::to_string(i));
            textNode->SetFontSize(16);
            textNode->SetPercentWidth(1);
            textNode->SetHeight(100);
            textNode->SetBackgroundColor(0xFFfffacd);
            textNode->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
            listItem->AddChild(textNode);
            list->AddChild(listItem);
        }
        return list;
    }
    } // namespace NativeModule
    
    #endif // MYAPPLICATION_NORMALTEXTLISTEXAMPLE_H

最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。 

这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开发必掌握的核心知识要点,内容包含了ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。

希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

获取这份完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS NEXT学习资料

鸿蒙(HarmonyOS NEXT)最新学习路线

  •  HarmonOS基础技能

  • HarmonOS就业必备技能 
  •  HarmonOS多媒体技术

  • 鸿蒙NaPi组件进阶

  • HarmonOS高级技能

  • 初识HarmonOS内核 
  • 实战就业级设备开发

有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS NEXT学习资料

《鸿蒙 (OpenHarmony)开发入门教学视频》

《鸿蒙生态应用开发V2.0白皮书》

图片

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

图片

 《鸿蒙开发基础》

  • ArkTS语言
  • 安装DevEco Studio
  • 运用你的第一个ArkTS应用
  • ArkUI声明式UI开发
  • .……

图片

 《鸿蒙开发进阶》

  • Stage模型入门
  • 网络管理
  • 数据管理
  • 电话服务
  • 分布式应用开发
  • 通知与窗口管理
  • 多媒体技术
  • 安全技能
  • 任务管理
  • WebGL
  • 国际化开发
  • 应用测试
  • DFX面向未来设计
  • 鸿蒙系统移植和裁剪定制
  • ……

图片

《鸿蒙进阶实战》

  • ArkTS实践
  • UIAbility应用
  • 网络案例
  • ……

图片

 获取以上完整鸿蒙HarmonyOS学习资料,请点击→纯血版全套鸿蒙HarmonyOS NEXT学习资料

总结

总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,他们才能在这个变革的时代中立于不败之地。 

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值