Android 车载应用开发指南(4) - CarService 详解

一 概述

CarService 是车载 Android 操作系统 Android Automotive OS (下文简介 AAOS)的核心服务之一,所有车载相关的应用都需要通过 CarService 来查询、控制整车的状态,不仅仅是车辆控制,实际上 CarService 几乎就是整个车载 Framework 最核心的组件。


二 CarService 架构

2.1 简介

AAOS 并没有大刀阔斧的改变 Android 原有的整体架构,几乎所有的核心服务 (AMS、WMS、PMS) 与手机并无区别,采用的是同一套源代码,所以我们可以将 AAOS 理解为 Android OS + Automotive Services + Automotive APPs

传统的手机系统加上相关的汽车服务,构成了现在的 AAOS,而其中 CarService 就是提供汽车相关功能的最主要的模块。

AAOS 定义了标准的硬件抽象层 HAL(Hardware Abstraction Layer) 来规范各个子系统与 Framework 的调用接口,并且通过 CarService 以及相关的 Car API 对上层应用提供标准编程接口。

车载 HAL 与 AAOS 架构:

  • Car App: 包括 OEM 和第三方开发的 App
  • Car API: 内有包含 CarSensorManager 在内的 API。位于 /packages/services/Car/car-lib
  • CarService: 系统中与车相关的服务。位于 /packages/services/Car
  • Vehicle HAL: 汽车的硬件抽象层描述。位于 /hardware/interfaces/automotive/vehicle/2.0/default/impl/vhal_v2_0

2.2 CarService 组成

CarService 源码位置:/packages/services/Car

其目录结构如下所示:

java 复制代码
 .
 ├── Android.mk
 ├── apicheck.mk
 ├── apicheck_msg_current.txt
 ├── apicheck_msg_last.txt
 ├── car-cluster-logging-renderer    //LoggingClusterRenderingService 继承 InstrumentClusterRenderingService
 ├── car-default-input-service   //按键消息处理
 ├── car-lib         //提供给汽车 App 特有的接口,许多定制的模块都在这里实现,包括 Sensor,HVAC,Cabin,ActiveParkingAssiance,Diagnostic,Vendor 等
 ├── car-maps-placeholder    //地图软件相关
 ├── car_product         //系统编译相关
 ├── car-support-lib     //android.support.car
 ├── car-systemtest-lib  //系统测试相关
 ├── car-usb-handler     //开机自启,用于管理车机 USB
 ├── CleanSpec.mk
 ├── evs  
 ├── obd2-lib
 ├── PREUPLOAD.cfg
 ├── procfs-inspector
 ├── service    //com.android.car 是一个后台运行的组件,可以长时间运行并且不需要和用户去交互的,这里即使应用被销毁,它也可以正常工作
 ├── tests
 ├── tools   //是一系列的工具,要提到的是里面的 emulator,测试需要用到的。python 写的,通过 adb 可以连接 vehicleHal 的工具,用于模拟测试
 ├── TrustAgent
 └── vehicle-hal-support-lib

Android 通信模式通常基于 C/S 模式,即有客户端和服务端,每个服务有对应的代理对象(比如 ActivityManager 相对服务 AMS,就是客户端)。

对于 CarService 也是采用了 C/S 模式Car App 并不会直接通过 CarService 的实例调用相关功能,而是通过对应的 Car API 完成对服务的调用。这里的 CarService 就是服务端,Car API 就是客户端。

Android 原生 CarService 包含了许多功能服务: Car ** Service(C/S 模式中的服务端)它们与 HAL 层的 VehicleHAL 通信,进而通过车载总线(例如 CAN 总线)与车身进行通讯,同时它们还通过 Car API:Car ** Manger(C/S 模式中的客户端)为应用层的 Car App 提供接口,从而让 App 能够实现对车身的控制与状态的显示。
Car***Manager:packages/services/Car/car-lib/src/android/car/

Car***Service:packages/services/Car/service/src/com/android/car/

以下列举 CarService 中核心服务:

