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

车载应用正在改变人们的出行体验。从导航到娱乐、从安全到信息服务,车载应用的开发已成为汽车智能化发展的重要组成部分。而对于开发者来说,如何将自己的应用程序无缝集成到车载系统中,利用汽车的硬件和服务能力,是一个极具挑战性的话题

那么,在Android平台上开发车载应用时,CarService究竟扮演了什么样的角色?它的功能和使用方法有哪些关键点?

随着车联网技术的普及,车载应用的种类和功能也在迅速增加,从简单的导航和音乐播放,到智能语音助手、驾驶行为分析等多样化的服务。CarService作为Android车载应用开发的基础组件,为开发者提供了与车辆深度交互的能力。在未来,车载应用的智能化、个性化将进一步提升驾驶体验,并成为汽车产品竞争的重要元素。

03 CarService 实现原理

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

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

  1. SystemServer 启动 CarServiceHelperService 服务

  2. 在调用 startService() 后,CarServiceHelperService 的onStart() 方法通过 bindService 的方式启动 CarService(一个系统级别的 APK,位于 system/priv-app)

  3. 启动 CarService 后首先调用 onCreate(),创建 ICarImpl 对象并初始化,在此时创建了一系列 Car 相关的核心服务,并遍历 init 初始化

  4. 然后调用 onBind 将该 ICarImpl 对象返回给CarServiceHelperService,CarServiceHelperService 在内部的一个 Binder 对象 ICarServiceHelperImpl传递给 CarService,建立双向跨进程

3.1 启动 CarServiceHelperService 服务

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

  • 源码路径:frameworks/base/services/java/com/android/server/SystemServer.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中调用StartCarServiceHelperService的onStart()方法。

CarServiceHelperService是CarService的 SystemService 端的配套服务。

  • 源码路径: 

frameworks/base/services/core/java/com/android/server/SystemServiceManager.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​​​​​​​

     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

 <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​​​​​​​

     @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​​​​​​​

     @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​​​​​​​

     @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 被保存在了 CarServiceHelperService 的 mCarService

  2. mCarService.transact 跨进程通信,调用 ICar.aidl 中定义的第一个方法 setCarServiceHelper

  • 源码路径:

/frameworks/opt/car/services/src/com/android/internal/car/CarServiceHelperService.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​​​​​​​

     @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的启动时序如下所示:

 

 

04 总结

本文讲解了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 进程,自此 CarService 和 system_server 两个进程建立了双向 Binder 通信。

  • ICarImpl 返回给 CarServiceHelperService 之后,CarServiceHelperService 也将其内部的一个 Binder 对象(ICarServiceHelperImpl)传递到了 CarService 进程,自此 CarService 和 system_server 两个进程建立了双向 Binder 通信。

 

CarService为Android车载应用开发者提供了一个强大而灵活的平台,让应用程序能够充分利用汽车的硬件和服务能力,打造更加智能化和便捷的驾驶体验。掌握CarService的使用,是车载应用开发中的重要一环,也是实现车载生态系统中创新应用的关键。

“在车载应用的世界里,技术的每一次进步,都是为了让行驶的每一公里更加安全、便捷和愉悦。”

 END 

链接:https://juejin.cn/post/7353827463632404517  本文为转载,转载文章所包含的文字来源于作者。如因内容或版权等问题,请联系进行删除

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/144571.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

BPG的定义和工作原理是什么?

在当今互联网中&#xff0c;网络通信的关键角色之一是BGP&#xff08;边界网关协议&#xff09;。BGP是一种路由矢量协议&#xff0c;负责在自治系统之间交换路由信息&#xff0c;并帮助数据包找到通过网络的理想链路。本文将介绍BGP、BGP4和BGP4&#xff0c;以及它们在网络中的…

深入探索Android开发之Kotlin核心技术学习大全

Android作为全球最流行的移动操作系统之一&#xff0c;其开发技能的需求日益增长。本文将为您介绍一套专为Android开发者设计的Kotlin核心技术学习资料&#xff0c;包括详细的学习大纲、PDF文档、源代码以及配套视频教程&#xff0c;帮助您从Kotlin基础到高级特性&#xff0c;再…

观《中国数据库前世今生》有感:从历史中汲取未来的力量

观《中国数据库前世今生》有感&#xff1a;从历史中汲取未来的力量 中国数据库技术的起步与发展 观看了《中国数据库前世今生》后&#xff0c;我对于中国数据库技术的历史变迁有了更深刻的理解。作为一名有一年开发经验的程序员&#xff0c;这部纪录片让我对中国数据库行业从8…

解析药用植物重楼甾体皂苷生物合成中的连续糖基化及其抗真菌作用-文献精读49

Unraveling the serial glycosylation in the biosynthesis of steroidal saponins in the medicinal plant Paris polyphylla and their antifungal action 解析药用植物重楼甾体皂苷生物合成中的连续糖基化及其抗真菌作用 摘要 糖–糖糖基转移酶在构建复杂的具有生物活性的…

解决Filament中使用ARCore出现绿色闪屏的问题

解决Filament中使用ARCore出现绿色闪屏的问题 问题现象 使用AR的工程中&#xff0c;出现绿屏闪烁。问题帧截图如下&#xff1a; 问题定位 问题来源 在filament的1.21.0之前的版本&#xff0c;Stream对象提供了stream(long externalTextureId)方法&#xff0c;允许传递一个…

游戏如何对抗定制挂

