解析 BVH 文件与执行 FK 过程
本文将对 BVH 文件进行讲解,用 Python 代码用递归下降的方式解析。解析完成后,通过前向运动学(Forward Kinematics)方法进行计算,并使用 panda3d
库进行播放。
BVH 文件介绍
BVH 是一种通用的人体特征动画文件格式,基于人体关节(Joint)的树状结构进行存储。
BVH 文件分为 Hierarchy 和 Motion 两部分, Hierarchy部分是描述虚拟角色的树形结构,Motion 部分是记录每一帧虚拟角色运动的姿态。下面是一个标准的 BVH 文件。
HIERARCHY
ROOT RootJoint
{
OFFSET 0.000000 0.000000 0.000000
CHANNELS 6 Xposition Yposition Zposition Xrotation Yrotation Zrotation
JOINT lHip
{
OFFSET 0.100000 -0.051395 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lKnee
{
OFFSET 0.000000 -0.410000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lAnkle
{
OFFSET 0.000000 -0.390000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lToeJoint
{
OFFSET 0.000000 -0.050000 0.130000
CHANNELS 3 Xrotation Yrotation Zrotation
End Site
{
OFFSET 0.010000 0.002000 0.060000
}
}
}
}
}
JOINT pelvis_lowerback
{
OFFSET 0.000000 0.093605 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lowerback_torso
{
OFFSET 0.000000 0.100000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lTorso_Clavicle
{
OFFSET 0.001000 0.157500 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lShoulder
{
OFFSET 0.117647 0.000000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lElbow
{
OFFSET 0.245000 0.000000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT lWrist
{
OFFSET 0.240000 0.000000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
End Site
{
OFFSET 0.116353 -0.002500 0.000000
}
}
}
}
}
JOINT rTorso_Clavicle
{
OFFSET -0.001000 0.157500 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT rShoulder
{
OFFSET -0.117647 0.000000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT rElbow
{
OFFSET -0.245000 0.000000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT rWrist
{
OFFSET -0.240000 0.000000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
End Site
{
OFFSET -0.116353 -0.002500 0.000000
}
}
}
}
}
JOINT torso_head
{
OFFSET 0.000000 0.282350 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
End Site
{
OFFSET 0.000000 0.192650 0.000000
}
}
}
}
JOINT rHip
{
OFFSET -0.100000 -0.051395 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT rKnee
{
OFFSET 0.000000 -0.410000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT rAnkle
{
OFFSET 0.000000 -0.390000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT rToeJoint
{
OFFSET 0.000000 -0.050000 0.130000
CHANNELS 3 Xrotation Yrotation Zrotation
End Site
{
OFFSET -0.010000 0.002000 0.060000
}
}
}
}
}
}
MOTION
Frames: 2
Frame Time: 0.016667
-0.001735 0.855388 0.315499 2.008551 7.606260 -0.798294 11.216058 -3.286777 -1.592436 13.521250 -1.153514 -4.213484 -17.754157 -3.216621 9.232892 -7.948705 0.211932 -1.528529 2.220789 -0.981058 -1.133630 2.071938 -6.311876 2.083844 2.020309 -0.533885 -19.342332 -5.129554 -37.575293 -50.190804 0.198025 -24.741038 4.442069 0.442380 2.547494 4.858004 1.951773 -5.809334 21.100535 23.710456 30.003467 53.240376 0.414981 10.414544 1.952633 3.576914 -9.482057 6.918939 1.457480 -0.035296 0.111891 -27.722826 -1.655032 2.430426 -2.964232 -5.507982 1.444119 2.239212 -3.180259 -0.892285 -0.008100 -0.007000 0.024400
-0.003810 0.853981 0.337002 2.017405 7.825929 -1.809751 11.713970 -2.355625 0.062023 15.198954 -1.861308 -4.389417 -17.189762 -3.614663 9.244711 -9.397213 0.262158 -1.565413 2.647300 -1.021514 0.131973 1.458470 -6.632789 1.868957 1.928817 -0.148344 -19.543616 -3.937845 -37.139413 -49.957499 0.204371 -24.672734 4.317351 0.916151 2.440320 4.849158 2.068848 -5.518149 21.184327 23.795785 30.805519 52.865110 0.417817 10.118764 1.952408 3.104611 -9.774695 7.021717 1.448893 -0.004226 0.069402 -27.594885 -2.728812 3.499348 -1.670271 -5.527619 1.835016 6.676684 -3.330738 -4.015991 -0.008600 -0.007000 0.026400
Hierarchy 部分
Hierarchy 描述了骨骼的树形结构,比如 rKnee
是一个关节(Joint):
JOINT rKnee
{
OFFSET 0.000000 -0.410000 0.000000
CHANNELS 3 Xrotation Yrotation Zrotation
JOINT rAnkle
{
...
}
}
- OFFSET - 当前结点相对于父结点的相对位置;
- CHANNELS - 表示欧拉角的旋转顺序;
- JOINT - 表示子节点,可能有多个。
而 RootJoint
是一个根节点(Root):
ROOT RootJoint
{
OFFSET 0.000000 0.000000 0.000000
CHANNELS 6 Xposition Yposition Zposition Xrotation Yrotation Zrotation
JOINT lHip
{
...
}
}
ROOT 与 JOINT 的不同之处在于 CHANNELS 属性有6个维度,前三维是该骨骼对应的 X, Y, Z 三个轴的顺序。一般来说,根节点的 OFFSET 为 (0, 0, 0) 。
END Site
是骨骼的末端,即骨骼树的叶子结点。
End Site
{
OFFSET -0.010000 0.002000 0.060000
}
显然,只需要 OFFSET 即可表示。
Motion 部分
Motion 部分有以下信息:
-
Frames 表示接下来动画中帧的数量;
-
Frame Time 表示帧率,即每帧持续时间;
-
接下来每一行代表一帧中的运动数据。这些数据, 是按照前面 CHANNEL 定义顺序出现的. 按照上面 BVH 结构的定义, 首先是根关节的平移量:Xposition, Yposition, Zposition, 接下来是根关节的旋转量:Xrotation, Yrotation, Zrotation ,然后是各个关节的旋转量。
Frames: 2
Frame Time: 0.016667
-0.001735 0.855388 0.315499 2.008551 7.606260 -0.798294 11.216058 -3.286777 -1.592436 13.521250 -1.153514 -4.213484 -17.754157 -3.216621 9.232892 -7.948705 0.211932 -1.528529 2.220789 -0.981058 -1.133630 2.071938 -6.311876 2.083844 2.020309 -0.533885 -19.342332 -5.129554 -37.575293 -50.190804 0.198025 -24.741038 4.442069 0.442380 2.547494 4.858004 1.951773 -5.809334 21.100535 23.710456 30.003467 53.240376 0.414981 10.414544 1.952633 3.576914 -9.482057 6.918939 1.457480 -0.035296 0.111891 -27.722826 -1.655032 2.430426 -2.964232 -5.507982 1.444119 2.239212 -3.180259 -0.892285 -0.008100 -0.007000 0.024400
...
总而言之,每个 CHANNEL 按照顺序对应 Motion 中的每个数据。
BVH 文件解析
根据上述说明,对于 Hierarchy 部分,我们建立的骨骼树应该包括以下三种结点:
class root(object):
def __init__(self, parent, name, offset, channel):
self.parent = parent
self.name = name
self.offset = offset
self.channel = channel # 6
self.children = []
class joint(object):
def __init__(self, parent, name, offset, channel):
self.parent = parent
self.name = name
self.offset = offset
self.channel = channel # 3
self.children = []
class end(object):
def __init__(self, parent, name, offset, channel):
self.parent = parent
self.name = name
self.offset = offset
但是实际上用类的方式来储存和访问各个结点情况过于冗余了:因为骨骼树结构很简单,而且自上而下可以给每个关节都赋予一个编号,用数组记录每个关节对应的 name, parent, offset 以及 channel 的情况即可表达所有的骨骼树信息。
为了解析 Hierarchy 部分,我们先定义一个 hierarchy_parser
类并预处理得到 HIERARCHY 部分:
class hierarchy_parser(object):
def __init__(self, bvh_file_path):
self.lines = get_hierarchy_lines(bvh_file_path)
self.line_number = 0
self.root_position_channel = []
self.joint_rotation_channels = []
self.joint_names = []
self.joint_parents = []
self.joint_offsets = []
def get_hierarchy_lines(bvh_file_path):
hierarchy_lines = []
for line in open(bvh_file_path, 'r'):
line = line.strip()
if line.startwith('MOTION'):
break
else:
hierarchy_lines.append(line)
return hierarchy_lines
然后用递归下降的思想,分别编写三种类的解析函数:
class hierarchy_parser(object):
# ...
def parse_offset(self, line):
return [float(x) for x in line.split()[1:]]
def parse_channels(self, line):
return [x for x in line.split()[2:]]
def parse_root(self, parent=-1):
self.joint_parents.append(parent)
self.joint_names.append(self.lines[self.line_number].split()[1])
self.line_number += 2
if self.lines[self.line_number].startswith('OFFSET'):
self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))
else:
print('cannot find root offset')
self.line_number += 1
if self.lines[self.line_number].startswith('CHANNELS'):
channels = self.parse_channels(self.lines[self.line_number])
if self.lines[self.line_number].split()[1] == '3':
self.joint_rotation_channels.append((channels[0], channels[1], channels[2]))
elif self.lines[self.line_number].split()[1] == '6':
self.root_position_channels.append((channels[0], channels[1], channels[2]))
self.joint_rotation_channels.append((channels[3], channels[4], channels[5]))
else:
print('cannot find root channels')
self.line_number += 1
while self.lines[self.line_number].startswith('JOINT'):
self.parse_joint(0)
self.line_number += 1
def parse_joint(self, parent):
self.joint_parents.append(parent)
index = len(self.joint_names)
self.joint_names.append(self.lines[self.line_number].split()[1])
self.line_number += 2
if self.lines[self.line_number].startswith('OFFSET'):
self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))
else:
print('cannot find joint offset')
self.line_number += 1
if self.lines[self.line_number].startswith('CHANNELS'):
channels = self.parse_channels(self.lines[self.line_number])
if self.lines[self.line_number].split()[1] == '3':
self.joint_rotation_channels.append((channels[0], channels[1], channels[2]))
else:
print('cannot find joint channels')
self.line_number += 1
while self.lines[self.line_number].startswith('JOINT') or \
self.lines[self.line_number].startswith('End'):
if self.lines[self.line_number].startswith('JOINT'):
self.parse_joint(index)
elif self.lines[self.line_number].startswith('End'):
self.parse_end(index)
self.line_number += 1
def parse_end(self, parent):
self.joint_parents.append(parent)
self.joint_names.append(self.joint_names[parent] + '_end')
self.line_number += 2
if self.lines[self.line_number].startswith('OFFSET'):
self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))
else:
print('cannot find joint offset')
self.line_number += 2
最后提供一个解析的入口:
class hierarchy_parser(object):
# ...
def analyze(self):
if not self.lines[self.line_number].startswith('HIERARCHY'):
print('cannot find hierarchy')
self.line_number += 1
if self.lines[self.line_number].startswith('ROOT'):
self.parse_root()
return self.joint_names, self.joint_parents, self.joint_offsets
前向运动学(Forward Kinematics)
对于骨骼树,要想确定每个关节在每一帧的位置,应该从 Root 结点开始,向下遍历计算每个关节的旋转,从而得到每个关节的位置。
因此,我们可以通过对树进行解析的方式得到每一帧下每个关节的全局旋转和全局坐标。由于我的 BVH 文件所有 CHANNEL 的顺序都是 (X, Y, Z) ,因此并未处理其它情况,如果有特殊情况需要注意。
import numpy as np
from scipy.spatial.transform import Rotation as R
def forward_kinematics(joint_name, joint_parent, joint_offset, motion_data, frame_id):
m = len(joint_name)
joint_positions = np.zeros((m, 3), dtype=np.float64)
joint_orientations = np.zeros((m, 4), dtype=np.float64)
channels = motion_data[frame_id]
rotations = np.zeros((m, 3), dtype=np.float64)
cnt = 1
for i in range(m):
if '_end' not in joint_name[i]:
for j in range(3):
rotations[i][j] = channels[cnt * 3 + j]
cnt += 1
for i in range(m):
parent = joint_parent[i]
if parent == -1:
for j in range(3):
joint_positions[0][j] = channels[j]
joint_orientations[0] = R.from_euler('XYZ', [rotations[0][0], \
rotations[0][1], rotations[0][2]], degrees=True).as_quat()
else:
if '_end' in joint_name[i]:
joint_orientations[i] = np.array([0, 0, 0, 1])
joint_positions[i] = joint_positions[parent] + \
R.from_quat(joint_orientations[parent]).as_matrix() @ joint_offset[i]
else:
rotation = R.from_euler('XYZ', [rotations[i][0], \
rotations[i][1], rotations[i][2]], degrees=True)
joint_orientations[i] = (R.from_quat(joint_orientations[parent]) * rotation).as_quat()
joint_positions[i] = joint_positions[parent] + \
R.from_quat(joint_orientations[parent]).as_matrix() @ joint_offset[i]
return joint_positions, joint_orientations
计算中涉及一些四元数(quaternion)的相关知识,可以通过 四元数和旋转 稍作了解。
动画播放
这部分内容参考了 GAMES105 课程。首先安装依赖库,pip install panda3d
。
然后直接将仓库中的 viewer.py
、GroundScene.egg
、character_model.py
和 walk60.bvh
和文件放在同一文件夹下,调用运行即可。
Hibiki33/BVHPlayer
完整的代码如下:
from viewer import SimpleViewer
import numpy as np
from scipy.spatial.transform import Rotation as R
class HierarchyParser(object):
def __init__(self, bvh_file_path):
self.lines = self.get_hierarchy_lines(bvh_file_path)
self.line_number = 0
self.root_position_channels = []
self.joint_rotation_channels = []
self.joint_names = []
self.joint_parents = []
self.joint_offsets = []
def get_hierarchy_lines(self, bvh_file_path):
hierarchy_lines = []
for line in open(bvh_file_path, 'r'):
line = line.strip()
if line.startswith('MOTION'):
break
else:
hierarchy_lines.append(line)
return hierarchy_lines
def parse_offset(self, line):
return [float(x) for x in line.split()[1:]]
def parse_channels(self, line):
return [x for x in line.split()[2:]]
def parse_root(self, parent=-1):
self.joint_parents.append(parent)
self.joint_names.append(self.lines[self.line_number].split()[1])
self.line_number += 2
if self.lines[self.line_number].startswith('OFFSET'):
self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))
else:
print('cannot find root offset')
self.line_number += 1
if self.lines[self.line_number].startswith('CHANNELS'):
channels = self.parse_channels(self.lines[self.line_number])
if self.lines[self.line_number].split()[1] == '3':
self.joint_rotation_channels.append((channels[0], channels[1], channels[2]))
elif self.lines[self.line_number].split()[1] == '6':
self.root_position_channels.append((channels[0], channels[1], channels[2]))
self.joint_rotation_channels.append((channels[3], channels[4], channels[5]))
else:
print('cannot find root channels')
self.line_number += 1
while self.lines[self.line_number].startswith('JOINT'):
self.parse_joint(0)
self.line_number += 1
def parse_joint(self, parent
推荐阅读
-
解析 BVH 文件与执行 FK 过程
-
解析BVH文件格式:深入剖析BVH文件的结构与内容
-
C代码 从源代码到可执行文件——编译全过程解析
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
ssh工作流程及原理-SSH(Secure Shell Protocol,安全的壳程序协议),它可以通过数据包加密技术将等待传输的数据包加密后再传输到网络上。ssh协议本身提供两个服务器功能:一个是类似telnet的远程连接使用shell的服务器;另一个就是类似ftp服务的sftp-server,提供更安全的ftp服务。 连接加密技术简介 目前常见的网络数据包加密技术通常是通过“非对称密钥系统”来处理的。主要通过两把不一样的公钥与私钥来进行加密与解密的过程。 公钥(public key):提供给远程主机进行数据加密的行为,所有人都可获得你的公钥来将数据加密。 私钥(private key):远程主机使用你的公钥加密的数据,在本地端就能够使用私钥来进行解密。私钥只有自己拥有。 SSH工作过程:在整个通讯过程中,为实现SSH的安全连接,服务端与客户端要经历如下五个阶段: 版本号协商阶段 SSH目前包括SSH1和SSH2两个版本,双方通过版本协商确定使用的版本 密钥和算法协商阶段 SSH支持多种加密算法,双方根据本端和对端支持的算法,协商出最终使用的算法 认证阶段 SSH客户端向服务器端发起认证请求,服务器端对客户端进行认证 会话请求阶段 认证通过后,客户端向服务器端发送会话请求 交互会话阶段 会话请求通过后,服务器端和客户端进行信息的交互 一、版本协商阶段 服务器端打开端口22,等待客户端连接; 客户端向服务器端发起TCP初始连接请求,TCP连接建立后,服务器向客户端发送第一个报文,包括版本标志字符串,格式为“SSH-<主协议版本号>.<次协议版本号>.<软件版本号>”,协议版本号由主版本号和次版本号组成,软件版本号主要是为调试使用。 客户端收到报文后,解析该数据包,如果服务器的协议版本号比自己的低,且客户端能支持服务器端的低版本,就使用服务器端的低版本协议号,否则使用自己的协议版本号。 客户端回应服务器一个报文,包含了客户端决定使用的协议版本号。服务器比较客户端发来的版本号,决定是否能同客户端一起工作。如果协商成功,则进入密钥和算法协商阶段,否则服务器断开TCP连接。 说明:上述报文都是采用明文方式传输。 二、密钥和算法协商阶段 服务器端和客户端分别发送算法协商报文给对端,报文中包含自己支持的公钥算法列表、加密算法列表、MAC(Message Authentication Code,消息验证码)算法列表、压缩算法列表等等。 服务器端和客户端根据对端和本端支持的算法列表得出最终使用的算法。 服务器端和客户端利用DH交换(Diffie-Hellman Exchange)算法、主机密钥对等参数,生成会话密钥和会话ID。 由此,服务器端和客户端就取得了相同的会话密钥和会话ID。对于后续传输的数据,两端都会使用会话密钥进行加密和解密,保证了数据传送的安全。在认证阶段,两端会使用会话用于认证过程。 会话密钥的生成: 客户端需要使用适当的客户端程序来请求连接服务器,服务器将服务器的公钥发送给客户端。(服务器的公钥产生过程:服务器每次启动sshd服务时,该服务会主动去找/etc/ssh/ssh_host*文件,若系统刚装完,由于没有这些公钥文件,因此sshd会主动去计算出这些需要的公钥文件,同时也会计算出服务器自己所需要的私钥文件。) 服务器生成会话ID,并将会话ID发给客户端。 若客户端第一次连接到此服务器,则会将服务器的公钥数据记录到客户端的用户主目录内的~/.ssh/known_hosts。若是已经记录过该服务器的公钥数据,则客户端会去比对此次接收到的与之前的记录是否有差异。客户端生成会话密钥,并用服务器的公钥加密后,发送给服务器。 ****服务器用自己的私钥将收到的数据解密,获得会话密钥。 服务器和客户端都知道了会话密钥,以后的传输都将被会话密钥加密。 三、认证阶段 SSH提供两种认证方法: 基于口令的认证(password认证):客户端向服务器发出password认证请求,将用户名和密码加密后发送给服务器,服务器将该信息解密后得到用户名和密码的明文,与设备上保存的用户名和密码进行比较,并返回认证成功或失败消息。 基于密钥的认证(publickey认证):客户端产生一对公共密钥,将公钥保存到将要登录的服务器上的那个账号的家目录的.ssh/authorized_keys文件中。认证阶段:客户端首先将公钥传给服务器端。服务器端收到公钥后会与本地该账号家目录下的authorized_keys中的公钥进行对比,如果不相同,则认证失败;否则服务端生成一段随机字符串,并先后用客户端公钥和会话密钥对其加密,发送给客户端。客户端收到后将解密后的随机字符串用会话密钥发送给服务器。如果发回的字符串与服务器端之前生成的一样,则认证通过,否则,认证失败。 注:服务器端对客户端进行认证,如果认证失败,则向客户端发送认证失败消息,其中包含可以再次认证的方法列表。客户端从认证方法列表中选取一种认证方法再次进行认证,该过程反复进行。直到认证成功或者认证次数达到上限,服务器关闭连接为止。实例
-
Android 11 WiFi开启流程-STA_PRIMARY,如果是打开其他WiFi,则参数2为传入的staId。 frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java public synchronized boolean setWifiEnabled(String packageName, boolean enable) { return setWifiEnabled2(packageName, STA_PRIMARY, enable); } public synchronized boolean setWifiEnabled2(String packageName, int staId,boolean enable) { if (enforceChangePermission(packageName) != MODE_ALLOWED) { return false; } boolean isPrivileged = isPrivileged(Binder.getCallingPid, Binder.getCallingUid); if (!isPrivileged && !isDeviceOrProfileOwner(Binder.getCallingUid, packageName) && !mWifiPermissionsUtil.isTargetSdkLessThan(packageName, Build.VERSION_CODES.Q, Binder.getCallingUid) && !isSystem(packageName, Binder.getCallingUid)) { mLog.info("setWifiEnabled not allowed for uid=%") .c(Binder.getCallingUid).flush; return false; } // If Airplane mode is enabled, only privileged apps are allowed to toggle Wifi if (mSettingsStore.isAirplaneModeOn && !isPrivileged) { mLog.err("setWifiEnabled in Airplane mode: only Settings can toggle wifi").flush; return false; } // If SoftAp is enabled, only privileged apps are allowed to toggle wifi if (!isPrivileged && mTetheredSoftApTracker.getState == WIFI_AP_STATE_ENABLED) { mLog.err("setWifiEnabled with SoftAp enabled: only Settings can toggle wifi").flush; return false; } mLog.info("setWifiEnabled package=% uid=% enable=%").c(packageName) .c(Binder.getCallingUid).c(enable).flush; long ident = Binder.clearCallingIdentity; try { if (staId == STA_PRIMARY && !mSettingsStore.handleWifiToggled(enable)) { // Nothing to do if wifi cannot be toggled return true; } } finally { Binder.restoreCallingIdentity(ident); } if (mWifiPermissionsUtil.checkNetworkSettingsPermission(Binder.getCallingUid)) { mWifiMetrics.logUserActionEvent(enable ? UserActionEvent.EVENT_TOGGLE_WIFI_ON : UserActionEvent.EVENT_TOGGLE_WIFI_OFF); } if (!mIsControllerStarted) { Log.e(TAG,"WifiController is not yet started, abort setWifiEnabled"); return false; } mWifiMetrics.incrementNumWifiToggles(isPrivileged, enable); if(staId == STA_PRIMARY) mActiveModeWarden.wifiToggled; else if(staId == STA_SECONDARY && (getNumConcurrentStaSupported > 1) && (getWifiEnabledState == WifiManager.WIFI_STATE_ENABLED)) mActiveModeWarden.qtiWifiToggled(staId, enable); else Log.e(TAG,"setWifiEnabled not allowed for Id: " + staId); return true; } 四、可以看到wifiservice调用了ActiveModeWarden的wifiToggled,发送了CMD_WIFI_TOGGLED的消息,通知WiFi切换了。 frameworks/opt/net/wifi/service/java/com/android/server/wifi/ActiveModeWarden.java public void wifiToggled { mWifiController.sendMessage(WifiController.CMD_WIFI_TOGGLED); } 五、我们看WifiController是怎么处理这个消息的。WifiController是ActiveModeWarden中的一个状态机,用来管理WiFi的操作,包括热点啊飞行模式什么的。 打开WiFi之前,状态机应该是在Disabled状态,我们看Disable状态里的处理。 class DisabledState extends BaseState { public boolean processMessageFiltered(Message msg) { switch (msg.what) { case CMD_WIFI_TOGGLED: case CMD_SCAN_ALWAYS_MODE_CHANGED: if (shouldEnableSta) { startClientModeManager; transitionTo(mEnabledState); } break; 启动一个新的客户端管理。 private boolean startClientModeManager { Log.d(TAG, "Starting ClientModeManager"); ClientListener listener = new ClientListener; ClientModeManager manager = mWifiInjector.makeClientModeManager(listener); listener.setActiveModeManager(manager); manager.start; if (!switchClientModeManagerRole(manager)) { return false; } mActiveModeManagers.add(manager); return true; } 六、start了ClientModeManager frameworks/opt/net/wifi/service/java/com/android/server/wifi/ClientModeManager.java public void start { Log.d(TAG, "Starting with role ROLE_CLIENT_SCAN_ONLY"); mRole = ROLE_CLIENT_SCAN_ONLY; mTargetRole = ROLE_CLIENT_SCAN_ONLY; mStateMachine.sendMessage(ClientModeStateMachine.CMD_START); } 看一下是谁处理了这个START消息呢 private class IdleState extends State { @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_START: // Always start in scan mode first. mClientInterfaceName = mWifiNative.setupInterfaceForClientInScanMode( mWifiNativeInterfaceCallback); if (TextUtils.isEmpty(mClientInterfaceName)) { Log.e(TAG, "Failed to create ClientInterface. Sit in Idle"); mModeListener.onStartFailure; break; } transitionTo(mScanOnlyModeState); break; } } 七、这里可以看出,WifiNative先去启动HAL frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiNative.java public String setupInterfaceForClientInScanMode( @NonNull InterfaceCallback interfaceCallback) { synchronized (mLock) { if (!startHal) { mWifiMetrics.incrementNumSetupClientInterfaceFailureDueToHal; return null; } Iface iface = mIfaceMgr.allocateIface(Iface.IFACE_TYPE_STA_FOR_SCAN); iface.externalListener = interfaceCallback; iface.name = createStaIface(iface); if (!mWifiCondManager.setupInterfaceForClientMode(iface.name, Runnable::run, new NormalScanEventCallback(iface.name), new PnoScanEventCallback(iface.name))) { Log.e(TAG, "Failed to setup iface in wificond=" + iface.name); teardownInterface(iface.name); mWifiMetrics.incrementNumSetupClientInterfaceFailureDueToWificond; return null; } iface.networkObserver = new NetworkObserverInternal(iface.id); if (!registerNetworkObserver(iface.networkObserver)) { teardownInterface(iface.name); return null; } mWifiMonitor.startMonitoring(iface.name); onInterfaceStateChanged(iface, isInterfaceUp(iface.name)); iface.featureSet = getSupportedFeatureSetInternal(iface.name); return iface.name; } } 八、启动HAL WifiVendorHal.java-->startVendorHal --> HalDeviceManager.java --> startWifi --> IWifi.start mWifi.start方法是启动实际加载WiFi动作的调用,这里涉及HIDL机制调用。通过获取IWifi接口对象,调用其方法。这里IWifi接口对象是IWifi.hal文件中实现。 android/hardware/interfaces/wifi/1.0/IWifi.hal 在编译时,编译器会将IWifi.hal解析为IWifi.java文件,直接看该文件中的start方法实现即可。 android/out/soong//.intermediates/hardware/interfaces/wifi/1.0/android.hardware.wifi-V1.0-java_gen_java/gen/srcs/android/hardware/wifi/V1_0/IWifi.java public android.hardware.wifi.V1_0.WifiStatus start throws android.os.RemoteException { try { ... ... ... ... mRemote.transact(3 /* start */, _hidl_request, _hidl_reply, 0 /* flags */); _hidl_reply.verifySuccess; _hidl_request.releaseTemporaryStorage; return _hidl_out_status; } finally { _hidl_reply.release; } } 通过binder调用,将调用到wifi.cpp中的start方法. android/hardware/interfaces/wifi/1.4/default/wifi.cpp Return<void> Wifi::start(start_cb hidl_status_cb) { return validateAndCall(this, WifiStatusCode::ERROR_UNKNOWN, &Wifi::startInternal, hidl_status_cb); } wifi.cpp->start ==> wifi.cpp->startInternal ==> wifi.cpp->initializeModeControllerAndLegacyHal ==> WifiModeController->initialize ==> DriverTool->LoadDriver 通过调用DriverTool->LoadDriver将返回到Android framework中。下面是LoadDriver的实现。 android/frameworks/opt/net/wifi/libwifi_hal/include/wifi_hal/driver_tool.cpp bool DriverTool::LoadDriver { return ::wifi_load_driver == 0; } 在wifi_load_driver方法中,将调用系统接口加载WiFi驱动ko。关于系统insmod接口的调用,本文不做分析。到这里,已梳理完在WifiNative类中调用的startHal方法。 android/frameworks/opt/net/wifi/libwifi_hal/wifi_hal_common.cpp int wifi_load_driver { ... ... ... ... insmod(file,args); ... ... ... ... } 调用WifiNl80211Manager类的setupInterfaceForClientMode方法。 该类的主要对WiFi 80211nl管理接口的封装,接口在WiFicond守护进程中呈现给WiFi框架。该类提供的接口仅使用与WiFi框架,访问权限受selinux权限保护。 setupInterfaceForClientMode方法主要为Station模式设置接口。 android/frameworks/base/wifi/java/android/net/wifi/nl80211/WifiNl80211Manager.java public boolean setupInterfaceForClientMode(@NonNull String ifaceName, @NonNull @CallbackExecutor Executor executor, @NonNull ScanEventCallback scanCallback, @NonNull ScanEventCallback pnoScanCallback) { ... ... ... ... // Refresh Handlers mClientInterfaces.put(ifaceName, clientInterface); try { IWifiScannerImpl wificondScanner = clientInterface.getWifiScannerImpl; mWificondScanners.put(ifaceName, wificondScanner); Binder.allowBlocking(wificondScanner.asBinder); ScanEventHandler scanEventHandler = new ScanEventHandler(executor, scanCallback); mScanEventHandlers.put(ifaceName, scanEventHandler); wificondScanner.subscribeScanEvents(scanEventHandler); PnoScanEventHandler pnoScanEventHandler = new PnoScanEventHandler(executor, pnoScanCallback); mPnoScanEventHandlers.put(ifaceName, pnoScanEventHandler); wificondScanner.subscribePnoScanEvents(pnoScanEventHandler); ... ... ... ... } 到这里,ClientModeStateMachine状态机在IdleState状态成功处理完了CMD_START消息。状态机将转到“mScanOnlyModeState”状态,将会执行以下调用流程(具体原因可查看状态机机制)。 IdleState.exit->StartedState.enter->StartedState.exit->ScanOnlyModeState.enter。 九、启动HAL以后,就要启动supplicant了。 在第五步的时候我们调用了ActiveModeWarden.java的startClientModeManagerh函数。start以后会执行switchClientModeManagerRole
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)
-
入门讲解:Spring源代码探索系列" 1. Spring源码剖析导论 2. Spring容器基础操作实战 3. Spring核心容器组件详解 4. 详解Spring基础:XmlBeanFactory源码解析 5. 掌握关键技术:如何获取并处理Document 6. 精读细节:BeanDefinitions的解析与注册过程 7. 深度解析:bean标签在源码中的执行与注册路径
-
树莓派打造自家私人云盘实战教程(第二部分):Docker+NextCloud+Nginx 教你一步步建站并上手" - 利用 Docker 架设简易高效的环境部署指南 - Docker 在树莓派上的安装入门,一看就会 - 参考链接:树莓派Docker安装指南 - 探索免费私有云盘方案:NextCloud 与 OwnCloud 同源平台解析 无需文件加密功能的话,NextCloud 是我们的首选。它与 SeaFile 相比,更加符合需求。 对于照片和视频备份同步,Daemon Sync 显示出了出色的表现,安装过程简单易懂,不妨一试身手。 现在,让我们一起步入正文,动手实践搭建属于自己的私人云盘吧!