Service 端 功能 Client 端
AppFocusService 管理同类应用焦点的服务 CarAppFocusManager
CarAudioService 汽车音频服务 CarAudioManager
CarPackageManagerService 汽车包管理服务 CarPackageManager
CarDiagnosticService 汽车诊断服务 CarDiagnosticManager
CarPowerManagerService 汽车电源管理服务 CarPowerManager
IInstrumentClusterManagerServcie 仪表服务 IInstrumentClusterManager
CarProjecitonService 投屏服务 CarProjecitonManager
VmsSubscriberService 车辆地图服务 VmsSubscriberManager
CarBluetoothService 汽车蓝牙服务 CarBluetoothManager
CarStorageMonitoringService 汽车存储监控服务 CarStorageMonitoringManager
CarDrivingStateService 汽车驾驶状态服务 CarDrivingStateManager
CarUXRestrictionsService 汽车用户体验限制服务 CarUXRestrictionsManager
CarConfigurationService 汽车配置服务 CarConfigurationManager
CarTrustedDeviceService 授信设备管理 CarTrustAgentEnrollmentManager
CarMediaService 媒体管理服务 CarMediaManager
CarBugreportManagerService 错误报告服务 CarBugreportManager

2.3 使用 CarService

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

前文提到,CarService 需要通过 Car API 为应用层提供接口,所以应用开发者只需要知道如何使用 Car API

第一步:判断平台是否支持车载功能

APP 层在调用 Car API 之前首先会调用 PMS 中的 hasSystemFeature() 方法判断设备是否支持车载功能

java 复制代码
 if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
     .....
 }

源码路径:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java 复制代码
 ​
     @GuardedBy("mAvailableFeatures")
     final ArrayMap<String, FeatureInfo> mAvailableFeatures;
 ​
     @Override
     public boolean hasSystemFeature(String name, int version) {
         // allow instant applications
         synchronized (mAvailableFeatures) {
             final FeatureInfo feat = mAvailableFeatures.get(name);
             if (feat == null) {
                 return false;
             } else {
                 return feat.version >= version;
             }
         }
     }

mAvailableFeatures 里面的内容是通过读取/system/etc/permissions下面的 xml 文件(对应 SDK 的位置---frameworks/native/data/etc 下的 XML 文件中的 feature 字段)

源码路径:frameworks/native/data/etc/car_core_hardware.xml

xml 复制代码
 <permissions>
     <!-- Feature to specify if the device is a car -->
     <feature name="android.hardware.type.automotive" />
     .....
 </permission>

源码路径:frameworks/native/data/etc/android.hardware.type.automotive.xml

xml 复制代码
 <!-- These features determine that the device running android is a car. -->
 <permissions>
     <feature name="android.hardware.type.automotive" />
 </permissions>

第二步:创建 Car 对象,获取 Manager

Car 作为汽车平台最高等级的 API(packages/services/Car/car-lib/src/android/car/Car.java),为外界提供汽车所有服务和数据的访问

  1. 通过createCar()方法新建 Car 实例
  2. 成功连接时可以通过getCarManager方法获取相关的 Manager 实例。比如 Hvac 通过 getCarManager 方法获取了一个 CarHvacManager,当获取到 manager 后就可以进行相关操作
java 复制代码
     // 创建 Car 实例
     Car carApiClient = Car.createCar(context);
     // 获取 CarHvacManager
     CarHvacManager manager = (CarHvacManager) mCarApiClient.getCarManager(Car.HVAC_SERVICE);
java 复制代码
     // 调用 disconnect() 断开连接
      carApiClient.disconnect();

三 CarService 实现原理

想要弄清楚CarService实现方式,首先需要搞明白CarService的启动流程。

CarService 启动流程主要分为以下四个步骤:

  1. SystemServer 启动 CarServiceHelperService 服务
  2. 在调用 startService() 后,CarServiceHelperServiceonStart() 方法通过 bindService 的方式启动 CarService(一个系统级别的 APK,位于 system/priv-app)
  3. 启动 CarService 后首先调用 onCreate(),创建 ICarImpl 对象并初始化,在此时创建了一系列 Car 相关的核心服务,并遍历 init 初始化
  4. 然后调用 onBind 将该 ICarImpl 对象返回给 CarServiceHelperServiceCarServiceHelperService 在内部的一个 Binder 对象 ICarServiceHelperImpl 传递给 CarService,建立双向跨进程

