Android 车载应用开发指南 - SystemUI

一  SystemUI 概述

SystemUI全称System User Interface,直译过来就是系统级用户交互界面,在 Android 系统中由SystemUI负责统一管理整个系统层的 UI,它是一个系统级应用程序(APK),源码在/frameworks/base/packages/目录下。

作者:话唠扇贝
链接:https://juejin.cn/post/7326464207365177380

1.1 SystemUI

Android - Phone中SystemUI从源码量看就是一个相当复杂的程序,常见的如:状态栏、消息中心、近期任务、截屏以及一系列功能都是在SystemUI中实现的。

源码位置:/frameworks/base/packages/SystemUI

常见 UI 组件有(包含但不限于,完整列表可以查看 SystemUI 服务组件列表)

  • 状态栏 StatusBar

  • 导航栏 NavigationBar

  • 通知栏 NotificationPanel

  • 快捷按键栏 QSPanel

  • 最近任务 Recent

  • 键盘锁 Keyguard

原生 Android 系统中 SystemUI 大概是这样

ae4568ceec04499171c3b80a07bd1659.jpeg

1.2 CarSystemUI

Android-AutoMotive中的SystemUI相对手机中要简单不少,目前商用车载系统中几乎必备的顶部状态栏、消息中心、底部导航栏在原生的Android系统中都已经实现了。

源码位置:frameworks/base/packages/CarSystemUI

a763f154d0f6502e0301d7e489a0b1f2.jpeg

虽然CarSystemUISystemUI的源码位置不同,但是二者实际上是复用关系。通过阅读CarSystemUI的 Android.bp 文件可以发现CarSystemUI在编译时把SystemUI以静态库的方式引入进来了。

android.bp 源码位置:/frameworks/base/packages/CarSystemUI/Android.bp

android_library {
    name: "CarSystemUI-core",
    ...
    static_libs: [
        "SystemUI-core",
        "SystemUIPluginLib",
        "SystemUISharedLib",
        "SystemUI-tags",
        "SystemUI-proto",
        ...
    ],
    ...
}

二 SystemUI 启动流程

System UI的启动大致可分为以下两个流程:

  • Framework中启动SystemUIService

  • SystemUIService中启动SystemUI所需的各种组件

说明:本文源码分析基于版本:android-12.0.0_r3

2.1 Framework 中的流程

SystemUI 是系统应用,所以它也是一个 APK,有入口 Application,只不过它是由 system_server 进程直接启动的。

关于SystemServer,它是 Android framework 中关键系统的服务,由 Android 系统最核心的进程Zygotefork 生成,进程名为system_server。常见的ActivityManagerServicePackageManagerServiceWindowManageService都是由SystemServer启动的。

SystemServer 源码路径:/frameworks/base/services/java/com/android/server/SystemServer.java

第一步:SystemServermain() 方法中调用 SystemServer.run()run()中调用startOtherServices()

public static void main(String[] args) {
    new SystemServer().run();
}
private void run() {
    ... ...
    // Start services.
    try {
        startBootstrapServices(t);
        startCoreServices(t);
        startOtherServices(t); //SystemServer在startOtherServices()被启动
     } 
    ... ...
}

第二步:startOtherServices()中通过AMS的回调方法ready(),然后调用startSystemUi()

