【视频文稿】车载Android应用开发与分析 - 开发系统应用
本期视频地址:https://www.bilibili.com/video/BV1NY411z7TK/
前言
Hello,大家好,我是林栩。
开发车载应用,其实主要都是在Android系统中编写各种系统应用,所以上期视频先介绍了Android系统源码的下载和编译流程,本期视频我们开始介绍,Android系统应用是如何开发的。
系统应用简介
我们第一次启动Android系统的手机时,会发现手机中已经预先安装了很多应用,例如:系统设置、桌面等等。这些应用并不是通过普通的方法安装到系统上的,而是直接嵌入在Android ROM中,直接刷写到硬件里的。通过这种方式安装的应用,无法使用通常的方法卸载。只有在获取root权限后,删除对应目录下的的apk文件(或者刷机),否则无法移除这些系统应用。
除此以外,我们还会发现系统应用拥有远超普通的应用的权限,以系统设置为例,它可以切换当前系统的用户类型,设置其它应用的通知权限,甚至于可以卸载Android系统上的普通应用,这些功能都是普通应用无法实现的,原因就在于Android SDK中有很多没有公开的API,这些API只允许系统应用调用。
所以,我们可以总结系统应用具有以下特点:
- 可以调用Android SDK未公开的私有API。
- 拥有更高的系统权限。
- 直接嵌入到Android ROM中,普通方法无法卸载。
系统应用准备条件
接下来我们演示如何编写一个 Android 系统应用,不过在此之前我们还需要做以下的准备:
第 1 步,制作 API 包
系统应用的特点决定了它的开发方式与普通的Android应用并不完全一样。首先系统应用可以调用Android SDK隐藏的API,这需要我们引入包含被隐藏API的jar包。当然如果不需要调用隐藏API,这一步可以跳过。在实际项目中,这一步会由负责framework开发的同事协助完成,因为farmework层一般都有新增的接口需要一起打包。
1)编译Android framework
我们可以使用make framework
指令编译 framework 的源码,或者使用mmm frameworks/base
以及在/framework/base
目录下执行mm
都可以。
但是要注意 make 指令后跟的是 module name 而不是模块的路径,所以这里不能写成 frameworks。
编译 Android 11和以后版本,编译指令有所调整,使用make framework-minus-apex
编译成功后,进入/out/target/common/obj/JAVA_LIBRARIES/framework-minus-apex_intermediates/
目录,该目录下的classes-header.jar
就是我们需要的jar包。
classes-header.jar
中包含了Android SDK中没有公开的API,例如:用于启用RRO机制的OverlayManager
。
如果没有下载AOSP源码,上述编译好的framework.jar可以去本视频的github仓库中下载,github地址[https://github.com/linxu-link/CarAndroidCourse]可以在本视频的简介中查看。
2)导入 Android Studio
生成 framework.jar 后,我们把它导入到 Android studio中,并在工程目录的 build.gradle中加入以下代码。
allprojects{gradle.projectsEvaluated {//Arctic Foxtasks.withType(JavaCompile) {Set<File> fileSet = options.bootstrapClasspath.getFiles()List<File> newFileList = new ArrayList<>();newFileList.add(new File("./app/libs/framework_header.jar"))newFileList.addAll(fileSet)options.bootstrapClasspath = files(newFileList.toArray())}}
}
在App目录的build.gradle中以compileOnly的形式引入jar包。
compileOnly files('libs/framework_header.jar')
第 2 步,制作系统签名
Android系统会识别应用的签名类型并根据签名类型赋予应用相应的权限等级,将普通应用提升为系统应用的重要条件就是应用需要使用系统签名。所以在这一步我们要先制作一份系统签名,方便我们在开发时调试应用。
1) 控制台进入AOSP的build目录
cd build/target/product/security
2)制作系统签名
openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out [platform.pem]0 -nocryptopenssl pkcs12 -export -in platform.x509.pem -inkey [platform.pem] -out [platform.pk12] -name [key的别名] -password pass:[key的密码]keytool -importkeystore -deststorepass [key的密码] -destkeypass [key的密码] -destkeystore platform.jks -srckeystore platform.pk12 -srcstoretype PKCS12 -srcstorepass [key的密码] -alias [android]
制作完成后,会在当前目录下载生成一个platform.jks的签名文件,将它导入到android studio中即可对应用进行签名。
3)导入 Android Studio
将platform.jks放置在App目录下,并build.gradle中加入以下代码。
signingConfigs {sign {storeFile file('platform.jks')storePassword '123456'keyAlias 'android'keyPassword '123456'}
}buildTypes {release {minifyEnabled falsesigningConfig signingConfigs.sign}debug {minifyEnabled falsesigningConfig signingConfigs.sign}
}
将系统签名引入android studio后,app工程就可以直接在Android模拟器中调用系统API,同时也可以获取更高等级的权限了。
注意:基于AOSP源码制作的test key文件,一般无法使用在真实环境中(例如:手机),车载项目则较为复杂,有的项目在开发阶段,就会使用较为严格的签名校验,那么AOSP的签名文件也是无法使用的。不过也有项目,会在最后的量产阶段更换签名,那么在此之前AOSP中test key依然可以使用。
有关签名文件补充资料如下:
在Android源码的build/target/product/security/目录下有如下5对常见的KEY:
-
media.pk8与media.x509.pem
适用于媒体/下载系统所包含的 apk 包的测试密钥。
-
platform.pk8与platform.x509.pem
适用于核心平台所包含的 apk 包的测试密钥。
-
shared.pk8与shared.x509.pem
适用于家庭/联系人进程中的共享内容的测试密钥。
-
testkey.pk8与testkey.x509.pem
适用于未另外指定密钥的 apk 包的通用默认密钥。
-
networkstack.pk8与networkstack.x509.pem
适用于网络系统所包含的 apk 包的测试密钥。
其中,“.pk8”文件为私钥,“.x509.pem”文件为公钥。注意,此目录中的测试密钥仅用于开发,不得用于在公开发布的映像中签署包。
有关密钥的更多内容,可以阅读官方的文档:https://source.android.com/docs/core/ota/sign_builds?hl=zh-cn
而这些密钥如何与被签名的APK对应上呢?在APK源码目录下的Android.bp
文件中有certificate
字段,用于指定签名时使用的KEY,如果不指定,默认使用testkey。系统应用对应的certificate
可设定为如下的值。
certificate: "platform"
certificate: "shared"
certificate: "media"
而在Android.bp
中的这些配置,需要在APK源码的AndroidManifest.xml文件中的<manifest>
节点添加如下内容:
android:sharedUserId="android.uid.system"
android:sharedUserId="android.uid.shared"
android:sharedUserId="android.media"
实践系统应用
第 1 步,定义需求
为了让各位能直观的感受到『系统应用』与『普通应用』的区别,我们要求『系统应用』完成以下的功能:
- 应用在系统开机后自行启动,即开机自启
- 开机后覆盖一个 View 在屏幕上,且不需要授权『显示在其它应用的上层』
- 应用被杀死后自动拉起,即进程保活
这些功能都是在『普通应用』上难以实现的需求,我们演示一下在『系统应用』上是如何实现的。
第 2 步,修改AndroidManifest.xml
开机自启与进程保活两项功能,Android系统本身已经提供了相应的机制来实现,我们只需要在manifest.xml中进行配置即可。
- persistent
设定应用是否保持常驻状态。默认值为false
,设定为true
为开启常驻模式,常驻模式仅适用于系统应用。
开启常驻模式后,应用会在Android系统开机动画播放完毕之前,就会完成启动,同时应用会常驻后台,即使被杀死后也会立即拉起。
<applicationandroid:persistent="true">
除此以外,系统应用中还有一些可能较为常用的属性可以配置,我们逐一介绍。
- android:sharedUserId
设定不同用户间共享数据。 默认情况下,Android 会为每个应用分配其唯一用户 ID。如果两个或多个应用将此属性设置为相同的值,则这些应用都将共享相同的 ID,前提是这些应用的签名完全相同。具有相同用户 ID 的应用可以访问彼此的数据,如果需要的话,还可以在同一进程中运行。
API 级别 29 中已弃用此属性。 注意,由于现有应用无法移除此值,这类应用应添加 android:sharedUserMaxSdkVersion=“32” ,以免在新用户安装时使用共享用户 ID。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"android:sharedUserId="android.uid.system"android:sharedUserMaxSdkVersion="32">
- directBootAware
直接启动模式。直接启动模式是在Android7.0之后出现的,当设备已正常开机但尚未解锁时,称设备处于DirectBoot模式。默认情况下,应用不会在DirectBoot模式下启动,即使是系统应用。
如果应用需要在DirectBoot模式下启动,可以在manifext.xml将directBootAware属性设定为true。
<application android:directBootAware="true" >
需要在“直接启动”模式下运行的一些常见应用用例包括:
- 已安排通知的应用,如闹钟应用;
- 提供重要用户通知的应用,如短信应用;
- 提供无障碍服务的应用,如 Talkback;
- 关键的系统服务,如CarService等。
注意,对应用程序而言,存储空间分为以下两种
- Credential encrypted storage,凭据加密存储区。默认存储数据的地方,仅在用户解锁手机后可用。
- Device encrypted storage,设备加密存储区。主要对应的就是DirectBoot时使用的存储空间。该存储空间在DirectBoot模式下和用户解锁手机后都可以使用。
0-当Android系统开机后,首先进入一个DirectBoot模式,如果应用在DirectBoot模式下运行时需要访问本地数据,可以通过调用Context.createDeviceProtectedStorageContext()
创建一个特殊的Context
实例。通过此实例发出的所有存储类 API 调用均可以访问设备的加密存储。如下所示:
Context directBootContext = appContext.createDeviceProtectedStorageContext();// Access appDataFilename that lives in device encrypted storageFileInputStream inStream = directBootContext.openFileInput(appDataFilename);// Use inStream to read content...
如果需要监听屏幕解锁的时机,可以注册下面的广播
<receiverandroid:directBootAware="true" >...<intent-filter><action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" /></intent-filter></receiver>
一些关键的系统应用或服务需要在Android屏幕解锁前完成启动并开始运行,这种情况就可以配置为直接启动模式。此时必仔细阅读官方文档,防止出现意外的bug,官方文档:https://developer.android.google.cn/training/articles/direct-boot?hl=zh-cn
- uses-library
指定应用必须与之关联的共享库。 该标签会告知系统将库的代码添加到软件包的类加载器中。
车载应用项目中可能会它用来加载一些framework自定义的共享库。
<uses-libraryandroid:name="string"android:required=["true" | "false"] />
android:name
库的名称。此名称由您使用的软件包的文档提供。例如,“android.test.runner
”是一个包含 Android 测试类的软件包。
android:required
指示应用是否需要 android:name
指定的库:
"true"
:如果没有此库,则应用将无法正常运行。系统不允许在没有此库的设备上安装应用。"false"
:应用可以使用此库(如果存在),但专门在没有此库的情况下运行(如果有必要)。系统允许安装应用,即使不存在此库也是如此。如果您使用"false"
,则需要在运行时检查有没有此库
完整的androidmanifest.xml配置如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"android:sharedUserId="android.uid.system"android:sharedUserMaxSdkVersion="32"xmlns:tools="http://schemas.android.com/tools"><uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /><applicationandroid:name=".SystemApplication"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:persistent="true"android:supportsRtl="true"android:theme="@style/Theme.SystemApp"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><serviceandroid:name=".SystemService"android:enabled="true"android:exported="true"><intent-filter><action android:name="com.android.systemapp.action" /></intent-filter></service></application></manifest>
第 3 步,编写逻辑代码
本应用中只有一个Service,在Application中启动该Service。
class SystemApplication : Application() {override fun onCreate() {super.onCreate()Log.e("System", "System APP started")val intent = Intent()intent.setPackage("com.android.systemapp")intent.setAction("com.android.systemapp.action")startService(intent)}
}
在Service中我们通过WindowManager绘制一个View,系统动画没有播放完毕之前,该View是无法进行绘制和显示的。换句话说,当这个View可以绘制时,系统动画已经播放完毕且SystemUI已经显示出来了。
// 创建用于 window 显示的context
val dm = getSystemService(DisplayManager::class.java)
val defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY)
val defaultDisplayContext = createDisplayContext(defaultDisplay)
val ctx = defaultDisplayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);// 在屏幕上绘制一个像素的view,用于监控开机动画是否播放完毕
val mWindowManager = ctx.getSystemService(WindowManager::class.java)
val bounds = mWindowManager.getCurrentWindowMetrics().getBounds();val windowSizeTest: View = object : View(ctx) {override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {Log.e(TAG, "system launch")}
}
Service 完整代码如下:
class SystemService : Service() {private val TAG = SystemService::class.java.simpleName;override fun onBind(intent: Intent): IBinder? {return null}override fun onCreate() {super.onCreate()// 创建用于 window 显示的contextval dm = getSystemService(DisplayManager::class.java)val defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY)val defaultDisplayContext = createDisplayContext(defaultDisplay)val ctx = defaultDisplayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);// 在屏幕上绘制一个像素的view,用于监控开机动画是否播放完毕val mWindowManager = ctx.getSystemService(WindowManager::class.java)val bounds = mWindowManager.getCurrentWindowMetrics().getBounds();val windowSizeTest: View = object : View(ctx) {override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {// 暂停5秒后,移除该ViewThread{sleep(5_000)mWindowManager.removeView(this)}.start()}}val testParams: WindowManager.LayoutParams = WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,WindowManager.LayoutParams.FLAG_NOT_FOCUSABLEand WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ONand WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)testParams.width = bounds.width() / 2testParams.height = bounds.height()/2testParams.gravity = Gravity.CENTERtestParams.title = TAGmWindowManager.addView(windowSizeTest, testParams)}
}
第 4 步,验证
在这一步中,我们通过Android Studio中的模拟器来验证系统应用的运行方式是否符合我们的预期。
将编写好的系统应用 push 到System/app/下,不过由于模拟器的 System 分区不开放写入权限,在此之前我们需要先获取 System 分区的写入权限。
1)修改模拟器写入权限
首先进入Android SDK 模拟器目录执行如下指令,控制台出现 remount succeeded 的信息,即表示修改写入权限成功了。
./emulator -list-avds
./emulator -writable-system -avd [10.1_WXGA_Tablet_API_31] -no-snapshot-load -qemu // 修改分区写入权限吧
adb root
adb remount
adb reboot // 重启模拟器
// 等待模拟器重启后
adb root
adb remount
2)将应用 apk push到 system/app/xxx 目录
在system/app目录下新建一个SystemApp
(名称任意),然后将 apk push到该目录下。
3)重启模拟器,查看效果
模拟器重启后,SystemApp进程会自动启动,并在屏幕上覆盖一个黑色View,整个过程中 SystemApp 没有弹出权限申请的窗口。
如果我们使用adb kill [进程号]
杀死 SystemApp,系统会立即将 SystemApp 进程拉起。普通应用上难以实现的进程保活在『系统应用』上轻而易举的就可以达成了,而且进入系统设置中查看 SystemApp 发现 SystemApp 实际上也无法被卸载。
总结
本期视频我们介绍了Android系统应用的开发方式,车载 Android 应用开发说到底都是在做系统应用开发,了解系统应用的开发方式是我们入门车载 Android 应用开发最基本的技术要求。
好,以上就是本视频的全部内容了。本视频的文字内容发布在我的个人微信公众号-『车载 Android』和我的个人博客中,视频中使用的 PPT 文件和源码发布在我的Github[https://github.com/linxu-link/CarAndroidCourse]中,在本视频的简介里可以找到相应的地址。
感谢您的观看,我们下期视频再见,拜拜。
参考资料
https://developer.android.google.cn/guide/topics/manifest/application-element?hl=zh-cn#persistent
https://developer.android.google.cn/guide/topics/manifest/manifest-element?hl=zh-cn#uid
相关文章:

【视频文稿】车载Android应用开发与分析 - 开发系统应用
本期视频地址:https://www.bilibili.com/video/BV1NY411z7TK/ 前言 Hello,大家好,我是林栩。 开发车载应用,其实主要都是在Android系统中编写各种系统应用,所以上期视频先介绍了Android系统源码的下载和编译流程&…...

Scala流程控制
目录 单分支 双分支 多分支 for 循环控制 循环守卫 循环步长 循环嵌套 循环返回值 While 和 do..While 循环控制 While循环控制 do..While 循环控制 循环中断 单分支 if (条件表达式) {执行代码块 }var age StdIn.readShort()if (age < 18){println("童年&quo…...

人脸活体检测系统(Python+YOLOv5深度学习模型+清新界面)
摘要:人脸活体检测系统利用视觉方法检测人脸活体对象,区分常见虚假人脸,以便后续人脸识别,提供系统界面记录活体与虚假人脸检测结果。本文详细介绍基于YOLOv5深度学习技术的人脸活体检测系统,在介绍算法原理的同时&…...

prometheus03-如何导出prometheus指标
Prometheus是一个开源的监控系统和时间序列数据库,用于收集和存储服务的指标数据。要导出Prometheus指标,你需要使用或实现一个Prometheus Exporter。以下是一个简单的指南,分为三个主要步骤: 选择或实现Prometheus Exporter Pr…...

Linux驱动开发——串口设备驱动
Linux驱动开发——串口设备驱动 一、串口简介 串口全称叫做串行接口,通常也叫做 COM 接口,串行接口指的是数据一个一个的顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距…...

LeetCode--缺失的第一个正数(41)和 接雨水(42)
目录 缺失的第一个正数 接雨水 0ms,100% 代码 缺失的第一个正数 来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/first-missing-positive 题目:给你一个未排序的整数数组 nums ,请…...

java源码阅读---ReentrantLock源码解析
ReentrantLock源码解读 在讲ReentrantLock之前我们先看一下Lock接口里的方法 Lock接口中的方法 lock()方法 void lock(); //直接加锁,如果加锁失败什么也不返回lockInterruptibly()方法 void lockInterruptibly() throws InterruptedException;lockInterruptibly()方法能够…...

OpenCv + Qt5.12.2 文字识别
OpenCv Qt5.12.2 文字检测与文本识别 前言 好久没有进行一些相关的更新的了,去年一共更新了四篇,最近一直在做音视频相关的直播服务,又是重新学习积攒经验的一个过程。去年疫情也比较严重,等到解封,又一直很忙&a…...

网络作业1【计算机网络】
网络作业1【计算机网络】前言推荐网络作业1一. 单选题(共7题,58.1分)二. 多选题(共1题,8.3分)三. 判断题(共4题,33.6分)最后前言 2023-3-13 20:11:42 以下内容源自《计…...

常见背包问题
一.前言若你想学习或正在学习动态规划,背包问题一定是你需要了解的一种题型,并且大多数人最初都是从背包问题入坑进而打开动态规划这一大门。背包问题分为多种,你可以先掌握最常见的主要是三类:01背包、完全背包、多重背包二.分析…...

【python】python编译器以及安装
✅作者简介:一名在读大二学生,希望大家多多支持 🔥系列专栏:python 💬个人主页:小园园子的CSDN博客 python编译器以及安装一、编译器与解释器详细内容Python解释器种类Python的运行机制二、python环境搭建p…...

Effective C++快速复习
Effective C快速复习 习惯 C 01 视 C 为一个语言联邦:C、Object-Oriented C、Template C、STL 02 尽量以 const, enum, inline 替换 #define:其实是尽量以编译器替换预处理器比较好,因为 #define 只是简单的字符串匹配替换,编译…...

【华为OD机试真题JAVA】绘图机器的绘图问题
标题:绘图机器的绘图问题| 时间限制:1秒 | 内存限制:262144K | 语言限制:不限 绘图机器的绘图笔初始位置在原点(0,0) 机器启动后按照以下规则来进行绘制直线 1. 尝试沿着横线坐标正向绘制直线 直到给定的终点E 2. 期间可以通过指令在纵坐标轴方向进行偏移 off…...

GPT-4最震撼我的一点
昨天我看了一遍OpenAI发的视频和论文,最震撼我的并不是根据手绘草图生成HTML页面代码,因为草图太简单,对于复杂的有交互的界面,还不知道它的能力究竟如何,能不能生成准确的、清晰的代码,我再实验一下再给大…...

LeetCode-复制带随机指针的链表
题目描述: 给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的…...

如何在Unity中实现AStar寻路算法及地图编辑器
文章目录AStar算法简介实现Node节点节点间的估价算法核心邻节点的搜索方式地图编辑器简介实现绘制地图网格障碍/可行走区域地图数据存储AStar算法 简介 Unity中提供了NavMesh导航寻路的AI功能,如果项目不涉及服务端它应该能满足大部分需求,但如果涉及服…...

线性代数之矩阵
一、思维导图二、矩阵及其运算1、矩阵的定义注:零矩阵:元素均为0 的矩阵,通常记作0m*n称为矩阵的类型。满足阶梯形矩阵 行简化的阶梯形矩阵即满足如下条件的矩阵: (1)阶梯形; (2)非零首元所在列其余元素均为0 ; (3) 非…...

【个人首测】百度文心一言 VS ChatGPT GPT-4
昨天我写了一篇文章GPT-4牛是牛,但这几天先别急,文中我测试了用GPT-4回答ChatGPT 3.5 和 Notion AI的问题,大家期待的图片输入也没有出现。 昨天下午百度发布了文心一言,对标ChatGPT,录屏无实机演示让百度股价暴跌。但是晚上百度就…...

基于STM32的ADC采样及各式滤波实现(HAL库,含VOFA+教程)
前言:本文为手把手教学ADC采样及各式滤波算法的教程,本教程的MCU采用STM32F103ZET6。以HAL库的ADC采样函数为基础进行教学,通过各式常见滤波的实验结果进行分析对比,搭配VOFA工具直观的展示滤波效果。ADC与滤波算法都是嵌入式较为…...

Redis高级篇
文章目录面试题库redis有哪些用法?redis单线程时代性能依然很快的原因?主线程和IO线程怎么协作完成请求处理的BigKey(重要)什么算是BigKey?怎么发现BigKey?怎么删除bigkey?bigkey生产调优缓存双…...

sess.close()这句话一般是干什么的,在代码中可以不加么?
sess.close()这句话是用于关闭TensorFlow会话对象的方法。 关闭会话对象可以释放资源,避免内存泄漏,以及清除图中的变量和操作。 在代码中是否可以不加这句话,取决于你是如何创建和使用会话对象的。如果你使用了with语句来创建和管理会话对…...

网络舆情监测处置平台,TOOM舆情如何做好舆情风险点及防控措施?
网络舆情监测处置平台是一个综合性的系统,旨在帮助企业、政府或其他组织有效地管理和处置网络舆情。从多个角度来分析该平台,我们可以考虑以下几个方面: 1,技术实现 网络舆情监测处置平台的技术实现是其核心,它通常采…...

百度文心一言对标 ChatGPT,你怎么看?
文心一言 VS ChatGPT接受不完美 期待进步里程碑意义文心一言初体验✔ 文学创作✔ 商业文案创作✔ 数理逻辑推算✔ 中文理解✔ 多模态生成写在最后何为文心?“文”就是我们中华语言文字中的文,“心”是希望该语言模型可以用心的去理解语言,用心…...

阿里笔试2023-3-15
太菜了,记录一下笔试题目,代码有更好解法欢迎分享。 1、满二叉子树的数量。 给定一颗二叉树,试求这课二叉树有多少个节点满足以该节点为根的子树是满二叉树?满二叉树指每一层都达到节点最大值。 第一行输入n表示节点数量ÿ…...

STM32:TIM定时器输出比较(OC)
一、输出比较简介 1、输出比较 OC(Output Comapre)输出比较输出比较可以通过比较CNT(时基单元)和CCR(捕获单元)寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频…...

HTTPS 加密协议
✏️作者:银河罐头 📋系列专栏:JavaEE 🌲“种一棵树最好的时间是十年前,其次是现在” 目录HTTPS"加密" 是什么HTTPS 的工作过程引入证书HTTPS http 安全层 (SSL) SSL 用来加密的协议,也叫 TLS …...

分布式锁和分布式事务
分布式锁 没有图形,只通过大量文字进行说明。分布式锁:redis分布式锁, zk分布式锁, 数据库做分布式锁 redis分布式锁 setnx key value ex 10 原子操作 AB两个线程减库存业务,假设库存是10 A线程获取锁,…...

RK3568平台开发系列讲解(驱动基础篇)I2C协议介绍
🚀返回专栏总目录 文章目录 一、I2C基本读写过程二、通讯的起始和停止信号三、数据有效性四、地址及数据方向五、响应沉淀、分享、成长,让自己和他人都能有所收获!😄 📢I2C的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。 一、…...

HTML 音频(Audio)
HTML 音频(Audio) 声音在HTML中可以以不同的方式播放. 问题以及解决方法 在 HTML 中播放音频并不容易! 您需要谙熟大量技巧,以确保您的音频文件在所有浏览器中(Internet Explorer, Chrome, Firefox, Safari, Opera)和所有硬件上…...

什么是Vue
✅作者简介:CSDN一位小博主,正在学习前端,欢迎大家一起来交流学习🏆 📃个人主页:白月光777的CSDN博客 🔥系列专栏:Vue从入门到进阶 💬个人格言:但行好事&…...