3.1 启动 CarServiceHelperService 服务

SystemServer会在startOtherServices()方法中让SystemServiceManager先通过反射的形式创建出StartCarServiceHelperService对象。

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

java 复制代码
 private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
     ...
     // 仅在 automotive 中启动
     if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
         t.traceBegin("StartCarServiceHelperService");
         final SystemService cshs = mSystemServiceManager
             .startService(CAR_SERVICE_HELPER_SERVICE_CLASS);
         if (cshs instanceof Dumpable) {
             mDumper.addDumpable((Dumpable) cshs);
         }
         if (cshs instanceof DevicePolicySafetyChecker) {
             dpms.setDevicePolicySafetyChecker((DevicePolicySafetyChecker) cshs);
         }
         t.traceEnd();
     }
     ...
 }

然后在SystemServiceManager中调用StartCarServiceHelperServiceonStart()方法。

CarServiceHelperServiceCarService的 SystemService 端的配套服务。

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

java 复制代码
 public SystemService startService(String className) {
     final Class<SystemService> serviceClass = loadClassFromLoader(className,
             this.getClass().getClassLoader());
     return startService(serviceClass);
 }
 ​
 public void startService(@NonNull final SystemService service) {
     // Register it.
 mServices.add(service);
     long time = SystemClock.elapsedRealtime();
     try {
         service.onStart();
     } catch (RuntimeException ex) {
         throw new RuntimeException("Failed to start service " + service.getClass().getName()
                 + ": onStart threw an exception", ex);
     }
     warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart");
 }
 ​

3.2 绑定 CarService 服务

源码路径: frameworks/opt/car/services/src/com/android/internal/car/CarServiceHelperService.java

java 复制代码
     private static final String CAR_SERVICE_INTERFACE = "android.car.ICar";
 ​
     @Override
     public void onStart() {
         EventLog.writeEvent(EventLogTags.CAR_HELPER_START);
 ​
         IntentFilter filter = new IntentFilter(Intent.ACTION_REBOOT);
         filter.addAction(Intent.ACTION_SHUTDOWN);
         mContext.registerReceiverForAllUsers(mShutdownEventReceiver, filter, null, null);
         mCarWatchdogDaemonHelper.addOnConnectionChangeListener(mConnectionListener);
         mCarWatchdogDaemonHelper.connect();
         Intent intent = new Intent();
         intent.setPackage("com.android.car");  // 绑定包名,设置广播仅对该包有效
         intent.setAction(CAR_SERVICE_INTERFACE);  // 绑定 action,表明想要启动能够响应设置的这个 action 的活动,并在清单文件 AndroidManifest.xml 中设置 action 属性
         // 绑定后回调
         if (!mContext.bindServiceAsUser(intent, mCarServiceConnection, Context.BIND_AUTO_CREATE,
                 mHandler, UserHandle.SYSTEM)) {
             Slogf.wtf(TAG, "cannot start car service");
         }
         loadNativeLibrary();
     }

源码路径:packages/services/Car/service/AndroidManifest.xml

sharedUserId 是系统级别的,类似 SystemUI,它编译出来同样是一个 APK 文件

设备文件路径: /system/priv-app/CarService/CarService.apk

xml 复制代码
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
         package="com.android.car"
         coreApp="true"
         android:sharedUserId="android.uid.system"> 
     ......
     <application android:label="@string/app_title"
          android:directBootAware="true"
          android:allowBackup="false"
          android:persistent="true">
 ​
         <service android:name=".CarService"
              android:singleUser="true"
              android:exported="true">
             <intent-filter>
                 <action android:name="android.car.ICar"/>
             </intent-filter>
         </service>
         ......
     </application>

3.3 CarService 初始化

CarService进入启动时序后,会在onCreate()方法中进行一系列自身的初始化操作,步骤如下:

1)通过 HIDL 接口获取到 HAL 层的 IHwBinder 对象IVehicle,与 AIDL 的用法类似,必须持有 IHwBinder 对象我们才可以与 Vehicle HAL 层进行通信。