mActivityManagerService.systemReady(() -> {
        ... ...  
        try {
            startSystemUi(context, windowManagerF);
        } catch (Throwable e) {
            reportWtf("starting System UI", e);
        }
        ... ...
    ,t);

第三步:startSystemUi()中可以看出,SystemUI本质就是一个Service,通过PM获取到的Componentcom.android.systemui/.SystemUIService,然后通过调用context.startServiceAsUser()完成对SystemUIService的启动。

private static void startSystemUi(Context context, WindowManagerService windowManager) {
        PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
        Intent intent = new Intent();
        intent.setComponent(pm.getSystemUiServiceComponent());
        intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
        //Slog.d(TAG, "Starting service: " + intent);
        context.startServiceAsUser(intent, UserHandle.SYSTEM);
        windowManager.onSystemUiStarted();
    }

第四步:SystemUIService 依附于SystemUI应用,所以SystemUIService启动前需要完成SystemUI整个应用的启动,其流程也就是应用常见的冷启动流程,这里展开讲一下:

  • SystemUI 应用启动流程

    context中的startServiceAsUser()是一个抽象方法,具体实现在ContextImpl.java里。实现方法startServiceCommon()中,通过ActivityManager.getService()就会走到AMS中,最终在AMS来启动SystemUIService

@Override
public ComponentName startServiceAsUser(Intent service, UserHandle user) {
    return startServiceCommon(service, false, user);
}


@Override
private ComponentName startServiceCommon(Intent service, boolean requireForeground,
                                         UserHandle user) {
    try {
        validateServiceIntent(service);
        service.prepareToLeaveProcess(this);
        ComponentName cn = ActivityManager.getService().startService(  //在AMS中开启Service
            mMainThread.getApplicationThread(), service,
            service.resolveTypeIfNeeded(getContentResolver()), requireForeground,
            getOpPackageName(), getAttributionTag(), user.getIdentifier());
        ... ...  
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

接下来进入AMS,一探究竟:

AMS 源码路径:/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

AMSstartService()方法里,会经过一系列内部流程,调用到bringUpServiceLocked()方法。

@Override
public ComponentName startService(IApplicationThread caller, Intent service,
        String resolvedType, boolean requireForeground, String callingPackage,
        String callingFeatureId, int userId)
        throws TransactionTooLargeException {
      ... ... 
        try {
            res = mServices.startServiceLocked(caller, service,
                    resolvedType, callingPid, callingUid,
                    requireForeground, callingPackage, callingFeatureId, userId); // 内部调用到 startServiceLocked()
        }
        ... ...
    }
}


ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType, int callingPid, int callingUid, boolean fgRequired, String callingPackage,
        @Nullable String callingFeatureId, final int userId,
        boolean allowBackgroundActivityStarts) throws TransactionTooLargeException {
    ... ... 
    if (caller != null) {
        // 这里记录app的进程信息
        final ProcessRecord callerApp = mAm.getRecordForAppLocked(caller);
    ... ...
    ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting); //内部调用到startServiceInnerLocked()
    ... ... 
    return cmp;
}
    
ComponentName startServiceInnerLocked(ServiceMap smap, Intent service, ServiceRecord r, boolean callerFg, boolean addToStarting) throws TransactionTooLargeException {
    ... ... 
    String error = bringUpServiceLocked(r, service.getFlags(), callerFg, false, false); //调用到bringUpServiceLocked()
    if (error != null) {
        return new ComponentName("!!", error);
    }
    ... ... 
    return r.name;
}

继续调用了bringUpServiceLocked()方法,

private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg, boolean whileRestarting, boolean permissionsReviewRequired)
        throws TransactionTooLargeException {
    ... ... 
    if (!isolated) {
        app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
        if (DEBUG_MU) Slog.v(TAG_MU, "bringUpServiceLocked: appInfo.uid=" + r.appInfo.uid + " app=" + app);
        //如果service进程存在
        if (app != null && app.thread != null) {
            try {
                app.addPackage(r.appInfo.packageName, r.appInfo.longVersionCode, mAm.mProcessStats);
                //启动service
                realStartServiceLocked(r, app, execInFg);
                return null;
            } catch (TransactionTooLargeException e) {
                throw e;
            } catch (RemoteException e) {
                Slog.w(TAG, "Exception when starting service " + r.shortInstanceName, e);
            }
        }
    }
    ... ... 


    // 如果不存在此进程
    if (app == null && !permissionsReviewRequired) {
        // 启动运行的线程
        if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
                hostingRecord, ZYGOTE_POLICY_FLAG_EMPTY, false, isolated, false)) == null) {
            String msg = "Unable to launch app "
                    + r.appInfo.packageName + "/"
                    + r.appInfo.uid + " for service "
                    + r.intent.getIntent() + ": process is bad";
            Slog.w(TAG, msg);
            bringDownServiceLocked(r);
            return msg;
        }
    }
    ... ... 
    return null;
}