近年来&#xff0c;游戏安全对抗强度相比以往更加激烈&#xff0c;具体表现在“定制挂”趋势显著。在近期收集的近万款外挂样本中&#xff0c;定制挂约占比78%&#xff0c;常见的内存修改器、变速器等通用作弊手段占比正在下降。 所谓定制挂&#xff0c;是指针对某款游戏单独开…

阅读笔记——《围城》

文前辅文&#xff1a;围在城里的人想逃出来&#xff0c;/城外的人想冲出去&#xff0c;/对婚姻也罢&#xff0c;职业也罢&#xff0c;/人生的愿望大都如此。 笔记 方鸿渐在海外&#xff08;欧洲&#xff09;留学&#xff0c;期间一直在玩&#xff0c;最终买了一个学位后回国&a…

SVM原理

SVM 这里由于过了很长时间 博主当时因为兴趣了解了下 博主现在把以前的知识放到博客上 作为以前的学习的一个结束 这些东西来自其他资料上 小伙伴看不懂英文的自行去翻译下吧 博主就偷个懒了 多维空间和低维空间 不一样的分法&#xff0c;将数据映射到高维 &…

为什么越来越多的企业选择设置外包岗位?

对于很多人而言&#xff0c;外包并不陌生。特别是在互联网大厂&#xff0c;像阿里、腾讯等&#xff0c;其内部有不少岗位都采取了外包形式。甚至政府部门也会把IT/自媒体之类的外包给专业的公司来运营&#xff0c;对于企业或者单位来说&#xff0c;这是进行成本优化后的最佳选择…

假期学习--iOS 编译链接

iOS 编译链接 编译流程 四步&#xff1a; 1.预处理 2.编译 3.汇编 4.链接 大概的步骤如下&#xff1a; 预处理 作为编译的第一步&#xff0c;将.m文件转换为.i文件 &#xff1b; 预处理是要处理源代码中所有以#开头的所有预编译指令 &#xff1b; 规则如下&#xff1…

Flutter局域网广播(UDP通信)与TCP通信

前言 现在有一个需求&#xff0c;手机和ESP32通过WIFI进行通信。流程如下&#xff1a; 手机创建TCP服务器手机向192.168.0.255的1002端口广播自己的ip地址以及TCP服务器的端口号ESP32监听到1002的广播内容后&#xff0c;连接手机的TCP服务器。最后就是ESP32硬件和TCP服务器进…

【Python】Anaconda插件:Sublime Text中的Python开发利器

上班的时候没人问我苦不苦&#xff0c;下班的时候总有人问为什么走这么早。 Anaconda 是一个专为Sublime Text打造的开源Python开发插件&#xff0c;旨在为开发者提供类似于IDE的丰富功能&#xff0c;提升Python编码效率。该插件提供了代码补全、语法检查、代码片段提示等多项…

【Elasticsearch】-图片向量化存储

需要结合深度学习模型 1、pom依赖 注意结尾的webp-imageio 包&#xff0c;用于解决ImageIO.read读取部分图片返回为null的问题 <dependency><groupId>org.openpnp</groupId><artifactId>opencv</artifactId><version>4.7.0-0</versio…

Java线程---并发集合

List CopyOnWriteArrayList 并发修改时保证线程安全 通过ReentrantLock实现多个线程并发修改时的线程安全同步&#xff08;添加元素的同时&#xff0c;不允许删除&#xff09; 添加新元素&#xff1a;list.add("") 按照指定下标替换元素&#xff1a;list.set(index…

图像超分辨率技术代码分享

图像超分辨率是一种计算机视觉技术&#xff0c;用于提高图像的分辨率&#xff0c;即将低分辨率图像转换为高分辨率图像。这项技术主要应用在各种场合&#xff0c;如卫星图像处理、医学成像、视频增强和老照片修复等领域。 涉及到的技术包括&#xff1a; 扩散模型&#xff1a;这…

教你把PDF电子画册加背景音乐

​如何让您的PDF电子画册更具吸引力&#xff0c;让人一眼就能爱上它呢&#xff1f;答案就是为画册添加背景音乐&#xff01;添加背景音乐的PDF电子画册相较于普通画册&#xff0c;更能吸引读者的注意力&#xff0c;提升阅读体验。那么&#xff0c;如何为PDF电子画册添加背景音乐…

LVGL 控件之仪表盘(lv_meter)

目录 一、概述二、仪表盘部件1、添加刻度2、添加指针3、设置仪表的角度和仪表的范围4、装饰4.1 仪表指针图片4.2 仪表的指示刻度4.3 仪表弧线指示器 5、API 函数 一、概述 仪表盘部件可以非常灵活地展示数据&#xff0c;其功能包括显示弧形&#xff08;arcs&#xff09;、指针…

二叉树层序遍历的2种方法

方法1使用1个队列 class Solution {//使用一个队列&#xff0c;如果一个队列可以解决&#xff0c;那么就不需要使用2个数组//要最下面一层的最左边的结点&#xff0c;那么就先入右结点再入左结点public int findBottomLeftValue(TreeNode root) {TreeNode node root;Deque&l…

表格HTML

//test.html <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <title>表格与CSS分开示例</tit…

openEuler 22.03 LTS 安装JDK 8(亲测很简单的安装办法)

Oracle JDK从2019年4月16日起开始商业用途统统收费&#xff0c;也就是说只能使用2019年1月15发布的JDK 8u202免费版本&#xff0c;没有订阅&#xff08;交钱&#xff09;是没安全更新可用的&#xff0c;所以只能使用替代品OpenJDK、AdoptOpenJDK等发布版&#xff0c;那为什么不…