2)创建 ICarImpl 对象,并调用init方法,它就是ICar.aidl接口的实现类,我们需要通过它才能拿到其他的 Service 的 IBinder 对象。

3)将ICar.aidl的实现类添加到 ServiceManager 中。

4)设定 SystemProperty,将CarService设定为创建完成状态,只有包含CarService在内的所有的核心 Service 都完成初始化,才能结束开机动画并发送开机广播。

源码路径:packages/services/Car/service/src/com/android/car/CarService.java

java 复制代码
     @Override
     public void onCreate() {
         LimitedTimingsTraceLog initTiming = new LimitedTimingsTraceLog(CAR_SERVICE_INIT_TIMING_TAG,
                 Trace.TRACE_TAG_SYSTEM_SERVER, CAR_SERVICE_INIT_TIMING_MIN_DURATION_MS);
         initTiming.traceBegin("CarService.onCreate");
 ​
         initTiming.traceBegin("getVehicle");
         // 获取 hal 层的 Vehicle service
         mVehicle = getVehicle();
         initTiming.traceEnd();
         ...
         //创建 ICarImpl 实例
         mICarImpl = new ICarImpl(this,
                 mVehicle,
                 SystemInterface.Builder.defaultSystemInterface(this).build(),
                 mVehicleInterfaceName);
         //然后调用 ICarImpl 的 init 初始化方法
         mICarImpl.init();
 ​
         linkToDeath(mVehicle, mVehicleDeathRecipient);
         //将该 service 注册到 ServiceManager
         ServiceManager.addService("car_service", mICarImpl);
         //设置 boot.car_service_created 属性
         SystemProperties.set("boot.car_service_created", "1");
 ​
         super.onCreate();
 ​
         initTiming.traceEnd(); // "CarService.onCreate"
     }
 ​
     @Nullable
     private static IVehicle getVehicle() {
         final String instanceName = SystemProperties.get("ro.vehicle.hal", "default");
 ​
         try {
             //该 service 启动文件 hardware/interfaces/automotive/vehicle/2.0/default/android.hardware.automotive.vehicle@2.0-service.rc
             return android.hardware.automotive.vehicle.V2_0.IVehicle.getService(instanceName);
         } catch (RemoteException e) {
             Slog.e(CarLog.TAG_SERVICE, "Failed to get IVehicle/" + instanceName + " service", e);
         } catch (NoSuchElementException e) {
             Slog.e(CarLog.TAG_SERVICE, "IVehicle/" + instanceName + " service not registered yet");
         }
         return null;
     }

接着再看ICarImpl的实现,如下所示:

1)创建各个核心服务对象

2)把服务对象缓存到 CarLocalServices 中,这里主要是为了方便 Service 之间的相互访问

源码路径:/packages/services/Car/service/src/com/android/car/ICarImpl.java