这个方法做了两件事:

  1. 如果SystemUIService所属进程已经存在,则直接调用realStartServiceLocked()

  2. 如果SystemUIService所属进程不存在,则执行startProcessLocked()方法创建进程,经过层层调用,最终也会走到realStartServiceLocked()中:

private final void realStartServiceLocked(ServiceRecord r,
        ProcessRecord app, boolean execInFg) throws RemoteException {
    ... ... 
    try {
    ... ... 
        app.thread.scheduleCreateService(r, r.serviceInfo,
                mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
                app.getReportedProcState());
        r.postNotification();
        created = true;
    }
}

这个方法内部调用了app.thread.scheduleCreateService(),而app.thread是一个IApplicationThread类型的,他的实现是ActivityThread的一个内部类ApplicationThread,而这个类正好实现了IApplicationThread.Stub在ApplicationThread类中,找到对应的调用方法:

public final void scheduleCreateService(IBinder token,
        ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
    updateProcessState(processState, false);
    CreateServiceData s = new CreateServiceData();
    s.token = token;
    s.info = info;
    s.compatInfo = compatInfo;


    sendMessage(H.CREATE_SERVICE, s);
}

可以看出,是发送一个消息给Handler,这个HandlerActivityThread的内部类H

public void handleMessage(Message msg) {
    switch (msg.what) {
        ... ... 
        case CREATE_SERVICE:
            if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                        ("serviceCreate: " + String.valueOf(msg.obj)));
            }
            handleCreateService((CreateServiceData)msg.obj);
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            break;
        ... ... 
    }


}

最终调用了handleCreateService()方法:

private void handleCreateService(CreateServiceData data) {


    LoadedApk packageInfo = getPackageInfoNoCheck(
            data.info.applicationInfo, data.compatInfo);
    Service service = null;
    try {
        //创建service的context
        ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
        //创建Application
        Application app = packageInfo.makeApplication(false, mInstrumentation);
        //获取类加载器
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        //加载service实例
        service = packageInfo.getAppFactory()
                .instantiateService(cl, data.info.name, data.intent);
        // Service resources must be initialized with the same loaders as the application
        // context.
        context.getResources().addLoaders(
                app.getResources().getLoaders().toArray(new ResourcesLoader[0]));


        context.setOuterContext(service);
        //初始化service
        service.attach(context, this, data.info.name, data.token, app,
                ActivityManager.getService());
        //调用service的onCreate方法
        service.onCreate();
        mServices.put(data.token, service);
        try {
            //通过serviceDoneExecuting告知AMS,service已经启动完成
            ActivityManager.getService().serviceDoneExecuting(
                    data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(service, e)) {
            throw new RuntimeException(
                "Unable to create service " + data.info.name
                + ": " + e.toString(), e);
        }
    }
}

这个方法主要做了以下几件事:

  1. 首先,创建上下文

  2. 创建SystemUIApplication,获取类加载器

  3. 加载SystemUIService实例,初始化SystemUIService, 调用onCreate()方法

  4. 最后通知AMSSystemUIService启动完成。

到这里SystemUIService已经启动完成。

第五步: 前面在SystemUIApplication创建成功后会回调内部的OnCreate()方法,在OnCreate()中方法注册了一个开机广播,当接收到开机广播后会调用SystemUIonBootCompleted()方法来告诉每个子模块 Android 系统已经完成开机。

@Override
    public void onCreate() {
        super.onCreate();
        Log.v(TAG, "SystemUIApplication created.");
        // 设置所有服务继承的应用程序主题。
        // 请注意,在清单中设置应用程序主题仅适用于activity。这里是让Service保持与主题设置同步。
        setTheme(R.style.Theme_SystemUI);


        if (Process.myUserHandle().equals(UserHandle.SYSTEM)) {
            IntentFilter bootCompletedFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED);
            bootCompletedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
            registerReceiver(new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (mBootCompleteCache.isBootComplete()) return;
                    if (DEBUG) Log.v(TAG, "BOOT_COMPLETED received");
                    unregisterReceiver(this);
                    mBootCompleteCache.setBootComplete();
                    if (mServicesStarted) {
                        final int N = mServices.length;
                        for (int i = 0; i < N; i++) {
                            mServices[i].onBootCompleted(); //通知SystemUI子模块
                        }
                    }
                }
            }, bootCompletedFilter);
               ...
        } else {
            // 我们不需要为正在执行某些任务的子进程启动服务。
           ...
        }
    }