java 复制代码
     @VisibleForTesting
     ICarImpl(Context serviceContext, IVehicle vehicle, SystemInterface systemInterface,
             String vehicleInterfaceName,
             @Nullable CarUserService carUserService,
             @Nullable CarWatchdogService carWatchdogService,
             @Nullable ICarPowerPolicySystemNotification powerPolicyDaemon) {
         ...
         mContext = serviceContext;
         mSystemInterface = systemInterface;
         CarLocalServices.addService(SystemInterface.class, mSystemInterface);
         //创建 VehicleHal 对象
         mHal = constructWithTrace(t, VehicleHal.class,
                 () -> new VehicleHal(serviceContext, vehicle));
         ...
         // 创建核心服务对象,并缓存到 CarLocalServices
         mCarPropertyService = constructWithTrace(t, CarPropertyService.class, () -> new CarPropertyService(serviceContext, mHal.getPropertyHal()));
         mCarDrivingStateService = constructWithTrace(t, CarDrivingStateService.class,() -> new CarDrivingStateService(serviceContext, mCarPropertyService));
         mCarUXRestrictionsService = constructWithTrace(t, CarUxRestrictionsManagerService.class, () -> new CarUxRestrictionsManagerService(serviceContext, mCarDrivingStateService, mCarPropertyService));
         ...
 ​
         // 将创建的服务对象依次添加到一个 list 中保存起来
         List<CarServiceBase> allServices = new ArrayList<>();
         allServices.add(mFeatureController);
         allServices.add(mCarUXRestrictionsService); // mCarUserService depends on it
         allServices.add(mCarUserService);
         allServices.add(mSystemActivityMonitoringService);
         allServices.add(mCarPowerManagementService);
         allServices.add(mCarPropertyService);
         allServices.add(mCarDrivingStateService);
         allServices.add(mCarOccupantZoneService);
         addServiceIfNonNull(allServices, mOccupantAwarenessService);
         allServices.add(mCarPackageManagerService);
         allServices.add(mCarInputService);
         allServices.add(mGarageModeService);   
         ...
     }
 ​
     @MainThread
     void init() {
         LimitedTimingsTraceLog t = new LimitedTimingsTraceLog(CAR_SERVICE_INIT_TIMING_TAG,
                 Trace.TRACE_TAG_SYSTEM_SERVER, CAR_SERVICE_INIT_TIMING_MIN_DURATION_MS);
 ​
         t.traceBegin("ICarImpl.init");
 ​
         t.traceBegin("VHAL.init");
         mHal.init();
         t.traceEnd();
 ​
         t.traceBegin("CarService.initAllServices");
         //启动的所有服务遍历调用 init 初始化(各个都继承了 CarServiceBase)
         for (CarServiceBase service : mAllServices) {
             t.traceBegin(service.getClass().getSimpleName());
             service.init();
             t.traceEnd();
         }
         t.traceEnd(); // "CarService.initAllServices"
 ​
         t.traceEnd(); // "ICarImpl.init"
     }

然后将上面 onCreate() 创建的 mICarImpl 对象返回:

  1. onBind() 回调方法会继续传递通过 bindService() 传递来的 intent 对象(即上面的bindServiceAsUser方法)
  2. onUnbind() 会处理传递给 unbindService() 的 intent 对象。如果 service 允许绑定,onBind() 会返回客户端与服务互相联系的通信句柄

源码路径:/packages/services/Car/service/src/com/android/car/CarService.java

java 复制代码
     @Override
     public IBinder onBind(Intent intent) {
         return mICarImpl;
     }

所以此处的 mICarImpl 会作为 IBinder 返回给CarServiceHelperService.java - bindServiceAsUser方法中的参数 mCarServiceConnection(回调)

3.4 回调 ServiceConnection

ICarImpl 初始化完毕,会作为 IBinder 返回给CarServiceHelperService.java - bindServiceAsUser方法中绑定此服务的 mCarServiceConnection(回调)

mCarServiceConnection 初始化如下:

  1. 其中返回的 ICarImpl 被保存在了 CarServiceHelperServicemCarService
  2. mCarService.transact 跨进程通信,调用 ICar.aidl 中定义的第一个方法 setCarServiceHelper

源码路径:/frameworks/opt/car/services/src/com/android/internal/car/CarServiceHelperService.java

java 复制代码
 private static final String CAR_SERVICE_INTERFACE = "android.car.ICar";
 private IBinder mCarService;
 private final ICarServiceHelperImpl mHelper = new ICarServiceHelperImpl();
 ​
     private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
             if (DBG) {
                 Slogf.d(TAG, "onServiceConnected: %s", iBinder);
             }
             handleCarServiceConnection(iBinder);
         }
 ​
         @Override
         public void onServiceDisconnected(ComponentName componentName) {
             handleCarServiceCrash();
         }
     };
     
     @VisibleForTesting
     void handleCarServiceConnection(IBinder iBinder) {
         synchronized (mLock) {
             if (mCarServiceBinder == iBinder) {
                 return; // already connected.
             }
             Slogf.i(TAG, "car service binder changed, was %s new: %s", mCarServiceBinder, iBinder);
             //1. 返回的 ICarImpl 被保存在了 CarServiceHelperService 的 mCarServiceBinder
             mCarServiceBinder = iBinder;
             Slogf.i(TAG, "**CarService connected**");
         }
 ​
         sendSetSystemServerConnectionsCall();
         ...
     }
 ​
     private void sendSetSystemServerConnectionsCall() {
         Parcel data = Parcel.obtain();
         data.writeInterfaceToken(CAR_SERVICE_INTERFACE);
         data.writeStrongBinder(mHelper.asBinder());
         //将 ICarServiceHelperImpl 类型的对象作为数据跨进程传递
         data.writeStrongBinder(mCarServiceConnectedCallback.asBinder());
         IBinder binder;
         synchronized (mLock) {
             binder = mCarServiceBinder;
         }
         int code = IBinder.FIRST_CALL_TRANSACTION;
         try {
             //2. 跨进程传输
             //对端是 mCarService 即 ICarImpl,调用 binder 的 transact 进行跨进程通信
             //其 code 代表需要调用的对端方法,data 为携带的传输数据
             //FIRST_CALL_TRANSACTION  = 0x00000001,即调用对端 ICar.aidl 中定义的第一个方法 setCarServiceHelper
             if (VERBOSE) Slogf.v(TAG, "calling one-way binder transaction with code %d", code);
             // oneway void setSystemServerConnections(in IBinder helper, in IBinder receiver) = 0;
             binder.transact(code, data, null, Binder.FLAG_ONEWAY);
             if (VERBOSE) Slogf.v(TAG, "finished one-way binder transaction with code %d", code);
         }
         ...
     }

跨进程 setSystemServerConnections

java 复制代码
     @Override
     public void setSystemServerConnections(IBinder helper, IBinder receiver) {
         Bundle bundle;
         try {
             EventLog.writeEvent(EventLogTags.CAR_SERVICE_SET_CAR_SERVICE_HELPER,
                     Binder.getCallingPid());
             assertCallingFromSystemProcess();
             //将 ICarServiceHelper 的代理端保存在 ICarImpl 内部 mICarServiceHelper
             ICarServiceHelper carServiceHelper = ICarServiceHelper.Stub.asInterface(helper);
             synchronized (mLock) {
                 mICarServiceHelper = carServiceHelper;
             }
             //同时也传给了 SystemInterface
             //此时他们有能力跨进程访问 CarServiceHelperService
             mSystemInterface.setCarServiceHelper(carServiceHelper);
             mCarOccupantZoneService.setCarServiceHelper(carServiceHelper);
             mCarUserService.setCarServiceHelper(carServiceHelper);
             ...
     }

3.5 小结

CarService的启动时序如下所示:


四 总结

本文讲解了CarService的总体结构、使用方法及启动流程。CarService中实现的功能非常庞大,可以说相比传统手机端的 Android 系统,AAOS 中独特且最重要的部分都在 Framework 的CarService中。

  • 首先 CarService 是一个系统级别的服务 APK,类似 SystemUI,其在开机时由 SystemServer 通过 CarServiceHelperService 启动。
  • CarServiceHelperService 通过绑定服务的方式启动 CarService ,启动之后创建了一个 Binder 对象 ICarImpl ,并通过 onBind 返回给 system_server 进程。
  • ICarImpl 构造方法中创建了一系列和汽车相关的核心服务,并依次启动这些服务即调用各自 init 方法。ICarImpl 返回给 CarServiceHelperService 之后,CarServiceHelperService 也将其内部的一个 Binder 对象(ICarServiceHelperImpl )传递到了 CarService 进程,自此 CarServicesystem_server 两个进程建立了双向 Binder 通信。
  • ICarImpl 返回给 CarServiceHelperService 之后,CarServiceHelperService 也将其内部的一个 Binder 对象(ICarServiceHelperImpl )传递到了 CarService 进程,自此 CarServicesystem_server 两个进程建立了双向 Binder 通信。

五 参考

Android carservice 架构及启动流程

【Android R】车载 Android 核心服务 - CarService 解析

相关推荐
F-2H12 分钟前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
你好helloworld15 分钟前
《操作系统真象还原》第九章(一) —— 在内核空间中实现线程
操作系统
gqkmiss1 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
mmsx1 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
m0_748247553 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255023 小时前
前端常用算法集合
前端·算法
真的很上进4 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203984 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2344 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
众拾达人4 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言