2.2 SystemUI 中的流程

第六步:SystemUIService初始化完成后会调用onCreate()方法,onCreate()中调用了SystemUIApplication中的startServiceIfNeeded()方法完成SystemUI子模块的初始化。

SystemUIService 源码位置:/frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java

public class SystemUIService extends Service {
    ... ...
 
    @Override
    public void onCreate() {
        super.onCreate();
 
        // Start all of SystemUI
        ((SystemUIApplication) getApplication()).startServicesIfNeeded(); //调用startServicesIfNeeded()
 
        ... ...
    }
}

第七步:SystemUIApplicationstartServicesIfNeeded()方法中,通过SystemUIFactory获取到配置在config.xml中每个子模块的className

SystemUIApplication 源码位置:/frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java

// SystemUIApplication
public void startServicesIfNeeded() {
    String[] names = SystemUIFactory.getInstance().getSystemUIServiceComponents(getResources());
    startServicesIfNeeded("StartServices", names);
}


// SystemUIFactory
/** Returns the list of system UI components that should be started. */
public String[] getSystemUIServiceComponents(Resources resources) {
    return resources.getStringArray(R.array.config_systemUIServiceComponents);
}

config.xml 位置:/frameworks/base/packages/SystemUI/res/values/config.xml

<!-- SystemUI Services: The classes of the stuff to start. -->
     <string-array name="config_systemUIServiceComponents" translatable="false">
         <item>com.android.systemui.util.NotificationChannels</item>
         <item>com.android.systemui.keyguard.KeyguardViewMediator</item>
         <item>com.android.systemui.recents.Recents</item>
         <item>com.android.systemui.volume.VolumeUI</item>
         <item>com.android.systemui.stackdivider.Divider</item>
         <item>com.android.systemui.statusbar.phone.StatusBar</item>
         <item>com.android.systemui.usb.StorageNotification</item>
         <item>com.android.systemui.power.PowerUI</item>
         <item>com.android.systemui.media.RingtonePlayer</item>
         <item>com.android.systemui.keyboard.KeyboardUI</item>
         <item>com.android.systemui.pip.PipUI</item>
         <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>
         <item>@string/config_systemUIVendorServiceComponent</item>
         <item>com.android.systemui.util.leak.GarbageMonitor$Service</item>
         <item>com.android.systemui.LatencyTester</item>
         <item>com.android.systemui.globalactions.GlobalActionsComponent</item>
         <item>com.android.systemui.ScreenDecorations</item>
         <item>com.android.systemui.biometrics.AuthController</item>
         <item>com.android.systemui.SliceBroadcastRelayHandler</item>
         <item>com.android.systemui.SizeCompatModeActivityController</item>
         <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>
         <item>com.android.systemui.theme.ThemeOverlayController</item>
         <item>com.android.systemui.accessibility.WindowMagnification</item>
         <item>com.android.systemui.accessibility.SystemActions</item>
         <item>com.android.systemui.toast.ToastUI</item>
     </string-array>

第八步:startServicesIfNeeded()中通过反射完成了每个SystemUI组件的创建,然后再调用各个SystemUIonStart()方法来继续执行子模块的初始化。

private SystemUI[] mServices;


private void startServicesIfNeeded(String metricsPrefix, String[] services) {
    if (mServicesStarted) {
        return;
    }
    mServices = new SystemUI[services.length];
    ...


    final int N = services.length;
    for (int i = 0; i < N; i++) {
        String clsName = services[i];
        if (DEBUG) Log.d(TAG, "loading: " + clsName);
        try {
            SystemUI obj = mComponentHelper.resolveSystemUI(clsName);
            if (obj == null) {
                Constructor constructor = Class.forName(clsName).getConstructor(Context.class);
                obj = (SystemUI) constructor.newInstance(this);
            }
            mServices[i] = obj;
        } catch (ClassNotFoundException
                | NoSuchMethodException
                | IllegalAccessException
                | InstantiationException
                | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }


        if (DEBUG) Log.d(TAG, "running: " + mServices[i]);
        // 调用各个子模块的start()
        mServices[i].start();
        // 首次启动时,这里始终为false,不会被调用
        if (mBootCompleteCache.isBootComplete()) {
            mServices[i].onBootCompleted();
        }
    }
    mServicesStarted = true;
}

这里的SystemUI是一个抽象类,状态栏、近期任务等等模块都是继承自SystemUI,通过这种方式可以很大程度上简化复杂的SystemUI程序中各个子模块创建方式,同时我们可以通过配置资源的方式动态加载需要的SystemUI模块。

SystemUI的源码如下,方法基本都能见名知意,就不再介绍了。

public abstract class SystemUI implements Dumpable {
    protected final Context mContext;


    public SystemUI(Context context) {
        mContext = context;
    }


    public abstract void start();


    protected void onConfigurationChanged(Configuration newConfig) {
    }


    @Override
    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
    }


    protected void onBootCompleted() {
    }

2.3 CarSystemUI 的启动流程

前文提到CarSystemUI复用了手机SystemUI的代码,所以CarSystemUI的启动流程和SystemUI的是完全一致的。

CarSystemUI中需要的功能与SystemUI中也有部分差异,那么是这些差异化的功能是如何引入并完成初始化?以及一些手机的SystemUI才需要的功能是如何去除的呢?

其实很简单,在SystemUI的启动流程中我们得知,各个子模块的 className 是通过SystemUIFactorygetSystemUIServiceComponents()获取到的,那么只要继承SystemUIFactory并重写getSystemUIServiceComponents()就可以了。

public class CarSystemUIFactory extends SystemUIFactory {


    @Override
    protected SystemUIRootComponent buildSystemUIRootComponent(Context context) {
        return DaggerCarSystemUIRootComponent.builder()
                .contextHolder(new ContextHolder(context))
                .build();
    }


    @Override
    public String[] getSystemUIServiceComponents(Resources resources) {
        Set<String> names = new HashSet<>();
        // 先引入systemUI中的components
        for (String s : super.getSystemUIServiceComponents(resources)) {
            names.add(s);
        }
        // 再移除CarsystemUI不需要的components
        for (String s : resources.getStringArray(R.array.config_systemUIServiceComponentsExclude)) {
            names.remove(s);
        }
        // 最后再添加CarsystemUI特有的components
        for (String s : resources.getStringArray(R.array.config_systemUIServiceComponentsInclude)) {
            names.add(s);
        }


        String[] finalNames = new String[names.size()];
        names.toArray(finalNames);


        return finalNames;
    }
}
<!-- 需要移除的Components. -->
    <string-array name="config_systemUIServiceComponentsExclude" translatable="false">
        <item>com.android.systemui.recents.Recents</item>
        <item>com.android.systemui.volume.VolumeUI</item>
        <item>com.android.systemui.stackdivider.Divider</item>
        <item>com.android.systemui.statusbar.phone.StatusBar</item>
        <item>com.android.systemui.keyboard.KeyboardUI</item>
        <item>com.android.systemui.pip.PipUI</item>
        <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>
        <item>com.android.systemui.LatencyTester</item>
        <item>com.android.systemui.globalactions.GlobalActionsComponent</item>
        <item>com.android.systemui.SliceBroadcastRelayHandler</item>
        <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>
        <item>com.android.systemui.accessibility.WindowMagnification</item>
        <item>com.android.systemui.accessibility.SystemActions</item>
    </string-array>


    <!-- 新增的Components. -->
    <string-array name="config_systemUIServiceComponentsInclude" translatable="false">
        <item>com.android.systemui.car.navigationbar.CarNavigationBar</item>
        <item>com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier</item>
        <item>com.android.systemui.car.window.SystemUIOverlayWindowManager</item>
        <item>com.android.systemui.car.volume.VolumeUI</item>
    </string-array>

通过以上方式,就完成了CarSystemUI子模块的替换。

2.4 小结

总结一下,SystemUI的大致启动流程可以归纳如下:

8622528ecec2330fd3e0500d7539ec4c.jpeg

SystemUI 是一个 persistent 应用,它由操作系统启动,主要流程为

  • Android 系统在开机后会创建 system_server 进程,它会启动各种系统所需要的服务,其中就包括 SystemUIService

  • SystemUIService 启动后进入到应用层 SystemUI 中,在 SystemUIApplication 它首先会初始化监听ACTION_BOOT_COMPLETED 等通知,待系统完成启动后会通知各个组件 onBootCompleted

  • 在进入 SystemUIService 中依然执行的 SystemUIApplication 中的startServicesIfNeeded() 方法启动 SystemUI 中的子模块。

  • 最终的服务启动逻辑都是在 SystemUIApplication 里面,并且都保存在 mServices 数组中。

三 总结

SystemUI在原生的车载 Android 系统是一个较为复杂的模块,本文主要介绍了SystemUICarSystemUI的功能、源码结构及启动时序,希望能帮到从事SystemUI开发的同学。

关注我获取更多知识或者投稿

dd70a4dede93ca65510dc3f1ce61111d.jpeg

1bc4c550baf20f3d8a92d83752ba277e.jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智能硬件开发指南---基于yocto project pdf下载,是一本指导智能硬件开发的电子书,提供了基于yocto project的开发方法和步骤。该书主要包括以下几个方面的内容。 首先,该指南介绍了yocto project的基本概念和原理。yocto project是一个开源项目,旨在为嵌入式Linux系统的构建提供一个开放、灵活和可定制的开发平台。它提供了一套工具链和脚本,可以自动化构建和定制Linux操作系统。 其次,该指南详细介绍了基于yocto project的硬件开发流程。它包括了从准备阶段到部署阶段的各个环节,如环境搭建、配置系统、选择和集成驱动、编译和调试等。通过遵循这些步骤,开发人员可以高效地完成硬件开发工作。 此外,该指南还提供了一些实用的开发技巧和经验分享。比如,如何有效管理软件包依赖关系、如何优化系统性能、如何进行远程调试等。这些技巧和经验对于提高开发效率和减少开发风险非常有帮助。 最后,该指南给出了一些实际案例和示例项目,供开发人员参考和借鉴。这些案例涵盖了各种不同的硬件平台和应用场景,包括工业控制、智能家居、车载娱乐等。通过学习这些案例,开发人员可以更好地理解和应用yocto project。 总之,智能硬件开发指南---基于yocto project pdf下载是一本非常实用的指导书,对于从事智能硬件开发的人员来说具有很高的参考价值。无论是初学者还是有经验的开发人员,都可以通过学习这本指南提高自己的开发能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值