普通视图
Flutter封装的路由工具类RouteUtils,可直接二次开发
Objective-C之Class底层结构探索
SwiftUI从入门到精髓
2023年度总结:我们都在用力的活着,拼尽了全力,却换回了伤痕累累!!!
MySql数据库开发中常见问题解决方法
iOSload和initialize详解
聊聊iOS自动释放池AutoreleasePool
深入了解swift函数派发机制
iOS APP启动全流程
真心送你一份iOS核心动画总结篇,赶紧上车,学完它才觉得物有所值,还有实战案例!!!
本文案列代码在篇尾,有小伙伴需要代码可以移至篇尾获取。
在iOS开发中,动画是提高用户体验重要的环节之一。一个设计严谨、精细的动画效果能给用户耳目一新的效果,这对于app而言是非常重要的。
简介
iOS动画主要是指Core Animation框架。官方使用文档地址为:Core Animation Guide。Core Animation是iOS和macOS平台上负责图形渲染与动画的基础框架。Core Animation可以作用与动画视图或者其他可视元素,为你完成了动画所需的大部分绘帧工作。你只需要配置少量的动画参数(如开始点的位置和结束点的位置)即可使用Core Animation的动画效果。Core Animation将大部分实际的绘图任务交给了图形硬件来处理,图形硬件会加速图形渲染的速度。这种自动化的图形加速技术让动画拥有更高的帧率并且显示效果更加平滑,不会加重CPU的负担而影响程序的运行速度。
Core Animation
Core Animation是一组非常强大的动画处理API,它的子类主要有4个:CABasicAnimation、CAKeyframeAnimation、CATransition、CAAnimationGroup。 Core Animation类的继承关系图:
属性
duration:动画的持续时间 beginTime:动画的开始时间 repeatCount:动画的重复次数 autoreverses:动画按照原动画返回执行 timingFunction:控制动画的显示节奏系统提供五种值选择,分别是:
- kCAMediaTimingFunctionLinear 线性动画
- kCAMediaTimingFunctionEaseIn 先快后慢
- kCAMediaTimingFunctionEaseOut 先慢后快
- kCAMediaTimingFunctionEaseInEaseOut 先慢后快再慢
- kCAMediaTimingFunctionDefault 默认,也属于中间比较快
delegate:动画代理。能够检测动画的执行和结束。 path:帧动画中的执行路径 type:过渡动画的动画类型。主要有以下4中类型:
- kCATransitionFade 渐变效果
- kCATransitionMoveIn 进入覆盖效果
- kCATransitionPush 推出效果
- kCATransitionReveal 离开效果
subtype:过渡动画的动画方向。
- kCATransitionFromRight 从右侧进入
- kCATransitionFromLeft 从左侧进入
- kCATransitionFromTop 从顶部进入
- kCATransitionFromBottom 从底部进入
动画的使用
动画使用步骤:
- 初始化一个动画对象(CAAnimation)并设置一些动画相关属性.
- 添加动画对象到层(CALayer)中,开始执行动画.
CALayer中很多属性都可以通过CAAnimation实现动画效果, 包括opacity, position, transform, bounds, contents等,具体可以在API文档中查找
通过调用CALayer的addAnimation:forKey:增加动画到层(CALayer)中,这样就能触发动画了.通过调用removeAnimationForKey:可以停止层中的动画.
UIView
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
[UIView animateWithDuration:1.0f animations:^{
_demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);
} completion:^(BOOL finished) {
_demoView.frame = CGRectMake(SCREEN_WIDTH/2-25, SCREEN_HEIGHT/2-50, 50, 50);
}];
UIView [begin commit]
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0f];
_demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);
[UIView commitAnimations];
Core Animation
CABasicAnimation *anima = [CABasicAnimation animationWithKeyPath:@"position"];
anima.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, SCREEN_HEIGHT/2-75)];
anima.toValue = [NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-75)];
anima.duration = 1.0f;
[_demoView.layer addAnimation:anima forKey:@"positionAnimation"];
动画详解
CABaseAnimation
基础动画主要提供了对于CALayer对象中的可变属性进行简单动画的操作。比如:位移、透明度、缩放、旋转、背景色等等。 主要提供如下属性: fromValue:keyPath对应的初始值 toValue:keyPath对应的结束值 示例:
1.呼吸动画
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:@"opacity"];
animation.fromValue = [NSNumber numberWithFloat:1.0f];
animation.toValue = [NSNumber numberWithFloat:0.0f];
animation.autoreverses = YES; //回退动画(动画可逆,即循环)
animation.duration = 1.0f;
animation.repeatCount = MAXFLOAT;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;//removedOnCompletion,fillMode配合使用保持动画完成效果
animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[self.alphaTagButton.layer addAnimation:animation forKey:@"aAlpha"];
2.摇摆动画 //设置旋转原点
self.sharkTagButton.layer.anchorPoint = CGPointMake(0.5, 0);
CABasicAnimation* rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; //角度转弧度(这里用1,-1简单处理一下)
rotationAnimation.toValue = [NSNumber numberWithFloat:1];
rotationAnimation.fromValue = [NSNumber numberWithFloat:-1];
rotationAnimation.duration = 1.0f;
rotationAnimation.repeatCount = MAXFLOAT;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.autoreverses = YES;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
rotationAnimation.fillMode = kCAFillModeForwards;
[self.sharkTagButton.layer addAnimation:rotationAnimation forKey:@"revItUpAnimation"];
注意: 如果fillMode=kCAFillModeForwards和removedOnComletion=NO,那么在动画执行完毕后,图层会保持显示动画执行后的状态。但在实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变。这就相当于Android早期的View动画。
CAKeyframeAnimation
CAKeyframeAnimation和CABaseAnimation都属于CAPropertyAnimatin的子类。CABaseAnimation只能从一个数值(fromValue)变换成另一个数值(toValue),而CAKeyframeAnimation则会使用一个NSArray保存一组关键帧。
主要属性: values:就是上述的NSArray对象。里面的元素称为”关键帧”(keyframe)。动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧 path:可以设置一个CGPathRef\CGMutablePathRef,让层跟着路径移动。path只对CALayer的anchorPoint和position起作用。如果你设置了path,那么values将被忽略。 keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0,keyTimes中的每一个时间值都对应values中的每一帧.当keyTimes没有设置的时候,各个关键帧的时间是平分的。 示例:
values属性应用
-(void)setUpCAKeyframeAnimationUseValues {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position";
NSValue *value1 = [NSValue valueWithCGPoint:CGPointMake(50, 50)];
NSValue *value2 = [NSValue valueWithCGPoint:CGPointMake(kWindowWidth - 50, 50)];
NSValue *value3 = [NSValue valueWithCGPoint:CGPointMake(kWindowWidth - 50, kWindowHeight-50)];
NSValue *value4 = [NSValue valueWithCGPoint:CGPointMake(50, kWindowHeight-50)];
NSValue *value5 = [NSValue valueWithCGPoint:CGPointMake(50, 50)];
animation.values = @[value1,value2,value3,value4,value5];
animation.repeatCount = MAXFLOAT; animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.duration = 6.0f; animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.keyButton.layer addAnimation:animation forKey:@"values"];
}
path方式应用
-(void)setUpCAKeyframeAnimationUsePath {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position";
CGMutablePathRef path = CGPathCreateMutable();
//矩形线路
CGPathAddRect(path, NULL, CGRectMake(50,50, kWindowWidth - 100,kWindowHeight - 100));
animation.path=path; CGPathRelease(path);
animation.repeatCount = MAXFLOAT;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.duration = 10.0f;
animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.keyButton.layer addAnimation:animation forKey:@"path"];
}
keyTimes属性使用
-(void)setUpCAKeyframeAnimationUsekeyTimes {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position.x";
animation.values = @[@0, @20, @-20, @20, @0];
animation.keyTimes = @[ @0, @(1 / 6.0), @(3 / 6.0), @(5 / 6.0), @1 ];
animation.duration = 0.5;
animation.additive = YES;
[self.sharkTagButton.layer addAnimation:animation forKey:@"keyTimes"];
}
CAAnimationGroup
CAAnimationGroup(组动画)是CAAnimation的子类,可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行。有点类似于Android的帧动画,不过这里的组动画是将一些基础的动画拼接而成的,比如同时缩小、旋转、渐变。
主要属性有: animations:用来保存一组动画对象的NSArray。 示例:
CABasicAnimation * animationScale = [CABasicAnimation animation];
animationScale.keyPath = @"transform.scale";
animationScale.toValue = @(0.1);
CABasicAnimation *animationRota = [CABasicAnimation animation]; animationRota.keyPath = @"transform.rotation";
animationRota.toValue = @(M_PI_2);
CAAnimationGroup * group = [[CAAnimationGroup alloc] init];
group.duration = 3.0;
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
group.repeatCount = MAXFLOAT;
group.animations = @[animationScale,animationRota];
[self.groupButton.layer addAnimation:group forKey:nil];
CATransition
CAAnimation的子类,用于做过渡动画或者转场动画,能够为层提供移出屏幕和移入屏幕的动画效果。 重要属性有: type:动画过渡类型,官方提供了如下类型:
- kCATransitionFade 渐变效果
- kCATransitionMoveIn 进入覆盖效果
- kCATransitionPush 推出效果
- kCATransitionReveal 揭露离开效果
subtype:动画过渡方向。
- kCATransitionFromRight 从右侧进入
- kCATransitionFromLeft 从左侧进入
- kCATransitionFromTop 从顶部进入
- kCATransitionFromBottom 从底部进入
- startProgress:动画起点(在整体动画的百分比)
- endProgress:动画终点(在整体动画的百分比)
示例:
MyViewController *myVC = [[MyViewController alloc]init];
CATransition *animation = [CATransition animation];
animation.timingFunction = UIViewAnimationCurveEaseInOut;
animation.type = @"cube"; animation.duration =0.5f;
animation.subtype =kCATransitionFromRight; //控制器间跳转动画
[[UIApplication sharedApplication].keyWindow.layer addAnimation:animation forKey:nil];
[self presentViewController:myVC animated:NO completion:nil];
透过springboot源码学习完静态资源加载原理,您也能立马实现出来!!!
带你从新手快速搭建springboot项目
一、创建项目
1.1、创建项目
1.2、配置编码
1.3、取消无用提示
1.4、取消无用参数提示
二、添加POM父依赖
<!-- 两种方式添加父依赖或者import方式 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
</parent>
maven的pom文件手动更新
添加完成maven的pom文件之后,会自动更新,也可能不会自动更新,那么我们需要手动更新它。
三、支持SpringMVC
<dependencies>
<!-- 支持SpringMVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
四、创建启动类、rest接口
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class, args);
}
}
五、配置插件
配置完成后,maven打包可以生成可执行jar文件
<build>
<plugins>
<!-- 打包成可执行jar -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 配置跳过测试 -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<!-- 配置jdk版本11 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
六、添加thymeleaf模板
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
七、添加配置
在resources文件夹下,创建application.properties
在resources文件夹下,创建templates文件夹
# 应用名称
spring.application.name=thymeleaf
# 应用服务 WEB 访问端口
server.port=8080
# THYMELEAF (ThymeleafAutoConfiguration)
# 开启模板缓存(默认值: true )
spring.thymeleaf.cache=false
# 检查模板是否存在,然后再呈现
spring.thymeleaf.check-template=true
# 检查模板位置是否正确(默认值 :true )
spring.thymeleaf.check-template-location=true
#Content-Type 的值(默认值: text/html )
spring.thymeleaf.content-type=text/html
# 开启 MVC Thymeleaf 视图解析(默认值: true )
spring.thymeleaf.enabled=true
# 模板编码
spring.thymeleaf.encoding=UTF-8
# 要被排除在解析之外的视图名称列表,⽤逗号分隔
spring.thymeleaf.excluded-view-names=
# 要运⽤于模板之上的模板模式。另⻅ StandardTemplate-ModeHandlers( 默认值: HTML5)
spring.thymeleaf.mode=HTML5
# 在构建 URL 时添加到视图名称前的前缀(默认值: classpath:/templates/ )
spring.thymeleaf.prefix=classpath:/templates/
# 在构建 URL 时添加到视图名称后的后缀(默认值: .html )
spring.thymeleaf.suffix=.html
八、添加controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class IndexController {
@RequestMapping("/index")
public ModelAndView index(){
ModelAndView mv = new ModelAndView();
mv.setViewName("index");
return mv;
}
}
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
public class HelloController {
@GetMapping("/Hello")
public String Hello(){
return "haha";
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* rest测试controller
*/
@RestController
public class RestIndexController {
@GetMapping("/restIndex")
public String index(){
return "rest";
}
}
九、添加html
在templates下创建index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
index
</body>
</html>
十、访问
需要maven执行编译,否则容易404
基于Servlet+JSP自实现的MVC模式,学完它你才真正上掌握Servlet与JSP原理
通过结合Servlet和JSP的MVC模式,我们可以发挥二者各自的优点:
- Servlet实现业务逻辑;
- JSP实现展示逻辑。
但是,直接把MVC搭在Servlet和JSP之上还是不太好,原因如下:
- Servlet提供的接口仍然偏底层,需要实现Servlet调用相关接口;
- JSP对页面开发不友好,更好的替代品是模板引擎;
- 业务逻辑最好由纯粹的Java类实现,而不是强迫继承自Servlet。
能不能通过普通的Java类实现MVC的Controller?类似下面的代码:
public class UserController {
@GetMapping("/signin")
public ModelAndView signin() {
...
}
@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
...
}
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}
}
上面的这个Java类每个方法都对应一个GET或POST请求,方法返回值是ModelAndView
,它包含一个View的路径以及一个Model,这样,再由MVC框架处理后返回给浏览器。
如果是GET请求,我们希望MVC框架能直接把URL参数按方法参数对应起来然后传入:
@GetMapping("/hello")
public ModelAndView hello(String name) {
...
}
如果是POST请求,我们希望MVC框架能直接把Post参数变成一个JavaBean后通过方法参数传入:
@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
...
}
为了增加灵活性,如果Controller的方法在处理请求时需要访问HttpServletRequest
、HttpServletResponse
、HttpSession
这些实例时,只要方法参数有定义,就可以自动传入:
@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
...
}
以上就是我们在设计MVC框架时,上层代码所需要的一切信息。
设计MVC框架
如何设计一个MVC框架?在上文中,我们已经定义了上层代码编写Controller的一切接口信息,并且并不要求实现特定接口,只需返回ModelAndView
对象,该对象包含一个View
和一个Model
。实际上View
就是模板的路径,而Model
可以用一个Map<String, Object>
表示,因此,ModelAndView
定义非常简单:
public class ModelAndView {
Map<String, Object> model;
String view;
}
比较复杂的是我们需要在MVC框架中创建一个接收所有请求的Servlet
,通常我们把它命名为DispatcherServlet
,它总是映射到/
,然后,根据不同的Controller的方法定义的@Get
或@Post
的Path决定调用哪个方法,最后,获得方法返回的ModelAndView
后,渲染模板,写入HttpServletResponse
,即完成了整个MVC的处理。
这个MVC的架构如下:
其中,DispatcherServlet
以及如何渲染均由MVC框架实现,在MVC框架之上只需要编写每一个Controller。
我们来看看如何编写最复杂的DispatcherServlet
。首先,我们需要存储请求路径到某个具体方法的映射:
@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {
private Map<String, GetDispatcher> getMappings = new HashMap<>();
private Map<String, PostDispatcher> postMappings = new HashMap<>();
}
处理一个GET请求是通过GetDispatcher
对象完成的,它需要如下信息:
class GetDispatcher {
Object instance; // Controller实例
Method method; // Controller方法
String[] parameterNames; // 方法参数名称
Class<?>[] parameterClasses; // 方法参数类型
}
有了以上信息,就可以定义invoke()
来处理真正的请求:
class GetDispatcher {
...
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
String parameterName = parameterNames[i];
Class<?> parameterClass = parameterClasses[i];
if (parameterClass == HttpServletRequest.class) {
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) {
arguments[i] = response;
} else if (parameterClass == HttpSession.class) {
arguments[i] = request.getSession();
} else if (parameterClass == int.class) {
arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == long.class) {
arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
} else if (parameterClass == boolean.class) {
arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
} else if (parameterClass == String.class) {
arguments[i] = getOrDefault(request, parameterName, "");
} else {
throw new RuntimeException("Missing handler for type: " + parameterClass);
}
}
return (ModelAndView) this.method.invoke(this.instance, arguments);
}
private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
String s = request.getParameter(name);
return s == null ? defaultValue : s;
}
}
上述代码比较繁琐,但逻辑非常简单,即通过构造某个方法需要的所有参数列表,使用反射调用该方法后返回结果。
类似的,PostDispatcher
需要如下信息:
class PostDispatcher {
Object instance; // Controller实例
Method method; // Controller方法
Class<?>[] parameterClasses; // 方法参数类型
ObjectMapper objectMapper; // JSON映射
}
和GET请求不同,POST请求严格地来说不能有URL参数,所有数据都应当从Post Body中读取。这里我们为了简化处理,只支持JSON格式的POST请求,这样,把Post数据转化为JavaBean就非常容易。
class PostDispatcher {
...
public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) {
Object[] arguments = new Object[parameterClasses.length];
for (int i = 0; i < parameterClasses.length; i++) {
Class<?> parameterClass = parameterClasses[i];
if (parameterClass == HttpServletRequest.class) {
arguments[i] = request;
} else if (parameterClass == HttpServletResponse.class) {
arguments[i] = response;
} else if (parameterClass == HttpSession.class) {
arguments[i] = request.getSession();
} else {
// 读取JSON并解析为JavaBean:
BufferedReader reader = request.getReader();
arguments[i] = this.objectMapper.readValue(reader, parameterClass);
}
}
return (ModelAndView) this.method.invoke(instance, arguments);
}
}
最后,我们来实现整个DispatcherServlet
的处理流程,以doGet()
为例:
public class DispatcherServlet extends HttpServlet {
...
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
String path = req.getRequestURI().substring(req.getContextPath().length());
// 根据路径查找GetDispatcher:
GetDispatcher dispatcher = this.getMappings.get(path);
if (dispatcher == null) {
// 未找到返回404:
resp.sendError(404);
return;
}
// 调用Controller方法获得返回值:
ModelAndView mv = dispatcher.invoke(req, resp);
// 允许返回null:
if (mv == null) {
return;
}
// 允许返回`redirect:`开头的view表示重定向:
if (mv.view.startsWith("redirect:")) {
resp.sendRedirect(mv.view.substring(9));
return;
}
// 将模板引擎渲染的内容写入响应:
PrintWriter pw = resp.getWriter();
this.viewEngine.render(mv, pw);
pw.flush();
}
}
这里有几个小改进:
- 允许Controller方法返回
null
,表示内部已自行处理完毕; - 允许Controller方法返回以
redirect:
开头的view名称,表示一个重定向。
这样使得上层代码编写更灵活。例如,一个显示用户资料的请求可以这样写:
@GetMapping("/user/profile")
public ModelAndView profile(HttpServletResponse response, HttpSession session) {
User user = (User) session.getAttribute("user");
if (user == null) {
// 未登录,跳转到登录页:
return new ModelAndView("redirect:/signin");
}
if (!user.isManager()) {
// 权限不够,返回403:
response.sendError(403);
return null;
}
return new ModelAndView("/profile.html", Map.of("user", user));
}
最后一步是在DispatcherServlet
的init()
方法中初始化所有Get和Post的映射,以及用于渲染的模板引擎:
public class DispatcherServlet extends HttpServlet {
private Map<String, GetDispatcher> getMappings = new HashMap<>();
private Map<String, PostDispatcher> postMappings = new HashMap<>();
private ViewEngine viewEngine;
@Override
public void init() throws ServletException {
this.getMappings = scanGetInControllers();
this.postMappings = scanPostInControllers();
this.viewEngine = new ViewEngine(getServletContext());
}
...
}
如何扫描所有Controller以获取所有标记有@GetMapping
和@PostMapping
的方法?当然是使用反射了。虽然代码比较繁琐,但我们相信各位童鞋可以轻松实现。
这样,整个MVC框架就搭建完毕。
实现渲染
有的童鞋对如何使用模板引擎进行渲染有疑问,即如何实现上述的ViewEngine
?其实ViewEngine
非常简单,只需要实现一个简单的render()
方法:
public class ViewEngine {
public void render(ModelAndView mv, Writer writer) throws IOException {
String view = mv.view;
Map<String, Object> model = mv.model;
// 根据view找到模板文件:
Template template = getTemplateByPath(view);
// 渲染并写入Writer:
template.write(writer, model);
}
}
Java有很多开源的模板引擎,常用的有:
他们的用法都大同小异。这里我们推荐一个使用Jinja语法的模板引擎Pebble,它的特点是语法简单,支持模板继承,编写出来的模板类似:
<html>
<body>
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
</body>
</html>
即变量用{{ xxx }}
表示,控制语句用{% xxx %}
表示。
使用Pebble渲染只需要如下几行代码:
public class ViewEngine {
private final PebbleEngine engine;
public ViewEngine(ServletContext servletContext) {
// 定义一个ServletLoader用于加载模板:
ServletLoader loader = new ServletLoader(servletContext);
// 模板编码:
loader.setCharset("UTF-8");
// 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录:
loader.setPrefix("/WEB-INF/templates");
// 模板后缀:
loader.setSuffix("");
// 创建Pebble实例:
this.engine = new PebbleEngine.Builder()
.autoEscaping(true) // 默认打开HTML字符转义,防止XSS攻击
.cacheActive(false) // 禁用缓存使得每次修改模板可以立刻看到效果
.loader(loader).build();
}
public void render(ModelAndView mv, Writer writer) throws IOException {
// 查找模板:
PebbleTemplate template = this.engine.getTemplate(mv.view);
// 渲染:
template.evaluate(writer, mv.model);
}
}
最后我们来看看整个工程的结构:
其中,framework
包是MVC的框架,完全可以单独编译后作为一个Maven依赖引入,controller
包才是我们需要编写的业务逻辑。
我们还硬性规定模板必须放在webapp/WEB-INF/templates
目录下,静态文件必须放在webapp/static
目录下,因此,为了便于开发,我们还顺带实现一个FileServlet
来处理静态文件:
@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })
public class FileServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 读取当前请求路径:
ServletContext ctx = req.getServletContext();
// RequestURI包含ContextPath,需要去掉:
String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
// 获取真实文件路径:
String filepath = ctx.getRealPath(urlPath);
if (filepath == null) {
// 无法获取到路径:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path path = Paths.get(filepath);
if (!path.toFile().isFile()) {
// 文件不存在:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 根据文件名猜测Content-Type:
String mime = Files.probeContentType(path);
if (mime == null) {
mime = "application/octet-stream";
}
resp.setContentType(mime);
// 读取文件并写入Response:
OutputStream output = resp.getOutputStream();
try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
input.transferTo(output);
}
output.flush();
}
}
运行代码,在浏览器中输入URLhttp://localhost:8080/hello?name=Bob
可以看到如下页面:
为了把方法参数的名称编译到class文件中,以便处理@GetMapping
时使用,我们需要打开编译器的一个参数,在Eclipse中勾选Preferences
-Java
-Compiler
-Store information about method parameters (usable via reflection)
;在Idea中选择Preferences
-Build, Execution, Deployment
-Compiler
-Java Compiler
-Additional command line parameters
,填入-parameters
;在Maven的pom.xml
添加一段配置如下:
<project ...>
<modelVersion>4.0.0</modelVersion>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
有些用过Spring MVC的童鞋会发现,本节实现的这个MVC框架,上层代码使用的公共类如GetMapping
、PostMapping
和ModelAndView
都和Spring MVC非常类似。实际上,我们这个MVC框架主要参考就是Spring MVC,通过实现一个“简化版”MVC,可以掌握Java Web MVC开发的核心思想与原理,对将来直接使用Spring MVC是非常有帮助的。
总结
一个MVC框架是基于Servlet基础抽象出更高级的接口,使得上层基于MVC框架的开发可以不涉及Servlet相关的HttpServletRequest
等接口,处理多个请求更加灵活,并且可以使用任意模板引擎,不必使用JSP。
青山不改,绿水常流!谢谢大家支持!!!
iOS:零碎整理iOS音视频开发API
在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒
- ios音效支持的格式
ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-aac、mp3、caf支持硬件解码,其他只支持软件解码, 软件界面因为比较耗电,所以,我们在开发过程中,经常采用的是caf、mp3
- 音频库
AVFoundation.framework
- 代码
// 打开资源
NSURL* url =[[NSBundle mainBundle]URLForResource:@"m_03" withExtension:@"wav"];
SystemSoundID soundID;
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID);
// 播放音效
AudioServicesPlaySystemSound(self.soundID);
// 删除音效
AudioServicesDisposeSystemSoundID(self.soundID);
- 框架
- 加载音乐资源并播放
AVAudioPlayer* player = musicDict[fileName];
if (!player) {
NSURL* url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
NSCAssert(url != nil, @"fileName not found musics");
NSError* error;
player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
if (error) {
NSLog(@"load music error");
return;
}
[musicDict setObject:player forKey:fileName];
}
if (player.isPlaying == NO) {
[player play];
}
6.暂停 停止操作
[player pause];// 暂停
[player stop];// 停止
[player isplaying];// 是否在播放
好了,现在能播放音乐了,但我们在看其他的应用的时候,一般当应用切换到后台的时候也能播放音乐,那这个又是如何实现的呢?这个只要设置音频的后台播放,具体为:
1> 在后台开启一个任务
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// 开启后台任务,让音乐继续播放
[application beginBackgroundTaskWithExpirationHandler:nil];
}
2> 设置项目配置文件
3> 设置音频链接会话,这个主要告诉设备如何处理音频事件的
1234 |
// 设置音频会话类型`` ``AVAudioSession* session = [AVAudioSession sharedInstance];`` ``[session setCategory:AVAudioSessionCategorySoloAmbient error:``nil``];`` ``[session setActive:``YES error:``nil``];
|
---|
这里有很多会话类型,如果想详细了解,可参考:http://blog.csdn.net/daiyelang/article/details/16986059
现在应该可以播放音乐了。
iOSUIKit动画从入门到放弃,简单易懂学习轻松容易掌握
动画 - UIKit
动画原理
- 视觉残留效应
- 运动模糊
做动画的时候要达到 60FPS 时候,画面才能流畅,不然用户会感觉界面卡顿。
UIView 提供的动画支持
UIView 动画本质上对 Core Animation 的封装,提供一个简洁好用的动画接口,在要求不复杂的情况下,完全可以实现很多动画。
UIView 动画可以设置的动画属性有:
- frame / bounds 大小变化
- center 中心位置
- transform 旋转平移等
- alpha 透明度
- backgroundColor 背景颜色
- contentStretch 拉伸内容
- Autolayout环境下的动画要直接修改constraint,注意 setNeedsUpdateConstraints,layoutIfNeeded的用法
UIView 类方法动画
-
动画的开始和结束方法
-
UIView Block动画
-
Spring 动画
-
Keyframes 动画
-
转场动画
- 单个视图的过渡
- 从旧视图到新视图的过渡
1. 动画的开始和结束方法
基本语句:
动画开始结束标记UIView.beginAnimations(String?, context: UnsafeMutablePointer<Void>)
第一个参数是动画标识,第二个参数是附加参数,在设置了代理的情况下,此参数将发送setAnimationWillStartSelector
和 setAnimationDidStopSelector
所指定的方法,一般设为 nil。
UIView.commitAnimations()
动画结束
动画参数设置方法
-
UIView.setAnimationDelay(NSTimeInterval)
设置动画的延时 -
UIView.setAnimationDuration(NSTimeInterval)
设置动画持续时间 -
UIView.setAnimationDelegate(AnyObject?)
设置动画代理 -
UIView.setAnimationWillStartSelector(Selector)
设置动画即将开始时代理执行的SEL -
UIView.setAnimationDidStopSelector(Selector)
设置动画结束时代理对象执行的SEL -
UIView.setAnimationRepeatCount(Float)
设置动画的重复次数 -
UIView.setAnimationCurve(UIViewAnimationCurve)
设置动画的曲线 -
UIView.setAnimationRepeatAutoreverses(Bool)
设置动画是否继续执行相反的动画 -
UIView.setAnimationsEnabled(Bool)
是否禁用动画效果(对象属性依然会被改变,只是没有动画效果) -
UIView.setAnimationBeginsFromCurrentState(Bool)
设置是否从当前状态开始播放动画- 假设上一个动画正在播放,且尚未播放完毕,我们将要进行一个新的动画:
- 当为true时:动画将从上一个动画所在的状态开始播放
- 当为false时:动画将从上一个动画所指定的最终状态开始播放(此时上一个动画马上结束)
以下是简单的例子:
2. UIView Block 动画
1)最简单的 Block 动画 包含时间和动画
UIView.animateWithDuration(NSTimeInterval) { // 动画持续时间
<#code#>//执行动画
}
2)带有动画完成回调的 Block 动画
UIView.animateWithDuration(NSTimeInterval, animations: {
<#code#>//执行动画
}) { (Bool) in
<#code#>// 动画完毕后执行的操作
}
3)可以设置延迟和过渡效果 Block 动画
UIView.animateWithDuration(NSTimeInterval, delay: NSTimeInterval, options: UIViewAnimationOptions, animations: {
<#code#>
}) { (<#Bool#>) in
<#code#>
}
注意,此处的 UIViewAnimationOptions 可以组合使用,在 swift 中写法 options: [UIViewAnimationOptions.CurveEaseInOut, UIViewAnimationOptions.Repeat]
具体的枚举值,看官方文档即可。
4)Spring 动画
iOS7 后新增 Spring 动画,iOS 系统动画大部分采用 Spring Animation。
UIView.animateWithDuration(NSTimeInterval, delay: NSTimeInterval, usingSpringWithDamping: CGFloat, initialSpringVelocity: CGFloat, options: UIViewAnimationOptions, animations: {
<#code#>
}) { (<#Bool#>) in
<#code#>
}
- Duration: 动画持续时间
- delay: 动画执行延时
- usingSpringWithDamping: 震动效果,范围 0~1,数值越小,震动效果越明显
- initialSpringVelocity: 初始速度
- options: 动画的过渡效果
5)Keyframes 关键帧动画
UIView.animateKeyframesWithDuration(NSTimeInterval, delay: NSTimeInterval, options: UIViewKeyframeAnimationOptions, animations: {
<#code#>
}) { (<#Bool#>) in
<#code#>
}
增加关键帧的方法
UIView.addKeyframeWithRelativeStartTime(Double, relativeDuration: Double, animations: {
<#code#>
})
注意开始时间和持续时间均是占总时间的比例
UIViewKeyframeAnimationOptions 的枚举值如下,可以组合使用
UIViewAnimationOptionLayoutSubviews //进行动画时布局子控件
UIViewAnimationOptionAllowUserInteraction //进行动画时允许用户交互
UIViewAnimationOptionBeginFromCurrentState //从当前状态开始动画
UIViewAnimationOptionRepeat //无限重复执行动画
UIViewAnimationOptionAutoreverse //执行动画回路
UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置
UIViewAnimationOptionOverrideInheritedOptions //不继承父动画设置
UIViewKeyframeAnimationOptionCalculationModeLinear //运算模式 :连续
UIViewKeyframeAnimationOptionCalculationModeDiscrete //运算模式 :离散
UIViewKeyframeAnimationOptionCalculationModePaced //运算模式 :均匀执行
UIViewKeyframeAnimationOptionCalculationModeCubic //运算模式 :平滑
UIViewKeyframeAnimationOptionCalculationModeCubicPaced //运算模式 :平滑均匀
关键帧动画:
private func blockAni5() {
UIView.animateKeyframesWithDuration(5, delay: 0.0, options: UIViewKeyframeAnimationOptions.CalculationModeLinear, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1.0/4, animations: {
self.greenView.backgroundColor = UIColor.redColor()
})
UIView.addKeyframeWithRelativeStartTime(1.0/4, relativeDuration: 1.0/4, animations: {
self.greenView.backgroundColor = UIColor.blackColor()
self.greenView.frame.size = CGSize(width: 50, height: 50)
})
UIView.addKeyframeWithRelativeStartTime(2.0/4, relativeDuration: 1.0/4, animations: {
self.greenView.backgroundColor = UIColor.yellowColor()
})
UIView.addKeyframeWithRelativeStartTime(3.0/4, relativeDuration: 1.0/4, animations: {
self.greenView.backgroundColor = UIColor.blueColor()
self.greenView.frame.size = CGSize(width: 250, height: 250)
})
}) { (_) in
print("动画完成blockAni5()")
}
}
简单的例子:
3. UIView 转场动画
在进行示例之前,大家需要注意一点过渡转变动画与动画属性动画的不同之处。我们在创建动画属性动画时只需要在animations闭包中添加对视图动画属性修改的代码即可,它没有作用域或作用视图的概念。而在过渡转变动画中有作用视图的概念,也就是说我们调用过渡转变动画方法时需要指定一个作用视图
过渡转变动画中的作用视图并不是我们的目标视图,而是目标视图的容器视图,那么大家不难想象,如果该容器视图中有多个子视图,那么这些子视图都会有过渡转变动画效果。
1)从旧视图到新视图的转场
UIView.transitionFromView(UIView, toView: UIView, duration: NSTimeInterval, options: UIViewAnimationOptions) { (<#Bool#>) in
<#code#>
}
在该动画过程中,fromView 会从父视图中移除,并将 toView 添加到父视图中。转场动画的作用对象是父视图,过渡效果体现在父视图上
2)单个试图的过渡
UIView.transitionWithView(UIView, duration: NSTimeInterval, options: UIViewAnimationOptions, animations: {
<#code#>
}) { (<#Bool#>) in
<#code#>
}
简单的例子
核心动画 CoreAnimations
Core Animation(核心动画)是一组强大的动画 API,是直接操作 CALayer 层来产生动画,相比上述的 UIView 动画,可以实现更复杂的动画效果。
事务管理 CATransaction
CALayer 的可用于动画的属性成为 Animatable properties,苹果官方有详细的列表,显示了所有了可以动画的属性 CALayer Animatable Properties。如果一个Layer对象对应着 View,则称这个 Layer 是一个 Root Layer, 非 Root Layer 一般是通过 CALayer 或者其子类直接创建的。
所有的非 Root Layer 在设置 Amimation Properties 的时候都存在隐式动画,默认的 duration 是0.25秒
事务(transaction)实际上是Core Animation用来包含一系列属性动画集合的机制,用指定事务去改变可以做动画的图层属性,不会立刻发生变化,而是提交事务时用一个动画过渡到新值。任何 Layer 的可动画属性的设置都属于某个 CATransaction,事务的作用是为了保证多个属性的变化同时进行。事务可以嵌套,当事务嵌套时候,只有最外层的事务 commit 之后,整个动画才开始。
CATransaction没有任何实例方法,只有类型方法。CATransaction.begin()
和CATransaction.commit()
构成了一个动画块:
CATransaction.begin()
/* animation block */
CATransaction.commit()
其他的方法
func animationDuration() -> CFTimeInterval // get duration, defaults to 0.25s
func setAnimationDuration(dur: CFTimeInterval) // set duration
func animationTimingFunction() -> CAMediaTimingFunction? // get timing function
func setAnimationTimingFunction(function: CAMediaTimingFunction?) // set timing function
func disableActions() -> Bool // get disable actions state
func setDisableActions(flag: Bool) // set disable actions state
func completionBlock() -> (() -> Void)? // get completion block
func setCompletionBlock(block: (() -> Void)?) // set completion block
以上四组的方法可以用以下两个方法代替
func valueForKey(key: String) -> AnyObject?
func setValue(anObject: AnyObject?, forKey key: String)
CATransaction
动画块只能处理CALayer相关动画,无法正确处理UIView的动画,甚至UIView的 Root layer(与UIView相关联的CALayer)也不行。
UIView 的 Root layer动画为什么会在CATransaction动画块中失效?
隐式动画的查找过程如下:
禁止隐式动画:
我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:
- 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
- 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
- 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。
于是这就解释了 UIKit 是如何禁用隐式动画的:每个 UIView 对它关联的图层都扮演了一个委托,并且提供了
-actionForLayer:forKey
的实现方法。当不在一个动画块的实现中,UIView 对所有图层行为返回 nil,但是在动画 block 范围之内,它就返回了一个非空值。
- UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画。
- 对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。
参考资料 iOS Actions
时间系统
CAMediaTiming
协议定义了在一段动画内用来控制逝去时间的属性的集合。CALayer 通过CAMediaTiming
协议实现了一个有层级关系的时间系统。
几个重要属性(都是CALayer的属性):
- beginTime 是相对于父级对象的开始时间
- timeOffset是active local time的偏移量
- speed 设置当前对象的时间流逝相对于父级对象时间流的流逝速度
- fillMode 决定了当前对象过了非 active 时间段的行为
显示动画
当需要对非 Root Layer 进行动画或者需要对动画做更多的自定义的行为的时候,需要使用显示动画,基类为 CAAnimation
核心动画类中可以直接使用的类有:
- CABasicAnimation
- CAKeyframeAnimation
- CATransition
- CAAnimationGroup
- CASpringAnimation
CABasicAnimation有三个比较重要的属性,fromValue,toValue,byValue,这三个属性都是可选的,但不能同时多于两个为非空.最终都是为了确定animation变化的起点和终点.中间的值都是通过插值方式计算出来的.插值计算的结果由timingFunction指定,默认timingFunction为nil,会使用liner的,也就是变化是均匀的.
1. 核心动画类的核心方法
-
初始化CAAnimation对象
- 一般使用animation方法生成实例
let animation = CABasicAnimation()
- 如果是CAPropertyAnimation的子类,可以使用'let animation = CABasicAnimation(keyPath: String?)'来生成
- 一般使用animation方法生成实例
-
设置动画的相关属性
- 执行时间
- 执行曲线
- keyPath 的目标值
- 代理等
animation.duration = 2.0
// animation.fromValue = UIColor.blackColor()
animation.toValue = NSValue(CGPoint: CGPointMake(300, 300))
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
-
动画的添加和移除
- 调用 CALayer 的
view2.layer.addAnimation(animation, forKey: "color")
- 停止动画
view2.layer.removeAnimationForKey(String)
和view2.layer.removeAllAnimations()
- 调用 CALayer 的
防止动画结束后回到初始状态
只需设置removedOnCompletion、fillMode两个属性就可以了。
transformAnima.removedOnCompletion = NO;
transformAnima.fillMode = kCAFillModeForwards;
解释:为什么动画结束后返回原状态?
给一个视图添加layer动画时,真正移动并不是我们的视图本身,而是 presentation layer 的一个缓存。动画开始时 presentation layer开始移动,原始layer隐藏,动画结束时,presentation layer从屏幕上移除,原始layer显示。这就解释了为什么我们的视图在动画结束后又回到了原来的状态,因为它根本就没动过。
这个同样也可以解释为什么在动画移动过程中,我们为何不能对其进行任何操作。
所以在我们完成layer动画之后,最好将我们的layer属性设置为我们最终状态的属性,然后将presentation layer 移除掉。
2. 核心动画类的常用属性
- KeyPath:可以指定 KeyPath 为 CALayer 的属性值,并对它修改,注意部分属性是不支持动画的
- duration:动画的持续时间
- repeatCount: 动画的重复次数
- timingFunction:动画的时间节奏控制
- fillMode:视图在非Active时的行为
- removedOnCompletion:动画执行完毕后是否从图层上移除,默认为YES(视图会恢复到动画前的状态),可设置为NO(图层保持动画执行后的状态,前提是fillMode设置为kCAFillModeForwards)
- beginTime:动画延迟执行时间(通过CACurrentMediaTime() + your time 设置)
- delegate:代理
func animationDidStart(anim: CAAnimation)
func animationDidStop(anim: CAAnimation, finished flag: Bool)
Timing Function对应的类是CAMediaTimingFunction,它提供了两种获得时间函数的方式,一种是使用预定义的五种时间函数,一种是通过给点两个控制点得到一个时间函数. 相关的方法为
CAMediaTimingFunction(name: String)
CAMediaTimingFunction(controlPoints: Float, c1y: Float, c2x: Float, c2y: Float)
五种预定义的时间函数名字的常量变量分别为
- kCAMediaTimingFunctionLinear,
- kCAMediaTimingFunctionEaseIn,
- kCAMediaTimingFunctionEaseOut,
- kCAMediaTimingFunctionEaseInEaseOut,
- kCAMediaTimingFunctionDefault
自定义的 Timing Function 的函数图像就是一条三次的贝塞尔曲线。
CAKeyframeAnimation动画
两个决定动画关键帧的属性:
-
values: 关键帧数组对象,里面每一个元素就是一个关键帧,动画会在相应时间段内,依次执行数组中每一个关键帧动画
-
path: 动画路径对象,可以指定一个路径,在执行动画时会沿着路径移动,path只能对CALayer的 anchorPoint 和 position 属性起作用
-
keyTimes: 设置关键帧对应的时间点。范围0 ~ 1,默认每一帧时间平分,keyTimes数组中的每个元素定义了相应的keyframe的持续时间值作为动画的总持续时间的一小部分,每个元素的值必须大于、或等于前一个值。
keyframeAni.keyTimes = [0.1,0.5,0.7,0.8,1]
-
calculationMode 计算模式,其主要针对的是每一帧的内容为一个座标点的情况,也就是对anchorPoint 和 position 进行的动画,表示插值计算的模式
- kCAAnimationLinear 默认值 直线相连来差值
- kCAAnimationDiscrete 离散的,不进行插值计算,所有关键帧逐个显示
- kCAAnimationPaced 动画均匀的,此时keytimes和timeFunctions无效
- kCAAnimationCubic 对关键帧为坐标点的关键帧进行圆滑曲线相连后插值计算,对于曲线的形状还可以通过tensionValues,continuityValues,biasValues来进行调整自定义
- kCAAnimationCubicPaced 在kCAAnimationCubic的基础上使得动画运行变得均匀,就是系统时间内运动的距离相同,此时keyTimes以及timingFunctions也是无效的.
简单例子
CATransition
转场动画,比 UIView 的转场动画具有更多的动画效果。
CATransition的属性:
-
type: 过渡动画的类型
- kCATransitionFade 渐变
- kCATransitionMoveIn 覆盖
- kCATransitionPush 推出
- kCATransitionReveal 揭开
私有动画类型的值有:"cube"、"suckEffect"、"oglFlip"、 "rippleEffect"、"pageCurl"、"pageUnCurl"等等
-
subtype: 过渡动画的方向
-
- kCATransitionFromRight 从右边
- kCATransitionFromLeft 从左边
-
- kCATransitionFromTop 从顶部
- kCATransitionFromBottom 从底部
CASpringAnimation
CASpringAnimation是iOS9新加入动画类型,是CABasicAnimation的子类,用于实现弹簧动画。
CASpringAnimation的重要属性:
- mass:质量(影响弹簧的惯性,质量越大,弹簧惯性越大,运动的幅度越大)
- stiffness:弹性系数(弹性系数越大,弹簧的运动越快)
- damping:阻尼系数(阻尼系数越大,弹簧的停止越快)
- initialVelocity:初始速率(弹簧动画的初始速度大小,弹簧运动的初始方向与初始速率的正负一致,若初始速率为0,表示忽略该属性)
- settlingDuration:结算时间(根据动画参数估算弹簧开始运动到停止的时间,动画设置的时间最好根据此时间来设置)
private func springAni() {
let ani = CASpringAnimation(keyPath: "bounds")
ani.mass = 10.0 //质量,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大
ani.stiffness = 5000 //刚度系数(劲度系数/弹性系数),刚度系数越大,形变产生的力就越大,运动越快
ani.damping = 100.0//阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,停止越快
ani.initialVelocity = 5.0//初始速率,动画视图的初始速度大小;速率为正数时,速度方向与运动方向一致,速率为负数时,速度方向与运动方向相反
ani.duration = ani.settlingDuration
ani.toValue = NSValue(CGRect: view4.bounds)
ani.removedOnCompletion = false
ani.fillMode = kCAFillModeForwards
ani.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
view2.layer.addAnimation(ani, forKey: "boundsAni")
}
CAAnimationGroup
使用Group可以将多个动画合并一起加入到层中,Group中所有动画并发执行,可以方便地实现需要多种类型动画的场景,group动画以数组表示。
private func groupAni() {
let posAni = CABasicAnimation(keyPath: "position")
posAni.toValue = NSValue(CGPoint: CGPoint(x: 310, y: 400))
let boundAni = CABasicAnimation(keyPath: "bounds")
boundAni.toValue = NSValue(CGRect: CGRectMake(0, 0, 200, 200))
let colorAni = CABasicAnimation(keyPath: "backgroundColor")
colorAni.toValue = UIColor.redColor().CGColor
let groupAni = CAAnimationGroup()
groupAni.animations = [posAni, boundAni, colorAni]
groupAni.duration = 1.5
groupAni.fillMode = kCAFillModeForwards
groupAni.removedOnCompletion = false
groupAni.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
view1.layer.addAnimation(groupAni, forKey: "groupAni")
}
文中demo的地址:Github 动画demo
整理了一篇非常全的iOS面试题,值得你收藏,为您的面试助力
目录
1. 一、基础知识点
2. 二、第三方框架
3. 三、算法
4. 四、编码格式(优化细节)
5. 五、其他知识点
础知识点
设式是什么? 你知道哪些设计模式,并简要叙述?
设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的事情。
1). MVC模式:Model View Control,把模型 视图 控制器 层进行解耦合编写。
2). MVVM模式:Model View ViewModel 把模型 视图 业务逻辑 层进行解耦和编写。
3). 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。
4). 观察者模式:KVO是典型的观察者模式,观察某个属性的状态,状态发生变化时通知观察者。
5). 委托模式:代理+协议的组合。实现1对1的反向传值操作。
6). 工厂模式:通过一个类方法,批量的根据已有模板生产对象。
MVC 和 MVVM 的区别
MVVM是对胖模型进行的拆分,其本质是给控制器减负,将一些弱业务逻辑放到VM中去处理。
MVC是一切设计的基础,所有新的设计模式都是基于MVC进行的改进。
参考:iOS MVVM架构总结
#import跟 #include 有什么区别,@class呢,#import<> 跟 #import””有什么区别?
1). #import是Objective-C导入头文件的关键字,#include是C/C++导入头文件的关键字,使用#import头文件会自动只导入一次,不会重复导入。
2). @class告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含。
3). #import<>用来包含系统的头文件,#import””用来包含用户头文件。
frame 和 bounds 有什么不同?
frame指的是:该view在父view坐标系统中的位置和大小。(参照点是父view的坐标系统)
bounds指的是:该view在本身坐标系统中的位置和大小。(参照点是本身坐标系统)
Objective-C的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方法用继
承好还是分类好?为什么?
答:Objective-C的类不可以多重继承;可以实现多个接口(协议);Category是类别;一般情况用分类好,用Category去重写类的方法,仅对本Category有效,不会影响到其他类与原有类的关系。
@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的
@property 的本质是什么?
@property = ivar + getter + setter;
“属性” (property)有两大概念:ivar(实例变量)、getter+setter(存取方法)
“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。
@property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?
属性可以拥有的特质分为四类:
1.原子性--- nonatomic 特质
2.读/写权限---readwrite(读写)、readonly (只读)
3.内存管理语义---assign、strong、 weak、unsafe_unretained、copy
4.方法名---getter= 、setter=
5.不常用的:nonnull,null_resettable,nullable
属性关键字 readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?
1). readwrite 是可读可写特性。需要生成getter方法和setter方法。
2). readonly 是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。
3). assign 是赋值特性。setter方法将传入参数赋值给实例变量;仅设置变量时,assign用于基本数据类型。
4). retain(MRC)/strong(ARC) 表示持有特性。setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。
5). copy 表示拷贝特性。setter方法将传入对象复制一份,需要完全一份新的变量时。
6). nonatomic 非原子操作。不写的话默认就是atomic。atomic 和 nonatomic 的区别在于,系统自动生成的 getter/setter 方法不一样。对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。
不过atomic可并不能保证线程安全。
参考:[爆栈热门 iOS 问题] atomic 和 nonatomic 有什么区别?
什么情况使用 weak 关键字,相比 assign 有什么不同?
1.在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。
2.自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。
IBOutlet连出来的视图属性为什么可以被设置成weak?
因为父控件的subViews数组已经对它有一个强引用。
不同点:
assign 可以用非 OC 对象,而 weak 必须用于 OC 对象。
weak 表明该属性定义了一种“非拥有关系”。在属性所指的对象销毁时,属性值会自动清空(nil)。
怎么用 copy 关键字?
用途:
- NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
- block 也经常使用 copy 关键字。
说明:
block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。
用@property声明的 NSString / NSArray / NSDictionary 经常使用 copy 关键字,为什么?如果改用strong关键字,可能造成什么问题?
答:用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
- 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。
- 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
总结:使用copy的目的是,防止把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发送变化会无意间篡改不可变类型对象原来的值。
浅拷贝和深拷贝的区别?
浅拷贝:只复制指向对象的指针,而不复制引用对象本身。
深拷贝:复制引用对象本身。内存中存在了两份独立对象本身,当修改A时,A_copy不变。
系统对象的 copy 与 mutableCopy 方法
不管是集合类对象(NSArray、NSDictionary、NSSet ... 之类的对象),还是非集合类对象(NSString, NSNumber ... 之类的对象),接收到copy和mutableCopy消息时,都遵循以下准则:
- copy 返回的是不可变对象(immutableObject);如果用copy返回值调用mutable对象的方法就会crash。
- mutableCopy 返回的是可变对象(mutableObject)。
1. 一、非集合类对象的copy与mutableCopy
2. 在非集合类对象中,对不可变对象进行copy操作,是指针复制,mutableCopy操作是内容复制;
3. 对可变对象进行copy和mutableCopy都是内容复制。用代码简单表示如下: NSString *str = @"hello word!"; NSString *strCopy = [str copy] // 指针复制,strCopy与str的地址一样 NSMutableString *strMCopy = [str mutableCopy] // 内容复制,strMCopy与str的地址不一样 NSMutableString *mutableStr = [NSMutableString stringWithString: @"hello word!"]; NSString *strCopy = [mutableStr copy] // 内容复制 NSMutableString *strMCopy = [mutableStr mutableCopy] // 内容复制
1. 二、集合类对象的copy与mutableCopy (同上)
2. 在集合类对象中,对不可变对象进行copy操作,是指针复制,mutableCopy操作是内容复制;
3. 对可变对象进行copy和mutableCopy都是内容复制。但是:集合对象的内容复制仅限于对象本身,对集合内的对象元素仍然是指针复制。(即单层内容复制) NSArray *arr = @[@[@"a", @"b"], @[@"c", @"d"]; NSArray *copyArr = [arr copy]; // 指针复制 NSMutableArray *mCopyArr = [arr mutableCopy]; //单层内容复制 NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil]; NSArray *copyArr = [mutableArr copy]; // 单层内容复制 NSMutableArray *mCopyArr = [mutableArr mutableCopy]; // 单层内容复制
【总结一句话】:
只有对不可变对象进行copy操作是指针复制(浅复制),其它情况都是内容复制(深复制)!
这个写法会出什么问题:@property (nonatomic, copy) NSMutableArray *arr;
问题:添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃。
//如:-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
// copy后返回的是不可变对象(即 arr 是 NSArray 类型,NSArray 类型对象不能调用 NSMutableArray 类型对象的方法)
原因:是因为 copy 就是复制一个不可变 NSArray 的对象,不能对 NSArray 对象进行添加/修改。
如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?
若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
具体步骤:
- 需声明该类遵从 NSCopying 协议
- 实现 NSCopying 协议的方法。
// 该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
// 注意:使用 copy 修饰符,调用的是copy方法,其实真正需要实现的是 “copyWithZone” 方法。
写一个 setter 方法用于完成 @property (nonatomic, retain) NSString *name,写一个 setter 方法用于完成 @property (nonatomic, copy) NSString *name
1. // retain - (void)setName:(NSString *)str {
2. [str retain];
3. [_name release];
4. _name = str;
5. } // copy - (void)setName:(NSString *)str {
6. id t = [str copy];
7. [_name release];
8. _name = t;
9. }
@synthesize 和 @dynamic 分别有什么作用?
@property有两个对应的词,一个是@synthesize(合成实例变量),一个是@dynamic。
如果@synthesize和@dynamic都没有写,那么默认的就是 @synthesize var = _var;
// 在类的实现代码里通过 @synthesize 语法可以来指定实例变量的名字。(@synthesize var = _newVar;)
- @synthesize 的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
- @dynamic 告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成(如,@dynamic var)。
常见的 Objective-C 的数据类型有那些,和C的基本数据类型有什么区别?如:NSInteger和int
Objective-C的数据类型有NSString,NSNumber,NSArray,NSMutableArray,NSData等等,这些都是class,创建后便是对象,而C语言的基本数据类型int,只是一定字节的内存空间,用于存放数值;NSInteger是基本数据类型,并不是NSNumber的子类,当然也不是NSObject的子类。NSInteger是基本数据类型Int或者Long的别名(NSInteger的定义typedef long NSInteger),它的区别在于,NSInteger会根据系统是32位还是64位来决定是本身是int还是long。
id 声明的对象有什么特性?
id 声明的对象具有运行时的特性,即可以指向任意类型的Objcetive-C的对象。
Objective-C 如何对内存管理的,说说你的看法和解决方法?
答:Objective-C的内存管理主要有三种方式ARC(自动内存计数)、手动内存计数、内存池。
1). 自动内存计数ARC:由Xcode自动在App编译阶段,在代码中添加内存管理代码。
2). 手动内存计数MRC:遵循内存谁申请、谁释放;谁添加,谁释放的原则。
3). 内存释放池Release Pool:把需要释放的内存统一放在一个池子中,当池子被抽干后(drain),池子中所有的内存空间也被自动释放掉。内存池的释放操作分为自动和手动。自动释放受runloop机制影响。
Objective-C 中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么?
答:线程创建有三种方法:使用NSThread创建、使用GCD的dispatch、使用子类化的NSOperation,然后将其加入NSOperationQueue;在主线程执行代码,方法是performSelectorOnMainThread,如果想延时执行代码可以用performSelector:onThread:withObject:waitUntilDone:
Category(类别)、 Extension(扩展)和继承的区别
区别:
- 分类有名字,类扩展没有分类名字,是一种特殊的分类。
- 分类只能扩展方法(属性仅仅是声明,并没真正实现),类扩展可以扩展属性、成员变量和方法。
- 继承可以增加,修改或者删除方法,并且可以增加属性。
我们说的OC是动态运行时语言是什么意思?
答:主要是将数据类型的确定由编译时,推迟到了运行时。简单来说, 运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。
为什么我们常见的delegate属性都用是week而不是retain/strong?
答:是为了防止delegate两端产生不必要的循环引用。
@property (nonatomic, weak) id delegate;
什么时候用delete,什么时候用Notification?
Delegate(委托模式):1对1的反向消息通知功能。
Notification(通知模式):只想要把消息发送出去,告知某些状态的变化。但是并不关心谁想要知道这个。
什么是 KVO 和 KVC?
1). KVC(Key-Value-Coding):键值编码 是一种通过字符串间接访问对象的方式(即给属性赋值)
举例说明:
stu.name = @"张三" // 点语法给属性赋值
[stu setValue:@"张三" forKey:@"name"]; // 通过字符串使用KVC方式给属性赋值
stu1.nameLabel.text = @"张三";
[stu1 setValue:@"张三" forKey:@"nameLabel.text"]; // 跨层赋值
2). KVO(key-Value-Observing):键值观察机制 他提供了观察某一属性变化的方法,极大的简化了代码。
KVO只能被KVC触发,包括使用setValue:forKey:方法和点语法。
// 通过下方方法为属性添加KVO观察
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// 当被观察的属性发送变化时,会自动触发下方方法 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
KVC 和 KVO 的 keyPath 可以是属性、实例变量、成员变量。
iOS 成员变量,属性变量,局部变量,实例变量,全局变量 详解
KVC的底层实现?
当一个对象调用setValue方法时,方法内部会做以下操作:
1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
ViewController生命周期
按照执行顺序排列:
- initWithCoder:通过nib文件初始化时触发。
- awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
- loadView:开始加载视图控制器自带的view。
- viewDidLoad:视图控制器的view被加载完成。
- viewWillAppear:视图控制器的view将要显示在window上。
- updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
- viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
- viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
- viewDidAppear:视图控制器的view已经展示到window上。
- viewWillDisappear:视图控制器的view将要从window上消失。
- viewDidDisappear:视图控制器的view已经从window上消失。
方法和选择器有何不同?
selector是一个方法的名字,方法是一个组合体,包含了名字和实现。
你是否接触过OC中的反射机制?简单聊一下概念和使用
1). class反射
通过类名的字符串形式实例化对象。
Class class = NSClassFromString(@"student");
Student *stu = [[class alloc] init];
将类名变为字符串。
Class class =[Student class];
NSString className = NSStringFromClass(class); 2). SEL的反射 通过方法的字符串形式实例化方法。 SEL selector = NSSelectorFromString(@"setName"); [stu performSelector:selector withObject:@"Mike"]; 将方法变成字符串。NSStringFromSelector(@selector(setName:));
调用方法有两种方式:
1). 直接通过方法名来调用。[person show];
2). 间接的通过SEL数据来调用 。SEL aaa = @selector(show); [person performSelector:aaa];
如何对iOS设备进行性能测试?
答: Profile-> Instruments ->Time Profiler
开发项目时你是怎么检查内存泄露?
1). 静态分析 analyze。
2). instruments工具里面有个leak可以动态分析。
什么是懒加载?
答:懒加载就是只在用到的时候才去初始化。也可以理解成延时加载。
我觉得最好也最简单的一个例子就是tableView中图片的加载显示了, 一个延时加载, 避免内存过高,一个异步加载,避免线程堵塞提高用户体验。
类变量的 @public,@protected,@private,@package 声明各有什么含义?
@public 任何地方都能访问;
@protected 该类和子类中访问,是默认的;
@private 只能在本类中访问;
@package 本包内使用,跨包不可以。
什么是谓词?
谓词就是通过NSPredicate给定的逻辑条件作为约束条件,完成对数据的筛选。
//定义谓词对象,谓词对象中包含了过滤条件(过滤条件比较多)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30];
//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];
isa指针问题
isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调 用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环。
如何访问并修改一个类的私有属性?
1). 一种是通过KVC获取。
2). 通过runtime访问并修改私有属性。
一个objc对象的isa的指针指向什么?有什么作用?
答:指向他的类对象,从而可以找到对象上的方法。
下面的代码输出什么?
@implementation Son : Father
- (id)init {
if (self = [super init]) {
NSLog(@"%@", NSStringFromClass([self class])); // Son
NSLog(@"%@", NSStringFromClass([super class])); // Son
}
return self;
}
@end
// 解析:
self 是类的隐藏参数,指向当前调用方法的这个类的实例。
super是一个Magic Keyword,它本质是一个编译器标示符,和self是指向的同一个消息接收者。
不同的是:super会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类里的。
上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *obj 这个对象。
写一个完整的代理,包括声明、实现
1. // 创建 @protocol MyDelagate @required -(void)eat:(NSString *)foodName;
2. @optional -(void)run;
3. @end // 声明 .h @interface person: NSObject
4.
5. @end // 实现 .m @implementation person - (void)eat:(NSString *)foodName { NSLog(@"吃:%@!", foodName);
6. }
7. - (void)run { NSLog(@"run!");
8. }
9.
10. @end
isKindOfClass、isMemberOfClass、selector作用分别是什么
isKindOfClass:作用是某个对象属于某个类型或者继承自某类型。
isMemberOfClass:某个对象确切属于某个类型。
selector:通过方法名,获取在内存中的函数的入口地址。
delegate 和 notification 的区别
1). 二者都用于传递消息,不同之处主要在于一个是一对一的,另一个是一对多的。
2). notification通过维护一个array,实现一对多消息的转发。
3). delegate需要两者之间必须建立联系,不然没法调用代理的方法;notification不需要两者之间有联系。
什么是block?
闭包(block):闭包就是获取其它函数局部变量的匿名函数。
block反向传值
1. * 在控制器间传值可以使用代理或者block,使用block相对来说简洁。
2.
3. * 在前一个控制器的touchesBegan:方法内实现如下代码。 // OneViewController.m TwoViewController *twoVC = [[TwoViewController alloc] init];
4. twoVC.valueBlcok = ^(NSString *str) { NSLog(@"OneViewController拿到值:%@", str);
5. };
6. [self presentViewController:twoVC animated:YES completion:nil]; // TwoViewController.h (在.h文件中声明一个block属性) @property (nonatomic ,strong) void(^valueBlcok)(NSString *str); // TwoViewController.m (在.m文件中实现方法) - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 传值:调用block if (_valueBlcok) {
7. _valueBlcok(@"123456");
8. }
9. }
block的注意点
1). 在block内部使用外部指针且会造成循环引用情况下,需要用__week修饰外部指针:
__weak typeof(self) weakSelf = self;
2). 在block内部如果调用了延时函数还使用弱指针会取不到该指针,因为已经被销毁了,需要在block内部再将弱指针重新强引用一下。
__strong typeof(self) strongSelf = weakSelf;
3). 如果需要在block内部改变外部栈区变量的话,需要在用__block修饰外部变量。
BAD_ACCESS在什么情况下出现?
答:这种问题在开发时经常遇到。原因是访问了野指针,比如访问已经释放对象的成员变量或者发消息、死循环等。
lldb(gdb)常用的控制台调试命令?
1). p 输出基本类型。是打印命令,需要指定类型。是print的简写
p (int)[[[self view] subviews] count]
2). po 打印对象,会调用对象description方法。是print-object的简写
po [self view]
3). expr 可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。
4). bt:打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈
5). br l:是breakpoint list的简写
你一般是怎么用Instruments的?
Instruments里面工具很多,常用:
1). Time Profiler: 性能分析
2). Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
3). Allocations:用来检查内存,写算法的那批人也用这个来检查。
4). Leaks:检查内存,看是否有内存泄露。
iOS中常用的数据存储方式有哪些?
数据存储有四种方案:NSUserDefault、KeyChain、File、DB。
其中File有三种方式:writeToFile:atomically:、Plist、NSKeyedAchiever(归档)
DB包括:SQLite、FMDB、CoreData
iOS的沙盒目录结构是怎样的?
沙盒结构:
- AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
- Documents:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。iCloud备份目录。(这里不能存缓存文件,否则上架不被通过)
- Library 目录:这个目录下有两个子目录:
Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.
Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。 - tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。
iOS多线程技术有哪几种方式?
答:pthread、NSThread、GCD、NSOperation
GCD 与 NSOperation 的区别:
GCD 和 NSOperation 都是用于实现多线程:
GCD 基于C语言的底层API,GCD主要与block结合使用,代码简洁高效。
NSOperation 属于Objective-C类,是基于GCD更高一层的封装。复杂任务一般用NSOperation实现。
写出使用GCD方式从子线程回到主线程的方法代码
答:dispatch_sync(dispatch_get_main_queue(), ^{ });
如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
// 使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{ /*加载图片1 / });dispatch_group_async(group, queue, ^{ /加载图片2 / });dispatch_group_async(group, queue, ^{ /加载图片3 */ });
// 当并发队列组中的任务执行完毕后才会执行这里的代码
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
dispatch_barrier_async(栅栏函数)的作用是什么?
1. 函数定义:dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
2. 作用: 1.在它前面的任务执行结束后它才执行,它后面的任务要等它执行完成后才会开始执行。 2.避免数据竞争 // 1.创建并发队列 dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT); // 2.向队列中添加任务 dispatch_async(queue, ^{ // 1.2是并行的 NSLog(@"任务1, %@",[NSThread currentThread]);
3. }); dispatch_async(queue, ^{ NSLog(@"任务2, %@",[NSThread currentThread]);
4. });
5.
6. dispatch_barrier_async(queue, ^{ NSLog(@"任务 barrier, %@", [NSThread currentThread]);
7. }); dispatch_async(queue, ^{ // 这两个是同时执行的 NSLog(@"任务3, %@",[NSThread currentThread]);
8. }); dispatch_async(queue, ^{ NSLog(@"任务4, %@",[NSThread currentThread]);
9. }); // 输出结果: 任务1 任务2 ——》 任务 barrier ——》任务3 任务4 // 其中的任务1与任务2,任务3与任务4 由于是并行处理先后顺序不定。
以下代码运行结果如何?
1. - (void)viewDidLoad {
2. [super viewDidLoad]; NSLog(@"1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2");
3. }); NSLog(@"3");
4. } // 只输出:1。(主线程死锁)
什么是 RunLoop
- 从字面上看,就是运行循环,跑圈
- 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)
- 一个线程对应一个RunLoop,基本作用就是保持程序的持续运行,处理app中的各种事件。
- 通过runloop,有事运行,没事就休息,可以节省cpu资源,提高程序性能。
主线程的run loop默认是启动的。iOS的应用程序里面,程序启动后会有一个如下的main()函数
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
RunLoop学习总结
说说你对 runtime 的理解
Runtime又叫运行时,是一套底层的C语言API,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的。
Runtime实现的机制是什么,怎么用,一般用于干嘛?
1). 使用时需要导入的头文件
2). Runtime 运行时机制,它是一套C语言库。
3). 实际上我们编写的所有OC代码,最终都是转成了runtime库的东西。
比如:
类转成了 Runtime 库里面的结构体等数据类型,
方法转成了 Runtime 库里面的C语言函数,
平时调方法都是转成了 objc_msgSend 函数(所以说OC有个消息发送机制)
// OC是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
// [stu show]; 在objc动态编译时,会被转意为:objc_msgSend(stu, @selector(show));
4). 因此,可以说 Runtime 是OC的底层实现,是OC的幕后执行者。
有了Runtime库,能做什么事情呢?
Runtime库里面包含了跟类、成员变量、方法相关的API。
比如:
(1)获取类里面的所有成员变量。
(2)为类动态添加成员变量。
(3)为类动态添加新的方法。
(4)动态改变类的方法实现等。(Method Swizzling)
因此,有了Runtime,想怎么改就怎么改。
什么是 Method Swizzle(黑魔法),什么情况下会使用?
1). 在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法 Method Swizzle。
2). Method Swizzle 指的是改变一个已存在的选择器对应的实现的过程。OC中方法的调用能够在运行时通过改变,通过改变类的调度表中选择器到最终函数间的映射关系。
3). 在OC中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用OC的动态特性,可以实现在运行时偷换selector对应的方法实现。
4). 每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的方法实现。
5). 我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP。
6). 我们可以利用 class_replaceMethod 来修改类。
7). 我们可以利用 method_setImplementation 来直接设置某个方法的IMP。
8). 归根结底,都是偷换了selector的IMP。
_objc_msgForward 函数是做什么的,直接调用它将会发生什么?
答:_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
什么是 TCP / UDP ?
TCP:传输控制协议。
UDP:用户数据协议。
TCP 是面向连接的,建立连接需要经历三次握手,是可靠的传输层协议。
UDP 是面向无连接的,数据传输是不可靠的,它只管发,不管收不收得到。
简单的说,TCP注重数据安全,而UDP数据传输快点,但安全性一般。
通信底层原理(OSI七层模型)
OSI采用了分层的结构化技术,共分七层:
物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
介绍一下XMPP?
XMPP是一种以XML为基础的开放式实时通信协议。
简单的说,XMPP就是一种协议,一种规定。就是说,在网络上传东西,XMM就是规定你上传大小的格式。
OC中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?
1. // 创建线程的方法 - [NSThread detachNewThreadSelector:nil toTarget:nil withObject:nil]
2. - [self performSelectorInBackground:nil withObject:nil];
3. - [[NSThread alloc] initWithTarget:nil selector:nil object:nil];
4. - dispatch_async(dispatch_get_global_queue(0, 0), ^{});
5. - [[NSOperationQueue new] addOperation:nil]; // 主线程中执行代码的方法 - [self performSelectorOnMainThread:nil withObject:nil waitUntilDone:YES];
6. - dispatch_async(dispatch_get_main_queue(), ^{});
7. - [[NSOperationQueue mainQueue] addOperation:nil];
tableView的重用机制?
答:UITableView 通过重用单元格来达到节省内存的目的: 通过为每个单元格指定一个重用标识符,即指定了单元格的种类,当屏幕上的单元格滑出屏幕时,系统会把这个单元格添加到重用队列中,等待被重用,当有新单元格从屏幕外滑入屏幕内时,从重用队列中找看有没有可以重用的单元格,如果有,就拿过来用,如果没有就创建一个来使用。
用伪代码写一个线程安全的单例模式
1. static id _instance;
2. + (id)allocWithZone:(struct _NSZone *)zone { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
3. _instance = [super allocWithZone:zone];
4. }); return _instance;
5. }
6.
7. + (instancetype)sharedData { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
8. _instance = [[self alloc] init];
9. }); return _instance;
10. }
11.
12. - (id)copyWithZone:(NSZone *)zone { return _instance;
13. }
如何实现视图的变形?
答:通过修改view的 transform 属性即可。
在手势对象基础类UIGestureRecognizer的常用子类手势类型中哪两个手势发生后,响应只会执行一次?
答:UITapGestureRecognizer,UISwipeGestureRecognizer是一次性手势,手势发生后,响应只会执行一次。
字符串常用方法:
NSString str = @"abc123";
NSArray arr = [str componentsSeparatedByString:@""]; //以目标字符串把原字符串分割成两部分,存到数组中。@[@"abc", @"123"];
如何高性能的给 UIImageView 加个圆角?
1. * 不好的解决方案:使用下面的方式会`强制Core Animation提前渲染屏幕的离屏绘制, 而离屏绘制就会给性能带来负面影响`,会有卡顿的现象出现。 self.view.layer.cornerRadius = 5.0f; self.view.layer.masksToBounds = YES;
2.
3. * 正确的解决方案:使用绘图技术
4.
5. - (UIImage *)circleImage { // NO代表透明 UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0); // 获得上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 添加一个圆 CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); CGContextAddEllipseInRect(ctx, rect); // 裁剪 CGContextClip(ctx); // 将图片画上去 [self drawInRect:rect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); // 关闭上下文 UIGraphicsEndImageContext(); return image;
6. }
7.
8. * 还有一种方案:使用了贝塞尔曲线"切割"个这个图片, 给UIImageView 添加了的圆角,其实也是通过绘图技术来实现的。 UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
9. imageView.center = CGPointMake(200, 300); UIImage *anotherImage = [UIImage imageNamed:@"image"]; UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
10. [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
11. cornerRadius:50] addClip];
12. [anotherImage drawInRect:imageView.bounds];
13. imageView.image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();
14. [self.view addSubview:imageView];
你是怎么封装一个view的
1. 1). 可以通过纯代码或者xib的方式来封装子控件 2). 建立一个跟view相关的模型,然后将模型数据传给view,通过模型上的数据给view的子控件赋值 /**
2. * 纯代码初始化控件时一定会走这个方法
3. */ - (instancetype)initWithFrame:(CGRect)frame { if(self = [super initWithFrame:frame]) {
4. [self setupUI];
5. } return self;
6. } /**
7. * 通过xib初始化控件时一定会走这个方法
8. */ - (id)initWithCoder:(NSCoder *)aDecoder { if(self = [super initWithCoder:aDecoder]) {
9. [self setupUI];
10. } return self;
11. }
12.
13. - (void)setupUI { // 初始化代码 }
HTTP协议中 POST 方法和 GET 方法有那些区别?
- GET用于向服务器请求数据,POST用于提交数据
- GET请求,请求参数拼接形式暴露在地址栏,而POST请求参数则放在请求体里面,因此GET请求不适合用于验证密码等操作
- GET请求的URL有长度限制,POST请求不会有长度限制
请简单的介绍下APNS发送系统消息的机制
APNS优势:杜绝了类似安卓那种为了接受通知不停在后台唤醒程序保持长连接的行为,由iOS系统和APNS进行长连接替代。
APNS的原理:
1). 应用在通知中心注册,由iOS系统向APNS请求返回设备令牌(device Token)
2). 应用程序接收到设备令牌并发送给自己的后台服务器
3). 服务器把要推送的内容和设备发送给APNS
4). APNS根据设备令牌找到设备,再由iOS根据APPID把推送内容展示
77. ios开发逆向传值的几种方法整理
第一种:代理传值
1. 第二个控制器: @protocol WJSecondViewControllerDelegate - (void)changeText:(NSString*)text; @end @property(nonatomic,assign)iddelegate;
2.
3. - (IBAction)buttonClick:(UIButton*)sender {
4. _str = sender.titleLabel.text;
5. [self.delegate changeText:sender.titleLabel.text];
6. [self.navigationController popViewControllerAnimated:YES];
7. }
8.
9. 第一个控制器:
10.
11. - (IBAction)pushToSecond:(id)sender {
12. WJSecondViewController *svc = [[WJSecondViewController alloc]initWithNibName:@"WJSecondViewController" bundle:nil];
13. svc.delegate = self;
14. svc.str = self.navigationItem.title;
15. [self.navigationController pushViewController:svc animated:YES];
16. [svc release];
17. }
18. - (void)changeText:(NSString *)text{ self.navigationItem.title = text;
19. }
第二种:通知传值
1. 第一个控制器: //注册监听通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(limitDataForModel:) name:@"NOV" object:nil];
2. - (void)limitDataForModel:(NSNotification *)noti{ self.gamesInfoArray = noti.object;
3. }
4.
5. 第二个控制器: //发送通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"NOV" object:gameArray];
第三种:单例传值
1. Single是一个单例类,并且有一个字符串类型的属性titleName
2. 在第二个控制器:
3.
4. - (IBAction)buttonClick:(UIButton*)sender {
5. Single *single = [Single sharedSingle];
6. single.titleName = sender.titleLabel.text;
7. [self.navigationController popViewControllerAnimated:YES];
8. }
9.
10. 第一个控制器:
11.
12. - (void)viewWillAppear:(BOOL)animated{
13. [super viewWillAppear:animated];
14. Single *single = [Single sharedSingle]; self.navigationItem.title = single.titleName;
15. }
第四种:block传值
1. 第二个控制器: @property (nonatomic,copy) void (^changeText_block)(NSString*);
2. - (IBAction)buttonClick:(UIButton*)sender {
3. _str = sender.titleLabel.text; self.changeText_block(sender.titleLabel.text);
4. [self.navigationController popViewControllerAnimated:YES];
5. }
6.
7. 第一个控制器:
8.
9. - (IBAction)pushToSecond:(id)sender {
10. WJSecondViewController *svc = [[WJSecondViewController alloc]initWithNibName:@"WJSecondViewController" bundle:nil];
11. svc.str = self.navigationItem.title;
12. [svc setChangeText_block:^(NSString *str) {
13. >self.navigationItem.title = str;
14. }];
15. [self.navigationController pushViewController:svc animated:YES];
16. }
第五种:extern传值
1. 第二个控制器: extern NSString *btn;
2. - (IBAction)buttonClick:(UIButton*)sender {
3. btn = sender.titleLabel.text;
4. [self.navigationController popViewControllerAnimated:YES];
5. }
6.
7. 第一个控制器: NSString *btn = nil;
8. - (void)viewWillAppear:(BOOL)animated{
9. [super viewWillAppear:animated]; self.navigationItem.title = btn;
10. }
第六种:KVO传值
1. 第一个控制器:
2.
3. - (void)viewDidLoad {
4. [super viewDidLoad];
5. _vc =[[SecondViewController alloc]init]; //self监听vc里的textValue属性 [_vc addObserver:self forKeyPath:@"textValue" options:0 context:nil];
6. }
7.
8. 第二个控制器:
9.
10. - (IBAction)buttonClicked:(id)sender { self.textValue = self.textField.text;
11. [self.navigationController popViewControllerAnimated:YES];
12. }
78. 浅谈iOS开发中方法延迟执行的几种方式
Method1. performSelector方法
Method2. NSTimer定时器
Method3. NSThread线程的sleep
Method4. GCD
公用延迟执行方法
- (void)delayMethod{ NSLog(@"delayMethodEnd");
Method1: performSelector
1. [self performSelector:@selector(delayMethod) withObject:nil/*可传任意类型参数*/ afterDelay:2.0];`
2. 注:此方法是一种非阻塞的执行方式,未找到取消执行的方法。
3.
4. > 程序运行结束
5. > 2015-08-31 10:56:59.361 CJDelayMethod[1080:39604] delayMethodStart2015-08-31 10:56:59.363 CJDelayMethod[1080:39604] nextMethod2015-08-31 10:57:01.364 CJDelayMethod[1080:39604] delayMethodEnd
Method2: NSTimer定时器
1. NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(delayMethod) userInfo:nil repeats:NO];`
2. 注:此方法是一种非阻塞的执行方式,
3. 取消执行方法:`- (void)invalidate;`即可
4.
5. > 程序运行结束
6. > 2015-08-31 10:58:10.182 CJDelayMethod[1129:41106] delayMethodStart2015-08-31 10:58:10.183 CJDelayMethod[1129:41106] nextMethod2015-08-31 10:58:12.185 CJDelayMethod[1129:41106] delayMethodEnd
Method3: NSThread线程的sleep
1. [NSThread sleepForTimeInterval:2.0];
2. 注:此方法是一种阻塞执行方式,建议放在子线程中执行,否则会卡住界面。但有时还是需要阻塞执行,如进入欢迎界面需要沉睡3秒才进入主界面时。
3. 没有找到取消执行方式。
4.
5. > 程序运行结束
6. > 2015-08-31 10:58:41.501 CJDelayMethod[1153:41698] delayMethodStart2015-08-31 10:58:43.507 CJDelayMethod[1153:41698] nextMethod
Method4: GCD
1. __block ViewController/*主控制器*/ *weakSelf = self; dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0/*延迟执行时间*/ * NSEC_PER_SEC));
2.
3. dispatch_after(delayTime, dispatch_get_main_queue(), ^{
4. [weakSelf delayMethod];
5. });
6.
7. 注:此方法可以在参数中选择执行的线程,是一种非阻塞执行方式。没有找到取消执行方式。
8.
9. > 程序运行结束
10. > 2015-08-31 10:59:21.652 CJDelayMethod[1181:42438] delayMethodStart2015-08-31 10:59:21.653 CJDelayMethod[1181:42438] nextMethod2015-08-31 10:59:23.653 CJDelayMethod[1181:42438] delayMethodEnd
1. 完整代码参见:
2.
3. > // > // ViewController.m > // CJDelayMethod > // > // Created by 陈杰 on 8/31/15. > // Copyright (c) 2015 chenjie. All rights reserved. > // >
4. > # import "ViewController.h" >
5. > @interface ViewController () > @property (nonatomic, strong) NSTimer *timer;
6. > @end > @implementation ViewController* >
7. > *`- (void)viewDidLoad { `*
8. >
9. > *` [super viewDidLoad]; `*
10. >
11. > *` NSLog(@"delayMethodStart"); `*
12. >
13. > *` [self methodOnePerformSelector];// `* >
14. > *` [self methodTwoNSTimer];// `* >
15. > *` [self methodThreeSleep];//`* >
16. > *` [self methodFourGCD]; `*
17. >
18. > *` NSLog(@"nextMethod");`*
19. >
20. > *`}`*
21. >
22. > *`- (void)methodFiveAnimation{ `*
23. >
24. > *` [UIView animateWithDuration:0 delay:2.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{ } completion:^(BOOL finished) { `*
25. >
26. > *` [self delayMethod]; `*
27. >
28. > *` }];`*
29. >
30. > *`}`
31. > `- (void)methodFourGCD{ `*
32. >
33. > *` __block ViewController`*`weakSelf = self; dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)); dispatch_after(delayTime, dispatch_get_main_queue(), ^{ `
34. >
35. > ` [weakSelf delayMethod]; `
36. >
37. > ` });`
38. >
39. > `}`
40. >
41. > `- (void)methodThreeSleep{ `
42. >
43. > ` [NSThread sleepForTimeInterval:2.0];`
44. >
45. > `}`
46. >
47. > `- (void)methodTwoNSTimer{`
48. >
49. > ` NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(delayMethod) userInfo:nil repeats:NO];`
50. >
51. > `}`
52. >
53. > `- (void)methodOnePerformSelector{`
54. >
55. > ` [self performSelector:@selector(delayMethod) withObject:nil/*可传任意类型参数*/ afterDelay:2.0];`
56. >
57. > `}`
58. >
59. > `- (void)delayMethod{`
60. >
61. > ` NSLog(@"delayMethodEnd");`
62. >
63. > `}`
64. > `- (void)didReceiveMemoryWarning { `
65. >
66. > ` [super didReceiveMemoryWarning]; `
67. >
68. > ` // Dispose of any resources that can be recreated.` >
69. > `}`
70. >
71. > `@end`
NSPersistentStoreCoordinator , NSManaged0bjectContext 和NSManaged0bject中的那些需要在线程中创建或者传递
答:NSPersistentStoreCoordinator是持久化存储协调者,主要用于协调托管对象上下文和持久化存储区之间的关系。NSManagedObjectContext使用协调者的托管对象模型将数据保存到数据库,或查询数据。
您是否做过一部的网络处理和通讯方面的工作?如果有,能具体介绍一下实现策略么?
答:使用NSOperation发送异步网络请求,使用NSOperationQueue管理线程数目及优先级,底层是用NSURLConnetion
你使用过Objective-C的运行时编程(Runtime Programming)么?如果使用过,你用它做了什么?你还能记得你所使用的相关的头文件或者某些方法的名称吗?
答:Objecitve-C的重要特性是Runtime(运行时),在#import 下能看到相关的方法,用过objc_getClass()和class_copyMethodList()获取过私有API;使用
Method method1 = class_getInstanceMethod(cls, sel1);
Method method2 = class_getInstanceMethod(cls, sel2);
method_exchangeImplementations(method1, method2);
代码交换两个方法,在写unit test时使用到。
Core开头的系列的内容。是否使用过CoreAnimation和CoreGraphics。UI框架和CA,CG框架的联系是什么?分别用CA和CG做过些什么动画或者图像上的内容。(有需要的话还可以涉及Quartz的一些内容)
答:UI框架的底层有CoreAnimation,CoreAnimation的底层有CoreGraphics。
UIKit |
---|
Core Animation |
Core Graphics |
Graphics Hardware |
使用CA做过menu菜单的展开收起(太逊了) |
是否使用过CoreText或者CoreImage等?如果使用过,请谈谈你使用CoreText或者CoreImage的体验。
答:CoreText可以解决复杂文字内容排版问题。CoreImage可以处理图片,为其添加各种效果。体验是很强大,挺复杂的。
自动释放池是什么,如何工作
答:当您向一个对象发送一个autorelease消息时,Cocoa就会将该对象的一个引用放入到最新的自动释放.它仍然是个OC的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放。
NSNotification和KVO的区别和用法是什么?什么时候应该使用通知,什么时候应该使用KVO,它们的实现上有什么区别吗?如果用protocol和delegate(或者delegate的Array)来实现类似的功能可能吗?如果可能,会有什么潜在的问题?如果不能,为什么?(虽然protocol和delegate这种东西面试已经面烂了…)
答:NSNotification是通知模式在iOS的实现,KVO的全称是键值观察(Key-value observing),其是基于KVC(key-value coding)的,KVC是一个通过属性名访问属性变量的机制。例如将Module层的变化,通知到多个Controller对象时,可以使用NSNotification;如果是只需要观察某个对象的某个属性,可以使用KVO。
对于委托模式,在设计模式中是对象适配器模式,其是delegate是指向某个对象的,这是一对一的关系,而在通知模式中,往往是一对多的关系。委托模式,从技术上可以现在改变delegate指向的对象,但不建议这样做,会让人迷惑,如果一个delegate对象不断改变,指向不同的对象。
你用过NSOperationQueue么?如果用过或者了解的话,你为什么要使用NSOperationQueue,实现了什么?请描述它和G.C.D的区别和类似的地方(提示:可以从两者的实现机制和适用范围来描述)。
答:使用NSOperationQueue用来管理子类化的NSOperation对象,控制其线程并发数目。GCD和NSOperation都可以实现对线程的管理,区别是 NSOperation和NSOperationQueue是多线程的面向对象抽象。项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中使用。
项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议在简单项目中使用。
既然提到G.C.D,那么问一下在使用G.C.D以及block时要注意些什么?它们两是一回事儿么?block在ARC中和传统的MRC中的行为和用法有没有什么区别,需要注意些什么?
答:使用block是要注意,若将block做函数参数时,需要把它放到最后,GCD是Grand Central Dispatch,是一个对线程开源类库,而Block是闭包,是能够读取其他函数内部变量的函数。
对于Objective-C,你认为它最大的优点和最大的不足是什么?对于不足之处,现在有没有可用的方法绕过这些不足来实现需求。如果可以的话,你有没有考虑或者实践过重新实现OC的一些功能,如果有,具体会如何做?
答:最大的优点是它的运行时特性,不足是没有命名空间,对于命名冲突,可以使用长命名法或特殊前缀解决,如果是引入的第三方库之间的命名冲突,可以使用link命令及flag解决冲突。
你实现过一个框架或者库以供别人使用么?如果有,请谈一谈构建框架或者库时候的经验;如果没有,请设想和设计框架的public的API,并指出大概需要如何做、需要注意一些什么方面,来使别人容易地使用你的框架。
答:抽象和封装,方便使用。首先是对问题有充分的了解,比如构建一个文件解压压缩框架,从使用者的角度出发,只需关注发送给框架一个解压请求,框架完成复杂文件的解压操作,并且在适当的时候通知给是哦难过者,如解压完成、解压出错等。在框架内部去构建对象的关系,通过抽象让其更为健壮、便于更改。其次是API的说明文档。
二、 第三方框架
AFNetworking 底层原理分析
AFNetworking主要是对NSURLSession和NSURLConnection(iOS9.0废弃)的封装,其中主要有以下类:
1). AFHTTPRequestOperationManager:内部封装的是 NSURLConnection, 负责发送网络请求, 使用最多的一个类。(3.0废弃)
2). AFHTTPSessionManager:内部封装是 NSURLSession, 负责发送网络请求,使用最多的一个类。
3). AFNetworkReachabilityManager:实时监测网络状态的工具类。当前的网络环境发生改变之后,这个工具类就可以检测到。
4). AFSecurityPolicy:网络安全的工具类, 主要是针对 HTTPS 服务。
5). AFURLRequestSerialization:序列化工具类,基类。上传的数据转换成JSON格式
(AFJSONRequestSerializer).使用不多。
6). AFURLResponseSerialization:反序列化工具类;基类.使用比较多:
7). AFJSONResponseSerializer; JSON解析器,默认的解析器.
8). AFHTTPResponseSerializer; 万能解析器; JSON和XML之外的数据类型,直接返回二进
制数据.对服务器返回的数据不做任何处理.
9). AFXMLParserResponseSerializer; XML解析器;
描述下SDWebImage里面给UIImageView加载图片的逻辑
SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前会先显示占位图片,当真实图片被加载出来后再替换占位图片。
加载图片的过程大致如下:
1.首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存
2.如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来
3.如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片
4.下载后的图片会加入缓存中,并写入磁盘中
5.整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来
SDWebImage原理:
调用类别的方法:
- 从内存(字典)中找图片(当这个图片在本次使用程序的过程中已经被加载过),找到直接使用。
- 从沙盒中找(当这个图片在之前使用程序的过程中被加载过),找到使用,缓存到内存中。
- 从网络上获取,使用,缓存到内存,缓存到沙盒。
友盟统计接口统计的所有功能
APP启动速度,APP停留页面时间等
三、算法
1.不用中间变量,用两种方法交换A和B的值
1. // 1.中间变量 void swap(int a, int b) { int temp = a;
2. a = b;
3. b = temp;
4. } // 2.加法 void swap(int a, int b) {
5. a = a + b;
6. b = a - b;
7. a = a - b;
8. } // 3.异或(相同为0,不同为1\. 可以理解为不进位加法) void swap(int a, int b) {
9. a = a ^ b;
10. b = a ^ b;
11. a = a ^ b;
12. }
2.求最大公约数
1. /** 1.直接遍历法 */ int maxCommonDivisor(int a, int b) { int max = 0; for (int i = 1; i <=b; i++) { if (a % i == 0 && b % i == 0) {
2. max = I;
3. }
4. } return max;
5. } /** 2.辗转相除法 */ int maxCommonDivisor(int a, int b) { int r; while(a % b > 0) {
6. r = a % b;
7. a = b;
8. b = r;
9. } return b;
10. } // 扩展:最小公倍数 = (a * b)/最大公约数
3.模拟栈操作
1. /**
2. * 栈是一种数据结构,特点:先进后出
3. * 练习:使用全局变量模拟栈的操作
4. */ #include #include #include //保护全局变量:在全局变量前加static后,这个全局变量就只能在本文件中使用 static int data[1024];//栈最多能保存1024个数据 static int count = 0;//目前已经放了多少个数(相当于栈顶位置) //数据入栈 push void push(int x){
5. assert(!full());//防止数组越界 data[count++] = x;
6. } //数据出栈 pop int pop(){
7. assert(!empty()); return data[--count];
8. } //查看栈顶元素 top int top(){
9. assert(!empty()); return data[count-1];
10. } //查询栈满 full bool full() { if(count >= 1024) { return 1;
11. } return 0;
12. } //查询栈空 empty bool empty() { if(count <= 0) { return 1;
13. } return 0;
14. } int main(){ //入栈 for (int i = 1; i <= 10; i++) {
15. push(i);
16. } //出栈 while(!empty()){ printf("%d ", top()); //栈顶元素 pop(); //出栈 } printf("\n"); return 0;
17. }
4.排序算法
1. 选择排序、冒泡排序、插入排序三种排序算法可以总结为如下:
2.
3. * 都将数组分为已排序部分和未排序部分。 1\. 选择排序将已排序部分定义在左端,然后选择未排序部分的最小元素和未排序部分的第一个元素交换。 2\. 冒泡排序将已排序部分定义在右端,在遍历未排序部分的过程执行交换,将最大元素交换到最右端。 3\. 插入排序将已排序部分定义在左端,将未排序部分元的第一个元素插入到已排序部分合适的位置。
选择排序*
【选择排序】:最值出现在起始端 第1趟:在n个数中找到最小(大)数与第一个数交换位置 第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置 重复这样的操作...依次与第三个、第四个...数交换位置 第n-1趟,最终可实现数据的升序(降序)排列。
void selectSort(int * arr, int length) {
for (int i = 0; i < length - 1; i++) {
//趟数 for (int j = i + 1; j < length; j++) { //比较次数
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
冒泡排序 【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾 第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置 第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置 第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置
void bublleSort(int * arr, int length) {
for (int i = 0; i < length - 1; i++) {
//趟数 for(int j = 0; j < length - i - 1; j++) {
//比较次数 if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
5.折半查找(二分查找)
1. /**
2. * 折半查找:优化查找时间(不用遍历全部数据)
3. *
4. * 折半查找的原理:
5. * 1> 数组必须是有序的
6. * 2> 必须已知min和max(知道范围)
7. * 3> 动态计算mid的值,取出mid对应的值进行比较
8. * 4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
9. * 5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
10. *
11. */ // 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置 int findKey(int *arr, int length, int key) { int min = 0, max = length - 1, mid; while (min <= max) {
12. mid = (min + max) / 2; //计算中间值 if (key > arr[mid]) {
13. min = mid + 1;
14. } else if (key < arr[mid]) {
15. max = mid - 1;
16. } else { return mid;
17. }
18. } return -1;
19. }
四、编码格式(优化细节)
1.在 Objective-C 中,enum 建议使用NS_ENUM和NS_OPTIONS宏来定义枚举类型。
1. //定义一个枚举(比较严密) typedef NS_ENUM(NSInteger, BRUserGender) {
2. BRUserGenderUnknown, // 未知 BRUserGenderMale, // 男性 BRUserGenderFemale, // 女性 BRUserGenderNeuter // 无性 }; @interface BRUser : NSObject @property (nonatomic, readonly, copy) NSString *name; @property (nonatomic, readonly, assign) NSUInteger age; @property (nonatomic, readonly, assign) BRUserGender gender;
3.
4. - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age gender:(BRUserGender)gender; @end //说明: //既然该类中已经有一个“初始化方法” ,用于设置 name、age 和 gender 的初始值: 那么在设计对应 @property 时就应该尽量使用不可变的对象:其三个属性都应该设为“只读”。用初始化方法设置好属性值之后,就不能再改变了。 //属性的参数应该按照下面的顺序排列: (原子性,读写,内存管理)
2.避免使用C语言中的基本数据类型,建议使用 Foundation 数据类型,对应关系如下:
int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
动画时间 -> NSTimeInterval
五、其它知识点
HomeKit,是苹果2014年发布的智能家居平台。
什么是 OpenGL、Quartz 2D?
Quatarz 2d 是Apple提供的基本图形工具库。只是适用于2D图形的绘制。
OpenGL,是一个跨平台的图形开发库。适用于2D和3D图形的绘制。
ffmpeg框架:
ffmpeg 是音视频处理工具,既有音视频编码解码功能,又可以作为播放器使用。
谈谈 UITableView 的优化
1). 正确的复用cell;
2). 设计统一规格的Cell;
3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;
4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
5). 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!
6). 减少子视图的层级关系;
7). 尽量使所有的视图不透明化以及做切圆操作;
8). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示;
9). 使用调试工具分析问题。
如何实行cell的动态的行高
如果希望每条数据显示自身的行高,必须设置两个属性,1.预估行高,2.自定义行高。
设置预估行高 tableView.estimatedRowHeight = 200。
设置定义行高 tableView.estimatedRowHeight = UITableViewAutomaticDimension。
如果要让自定义行高有效,必须让容器视图有一个自下而上的约束。
说说你对 block 的理解
栈上的自动复制到堆上,block 的属性修饰符是 copy,循环引用的原理和解决方案。
block的循环引用;block的代码实现;为什么会造成循环引用;block是如何强引用self的;
什么是野指针、空指针?
野指针:不知道指向了哪里的指针叫野指针。即指针指向不确定,指针存的地址是一个垃圾值,未初始化。
空指针:不指向任何位置的指针叫空指针。即指针没有指向,指针存的地址是一个空地址,NULL。
什么是 OOA / OOD / OOP ?
OOA(Object Oriented Analysis) --面向对象分析
OOD(Object Oriented Design) --面向对象设计
OOP(Object Oriented Programming)--面向对象编程
多线程是什么
多线程是个复杂的概念,按字面意思是同步完成多项任务,提高了资源的使用效率,从硬件、操作系统、应用软件不同的角度去看,多线程被赋予不同的内涵,对于硬件,现在市面上多数的CPU都是多核的,多核的CPU运算多线程更为出色;从操作系统角度,是多任务,现在用的主流操作系统都是多任务的,可以一边听歌、一边写博客;对于应用来说,多线程可以让应用有更快的回应,可以在网络下载时,同时响应用户的触摸操作。在iOS应用中,对多线程最初的理解,就是并发,它的含义是原来先做烧水,再摘菜,再炒菜的工作,会变成烧水的同时去摘菜,最后去炒菜。
iOS 中的多线程
iOS中的多线程,是Cocoa框架下的多线程,通过Cocoa的封装,可以让我们更为方便的使用线程,做过C++的同学可能会对线程有更多的理解,比如线程的创立,信号量、共享变量有认识,Cocoa框架下会方便很多,它对线程做了封装,有些封装,可以让我们创建的对象,本身便拥有线程,也就是线程的对象化抽象,从而减少我们的工程,提供程序的健壮性。
GCD是(Grand Central Dispatch)的缩写 ,从系统级别提供的一个易用地多线程类库,具有运行时的特点,能充分利用多核心硬件。GCD的API接口为C语言的函数,函数参数中多数有Block,关于Block的使用参看这里,为我们提供强大的“接口”,对于GCD的使用参见本文
NSOperation与Queue
NSOperation是一个抽象类,它封装了线程的细节实现,我们可以通过子类化该对象,加上NSQueue来同面向对象的思维,管理多线程程序。具体可参看这里:一个基于NSOperation的多线程网络访问的项目。
NSThread
NSThread是一个控制线程执行的对象,它不如NSOperation抽象,通过它我们可以方便的得到一个线程,并控制它。但NSThread的线程之间的并发控制,是需要我们自己来控制的,可以通过NSCondition实现。
参看 iOS多线程编程之NSThread的使用
其他多线程
在Cocoa的框架下,通知、Timer和异步函数等都有使用多线程,(待补充).
在项目什么时候选择使用GCD,什么时候选择NSOperation?
项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中使用。
项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议在简单项目中使用。
KVO,NSNotification,delegate及block区别
- KVO就是cocoa框架实现的观察者模式,一般同KVC搭配使用,通过KVO可以监测一个值的变化,比如View的高度变化。是一对多的关系,一个值的变化会通知所有的观察者。
- NSNotification是通知,也是一对多的使用场景。在某些情况下,KVO和NSNotification是一样的,都是状态变化之后告知对方。NSNotification的特点,就是需要被观察者先主动发出通知,然后观察者注册监听后再来进行响应,比KVO多了发送通知的一步,但是其优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用也更灵活。
- delegate 是代理,就是我不想做的事情交给别人做。比如狗需要吃饭,就通过delegate通知主人,主人就会给他做饭、盛饭、倒水,这些操作,这些狗都不需要关心,只需要调用delegate(代理人)就可以了,由其他类完成所需要的操作。所以delegate是一对一关系。
- block是delegate的另一种形式,是函数式编程的一种形式。使用场景跟delegate一样,相比delegate更灵活,而且代理的实现更直观。
- KVO一般的使用场景是数据,需求是数据变化,比如股票价格变化,我们一般使用KVO(观察者模式)。delegate一般的使用场景是行为,需求是需要别人帮我做一件事情,比如买卖股票,我们一般使用delegate。
Notification一般是进行全局通知,比如利好消息一出,通知大家去买入。delegate是强关联,就是委托和代理双方互相知道,你委托别人买股票你就需要知道经纪人,经纪人也不要知道自己的顾客。Notification是弱关联,利好消息发出,你不需要知道是谁发的也可以做出相应的反应,同理发消息的人也不需要知道接收的人也可以正常发出消息。
将一个函数在主线程执行的4种方法
1. * GCD方法,通过向主线程队列发送一个block块,使block里的方法可以在主线程中执行。 dispatch_async(dispatch_get_main_queue(), ^{ //需要执行的方法 });
2.
3.
4. * NSOperation 方法 NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主队列 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ //需要执行的方法 }];
5. [mainQueue addOperation:operation];
6.
7.
8. * NSThread 方法
9.
10. [self performSelector:@selector(method) onThread:[NSThread mainThread] withObject:nil waitUntilDone:YES modes:nil];
11.
12. [self performSelectorOnMainThread:@selector(method) withObject:nil waitUntilDone:YES];
13.
14. [[NSThread mainThread] performSelector:@selector(method) withObject:nil];
15.
16.
17. * RunLoop方法
18.
19. [[NSRunLoop mainRunLoop] performSelector:@selector(method) withObject:nil];
如何让计时器调用一个类方法
- 计时器只能调用实例方法,但是可以在这个实例方法里面调用静态方法。
- 使用计时器需要注意,计时器一定要加入RunLoop中,并且选好model才能运行。scheduledTimerWithTimeInterval方法创建一个计时器并加入到RunLoop中所以可以直接使用。
- 如果计时器的repeats选择YES说明这个计时器会重复执行,一定要在合适的时机调用计时器的invalid。不能在dealloc中调用,因为一旦设置为repeats 为yes,计时器会强持有self,导致dealloc永远不会被调用,这个类就永远无法被释放。比如可以在viewDidDisappear中调用,这样当类需要被回收的时候就可以正常进入dealloc中了。
1. [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
2.
3. -(void)timerMethod
4. { //调用类方法 [[self class] staticMethod];
5. }
6.
7. -(void)invalid
8. {
9. [timer invalid];
10. timer = nil;
11. }
如何重写类方法
○1、在子类中实现一个同基类名字一样的静态方法
○2、在调用的时候不要使用类名调用,而是使用[self class]的方式调用。原理,用类名调用是早绑定,在编译期绑定,用[self class]是晚绑定,在运行时决定调用哪个方法。
NSTimer创建后,会在哪个线程运行
○用scheduledTimerWithTimeInterval创建的,在哪个线程创建就会被加入哪个线程的RunLoop中就运行在哪个线程
○自己创建的Timer,加入到哪个线程的RunLoop中就运行在哪个线程。
id和NSObject*的区别
○id是一个 objc_object 结构体指针,定义是
typedef struct objc_object *id
○id可以理解为指向对象的指针。所有oc的对象 id都可以指向,编译器不会做类型检查,id调用任何存在的方法都不会在编译阶段报错,当然如果这个id指向的对象没有这个方法,该崩溃还是会崩溃的。
○NSObject *指向的必须是NSObject的子类,调用的也只能是NSObjec里面的方法否则就要做强制类型转换。
○不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。NSObject *可指向的类型是id的子集。
static关键字的作用
回答一:
1.在函数体内定义的static他的作用域为该函数体,该变量在内存中只被分配一次,因此,其值在下次调用时仍维持上次的值不变;
2.在模块内的static全局变量可以被模块内所有函数访问,但是不能被模块外的其他函数访问;
3.在模块内的staic全局变量可以被这一模块内的其他函数调用,这个函数的使用范围被限制在这个模块内;
4.在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝,也就是说只要是该类的对象,那么该对象的中被static修饰的成员变量都指向同一块地址。
回答二:
修饰局部变量:
1.延长局部变量的生命周期,程序结束才会销毁。
2.局部变量只会生成一份内存,只会初始化一次。
3.改变局部变量的作用域。
修饰全局变量:
1.只能在本文件中访问,修改全局变量的作用域,生命周期不会改
2.避免重复定义全局变量
在OC中static关键字使用误区
1.使用static修饰实例变量是不被允许的
2.使用static修饰了方法,也是错误的
参考:
如何正确使用const,static,extern
OC中 static 与 const 的作用
使用 Swift 语言编程的优缺点 总的来说,我认为使用 Swift 来作为编程语言的优点还是要远远大于缺点的,而且很多缺点苹果也在逐渐改善。
优点:
1、简洁的语法
2、更强的类型安全
3、函数式编程的支持
Swift 语言本身提供了对函数式编程的支持。
Objc 本身是不支持的,但是可以通过引入 ReactiveCocoa 这个库来支持函数式编程。
4、编写 OS X 下的自动化脚本
缺点
1、App体积变大
使用Swift 后, App 体积大概增加 5-8 M 左右,对体积大小敏感的慎用。
体积变大的原因是因为 Swift 还在变化,所以 Apple 没有在 iOS 系统里放入 Swift 的运行库,反而是每个 App 里都要包含其对应的 Swift 运行库。
2、Xcode 支持不够好
如果你是使用 Xcode经常卡住或者崩溃想必你是肯定碰到过了,这个是目前使用 Swift 最让人头疼的事情,即使是到现在XCode 9, 有时候也会遇到这种问题,所以要看你的承受力了……
3、第三方库的支持不够多
目前确实 Swift 编写的第三方库确实不多,但可以通过桥接的方式来使用 Objc 的三方库,基本上没有太大问题。现在已经改善很多了…
4、语言版本更新带来的编译问题
语言本身还在发展,所以每次版本更新后都会出现编译不过的情况(至少到目前为止还是),但是自从 4.0 版本发布后,改动没有 beta 时候那么大了,而且根据 Xcode 提示基本就可以解决语法变动导致的编译问题了。
青山不改,绿水常流
iOS Swift开发面试题总结
Swift 优点 (相对 OC)
- Swift 更加安全,是类型安全的语言
- 代码少,语法简洁,可以省去大量冗余代码
- Swift 速度更快,运算性能更高,(Apple 专门对编译器进行了优化)
Swift 中 类(class) 和 结构体(struct) 的区别,以及各自优缺点?
- 类:
-
- 引用类型
-
-
- 在进行变量赋值时,是通过指针copy,属于浅拷贝(shallow copy)
- 数据的存储是在堆空间
-
-
- 可以被继承(前提是类没有被 final 关键字修饰),子类可以使用父类的属性和方法
- (当class继承自 Object,拥有runtime机制)类型转换可以在运行时检查和解释一个实例对象
- 用 deinit(析构函数)来释放资源 类似OC(dealloc)
- 类的方法地址 是不确定的,只有在具体运行时,才能确定调用的具体值
- 结构体
-
- 值类型
-
-
- 在进行变量赋值是,是深拷贝(deep copy),产生了新的副本
- 数据的存储时在栈空间(大部分情况下,不需要考虑内存泄露问题,栈空间的特点是用完即释放)
-
-
- 结构体调用方法,在编译完成就可以确定方法具体的地址值,以便直接调用
综上,在满足程序要求的情况下 优先使用 结构体
Swift中strong 、weak和unowned是什么意思?二者有什么不同?何时使用unowned?
Swift 的内存管理机制与 Objective-C一样为 ARC(Automatic Reference Counting)。它的基本原理是,一个对象在没有任何强引用指向它时,其占用的内存会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中。
-
strong
代表着强引用,是默认属性。当一个对象被声明为strong
时,就表示父层级对该对象有一个强引用的指向。此时该对象的引用计数会增加1。 -
weak
代表着弱引用。当对象被声明为weak
时,父层级对此对象没有指向,该对象的引用计数不会增加1。它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不亏崩溃 -
unowned
与弱引用本质上一样。不同的是,unowned 无主引用 实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained), 试图在实例销毁后访问无主引用,会产生运行时错误(野指针) -
weak
unowned
只能用在 类实例上面 -
weak
、unowned
都能解决 循环引用,unowned
要比weak
性能 稍高
-
- 在生命周期中可能会 变成
nil
的使用weak
- 初始化赋值以后再也不会变成
nil
使用unowned
- 在生命周期中可能会 变成
Swift 中什么是可选类型?
- Swift中可选类型为了表示 一个变量 允许为 空(nil)的情况
- 类型名称后 加
?
定义 可选项 - 可
选项的本质
是 枚举类型
Swift 中什么 是 泛型?
- 跟JS和Dart 类似,泛型 可以将类型 参数化,提高代码复用率,减少代码量
- Swift泛型函数 并不会 在底层 生成 若干个 (匹配类型)函数 ,产生函数重载,而是: 在函数调用时,会将 参数的类型 传递给 目标函数
- Swift泛型应用在协议上时,需要使用关联类型(associatedtype)
怎么理解 Swift中的泛型约束
泛型约束 可以 更精确的知道 参数 需要 遵循什么标准
//someT遵循的是某个class,someU遵循的是某个协议,这样在传参的时候明确参数类型
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}
Swift 中 static 和 class 关键字的区别
在 Swift 中 static
和 class
都是表示「类型范围作用域」的关键字。
在所有类型(class[类]、struct、enum )中使用
-
static
修饰都可以表示类方法类与属性(包括存储属性和计算属性)。 -
class
是专门用在calss 类型
中修饰类方法和类的计算属性(注意:无法使用 class 修饰存储属性)。
结构体只能用 static 修饰 类方法或属性
在class类型
中 static
和 class
的区别
在 class 类型
中 static
和 class
都可以表示类型范围作用域,那区别是
-
class
无法修饰存储属性,而 static 可以。 - 使用 static 修饰的类方法和类属性无法在子类中重载。也就是说 static 修饰的类方法和类属性包含了 final 关键字的特性。相当于 final class 。
一般在 protocol定义一个类方法或者类计算属性推荐使用 static
关键字来修饰。使用 protocol 时,在 struct 和 enum 中仍然使用 static,在 class 类型中 class 和 static 关键字都可以使用。
Swift 中的模式匹配?
模式 是用于 匹配的规则,比如 switch 的 case 、捕捉错误的 catch 、 if guard while for 语句条件等
Swift 中的访问控制?
Swift 提供了 5个 不同的访问级别,从高到低排列如下:
open
-
- 允许在任意模块中访问、继承、重写
- open 只能用在 类 、 类成员上
public
-
- 允许在任意模块中访问
- 修饰类时不允许其他模块进行继承、重写
-
internal
- 默认
-
- 只允许在定义实体的模块中访问,不允许在其他模块中访问
fileprivate
-
- 只允许在定义实体的源文件中访问
-
private
:
-
- 只允许在定义实体的封闭声明中(作用域)访问
怎么理解 copy - on - write? 或者 理解Swift中的写时复制
值类型在复制时,复制对象 与 原对象 实际上在内存中指向同一个对象,**当且仅当 ** 修改复制的对象时,才会在内存中创建一个新的对象,
- 为了提升性能,Struct, String、Array、Dictionary、Set采取了Copy On Write的技术
- 比如仅当有“写”操作时,才会真正执行拷贝操作
- 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
原理
在结构体内部用一个引用类型来存储实际的数据,
- 在不进行写入操作的普通传递过程中,都是将内部的reference的应用计数+1,
- 当进行写入操作时,对内部的 reference 做一次 copy 操作用来存储新的数据;防止和之前的reference产生意外的数据共享。
swift中提供[isKnownUniquelyReferenced
]函数,他能检查一个类的实例是不是唯一的引用,如果是,我们就不需要对结构体实例进行复制,如果不是,说明对象被不同的结构体共享,这时对它进行更改就需要进行复制。
Swift 为什么将 Array,String,Dictionary,Set,设计为值类型?
值类型 相比 引用类型的优点
- 值类型和引用类型相比,最大优势可以高效的使用内存;
- 值类型在栈上操作,引用类型在堆上操作;
- 栈上操作仅仅是单个指针的移动,
- 堆上操作牵涉到合并,位移,重链接
Swift 这样设计减少了堆上内存分配和回收次数,使用 copy-on-write将值传递与复制开销降到最低
String,Array,Dictionary设计成值类型,也是为了线程安全考虑。通过Swift的let设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程中内存访问和操作顺序的问题
什么是属性观察器?
属性观察是指在当前类型内对特性属性进行监测,并作出响应,属性观察是 swift 中的特性,具有2种, willset
和 didset
-
willSet 传递新值 newValue
-
didSet 传递旧值 oldValue
-
在初始化器中对属性初始化时,不会触发观察器
-
属性观察器 只能用在 存储属性 ,不可用在 计算属性
-
可以 为 非
lazy
(即延迟存储属性)的var
存储属性 设置 属性观察器
-
willSet
会传递新值,默认叫 newValue -
didSet
会传递旧值,默认叫 oldValue
注意:
- 在初始化器中设置属性值不会触发 属性观察器
-
- 属性定义时设置初始值也不会出发 属性观察,原因是
-
-
- 属性定义时设置初始值,本质跟在 初始化器中设置值是一样的
-
- 属性观察器 只能用在 存储属性 ,不可用在 计算属性
struct Cicle {
/// 存储属性
var radius :Double {
willSet {
print("willSet -- ",newValue,"radius == ",radius)
}
didSet {
print("didSet ++ ",oldValue,"radius == ",radius)
}
}
/*
上述代码 跟下面等价,不推荐
var radius :Double {
willSet(jk_newValue) {
print("willSet -- ",jk_newValue,"radius == ",radius)
}
didSet(jk_oldValue) {
print("didSet ++ ",jk_oldValue,"radius == ",radius)
}
}
*/
}
var circle = Cicle.init(radius: 10.0)
circle.radius = 20.0
/*
willSet -- 20.0 radius == 10.0
didSet ++ 10.0 radius == 20.0
*/
print("result == ",circle.radius)
//result == 20.0
拓展:
●属性观察器,计算属性这两个功能,同样可以应用在全局变量/局部变量
●属性观察器,计算属性 不可以同时 应用在同一个类(不包括继承)的属性中
Swift 异常捕获
do - try - catch 机制
- Swift中可以通过
Error
协议自定义运行时的错误信息 - 函数内部通过
throw
抛出 自定义Error
, 抛出 Error 的函数必须加上throws
声明(逻辑通过不会抛出,反之可能抛出) - 需要使用
try
调用 可能会 抛出 的Error
函数 - 通过 try 尝试调用 函数 抛出的异常 必须要 处理异常;否在会编译报错; 反之 运行时 在 top level (main) 报错,闪退
defer 的用法
- 使用defer代码块来表示在函数返回前,函数中最后执行的代码。无论函数是否会抛出错误,这段代码都将执行。
如何将Swift中协议 部分方法 设计成可选?
- 方案一(不推荐,除非需要暴露给OC用)
-
- 在协议和方法前面添加
@objc
,然后在方法前面添加optional
关键字,改方式实际上是将协议转为了OC的方式
- 在协议和方法前面添加
@objc protocol someProtocol {
@objc optional func test()
}
协议 可以 用来定义 属性 方法 下标
的 声明,协议 可以被 类 结构体 枚举
遵守(多个协议之间用,
隔开)
protocol Drawable {
func draw()
var x:Int { get set }
var y:Int { get }
subscript(index:Int) -> Int {get set}
}
protocol Test1 {}
protocol Test2 {}
protocol Test3 {}
class TestClass: Test1,Test2,Test3 {
}
注意:
- 协议中定义的方法,不能有默认参数
- 默认情况下,协议中定义的内容需要全部实现
Swift和OC中的 protocol 有什么不同?
- 相同点,两者都是用作代理
- 不同点
-
- Swift
-
-
- Swift中的 protocol 还可以对接口进行抽象,可以实现面向协议,从而大大提高编程效率
- Swift中的 protocol 可以用于值类型,结构体,枚举;
-
比较Swift 和OC中的初始化方法 (init) 有什么不同?
swift 的初始化方法,因为引入了两段式初始化和安全检查因此更加严格和准确,
swift初始化方法需要保证所有的非optional的成员变量都完成初始化,
同时 swfit 新增了convenience和 required两个修饰初始化器的关键字
- convenience只提供一种方便的初始化器,必须通过一个指定初始化器来完成初始化
- required是强制子类重写父类中所修饰的初始化方法
Swift 和OC 中的自省 有什么区别?
- OC
-
- 自省在OC中就是判断某一对象是否属于某一个类的操作,有以下2中方式
-
-
-
[obj iskinOfClass:[SomeClass class]]
: obj 必须是 SomeClass 的 对象或 其子类对象;return YES; -
[obj isMemberOfClass:[SomeClass class]]
: obj 必须是 SomeClass 的 对象;return YES;
-
-
- Swift
-
- Swift 中由于很多 class 并非继承自 NSObject, 故而 Swift 使用
is
来判断是否属于某一类型, is 不仅可以作用于class, 还可以作用于enum和struct
- Swift 中由于很多 class 并非继承自 NSObject, 故而 Swift 使用
Swift 与 OC 如何相互调用
- Swift -> OC
-
- 需要创建一个
Target-BriBridging-Header.h
(默认在OC项目中,会提示自动创建)的桥文件,在该文件中,导入需要调用的OC代码的头文件即可
- 需要创建一个
- OC -> Swift
-
- 直接导入
Target-Swift.h
(该文件是Xcode自动创建) Swift如何需要被OC调用,需要使用@objc
对方法或属性进行修饰
- 直接导入
Swift 中特殊的标记
Swift调用OC
●新建一个桥接文件,文件格式默认为:{targetName}-Bridging-Header.h;(一般在OC项目中,创建Swift文件,Xcode会自动提示生成该文件,仅需点击确认即可)
●在{targetName}-Bridging-Header.h 文件中 #import OC 需要 暴露 给 Swift的内容
OC 调用 Swift
●Xcode 已经默认 生成 一个 用于 OC 调用 Swift的头文件,文件名格式是: {targetName}-Swift.h
●Swift 暴露给 OC的 类 一定要继承 NSObject
●使用 @objc 修饰 需要暴露 给 OC的成员
●使用@objcMembers 修饰类
○代表 默认所有的 成员 都会 暴露给 OC(包括扩展中定义的成员)
○最终 是否成功 暴露,还需要考虑 成员自身的 访问级别
拓展
●为什么Swift 暴露给 OC 的类 要最终 继承 NSObject?
因为 OC 中的方法调用 是通过 Runtime 机制,需要通过 isa 指针 去完成 一些列消息的发送等, 而 只有继承自 NSObject 的类 才具有 isa 指针,才具备 Runtime 消息 发送的能力
●p.run() 底层是如何调用的? 反过来,OC调用Swift 又是如何调用?
○JKPerson 是 OC 的类,以及OC 中定义的初始化 和 run 方法
○在Swift中 调用 JKPerson 对象的 run 方法 ,底层是如何调用的?
Swift复制代码
var p = JKPerson(age: 10,name:"Jack")
p.run()
答:走 Runtime 运行时机制, 反过来 OC 调用 Swift中的类 跟 问题一 一样,也是通过 Runtime 机制
●car.run() 底层是如何调用的?
答 : (虽然 Car 类 被暴露给 OC使用)在Swift中 car.run(),底层依然是 通过 类似 C++ 的虚表 机制 来调用的;
拓展:
如果想要 Swift中的方法 调用 也使用 Runtime 机制,需要在方法名称前面 加上 dynamic关键字
Swift定义常量 和 OC定义常量的区别?
//OC:
const int price = 0;
//Swift:
let price = 0
- OC中 const 常量类型和数值是在编译时确定的
- Swift 中 let 常量(只能赋值一次),其类型和值既可以是静态的,也可以是一个动态的计算方法,它们在运行时确定的。
Swift 中的函数重载
构成函数重载的规则
- 函数名相同
- 参数个数不同 || 参数类型不同 || 参数标签不同
注意: 返回值类型 与函数重载无关;返回值类型不同时,函数重载会报错:
func overloadsum(v1 : Int,v2:Int) -> Int {
v2 + v1
}
// 参数个数不同
func overloadsum(v1 : Int,v2:Int,v3:Int) -> Int {
v2 + v1 + v3
}
// 参数类型不同
func overloadsum(v1 : Int,v2:Double) -> Double {
v2 + Double(v1)
}
// 参数标签不同
func overloadsum(_ v1 : Int,_ v2:Int) -> Int {
v2 + v1
}
func overloadsum(a : Int,_ b:Int) -> Int {
a + b
}
/**
返回值类型不同时,在函数重载时,会报错:
Ambiguous use of 'overloadsum(v1:v2:)'
func overloadsum(v1 : Int,v2:Int) {
}
*/
public func overloadtest() {
let result1 = overloadsum(v1: 10, v2: 20)
let result2 = overloadsum(v1: 10, v2: 20, v3: 30)
let result3 = overloadsum(v1: 10, v2: 20)
let result4 = overloadsum(10, 20)
let resutt4_1 = overloadsum(a: 10, 20)
print(result1,result2,result3,result4,resutt4_1)
//30 60 30 30 30
}
Swift 中的枚举,关联值 和 原始值的区分?
-
- 将 枚举的成员值 跟 其他类型的值 关联 存储在一起
- 存储在枚举变量中,占用枚举变量内存
enum Score {
case points(Int)
case grade(Character)
}
-
- 枚举成员可以使用相同类型的默认值预先关联,这个默认值叫做:原始值
- 不会存储在 枚举变量中,不占用枚举变量内存
enum PokerSuit : Character {
case spade = "♠"
case heart = "♥"
case diamond = "♦"
case club = "♣"
}
闭包是引用类型吗?
闭包和函数都是是引用类
型。如果一个闭包被分配给一个变量,这个变量复制给另一个变量,那么他们引用的是同一个闭包,他们的捕捉列表也会被复制。
swift 中的闭包结构是什么样子的?
{
(参数列表) -> 返回值类型 in 函数体代码
}
什么是尾随闭包
- 尾随闭包 是一个被 书写在 函数调用 括号 后面的 闭包表达式
基本定义
●Swift中可通过 func 定义一个函数,也可以通过 闭包表达式 定义一个函数
闭包表达式的格式:
{
(参数列表) -> 返回值类型 in
函数体代码
}
闭包表达式与定义函数的语法相对比,有区别如下:
1没有func
2没有函数名
3返回值类型添加了关键字in
let fn1 = {
(v1 : Int,v2 : Int) -> Int in
return v1 + v2
}
let result1 = fn1(10,5)
let result2 = {
(v1:Int,v2:Int) -> Int in
return v2 + v1
}(10,6)
print(result1,result2) // 15 16
闭包表达式的简写
func exec(v1:Int,v2:Int,fn:(Int,Int)->Int) {
print(fn(v1,v2))
}
闭包表达式的简写
private func test2() {
// 1: 没有简写
exec(v1:10, v2:20, fn: {
(v1:Int,v2:Int) -> Int in
return v1 + v2
})
// 2: 简写1
exec(v1: 2, v2: 3, fn: {
v1,v2 in return v1 + v2
})
// 3:简写 2
exec(v1: 3, v2: 4, fn: {
v1,v2 in v1 + v2
})
// 4: 简写3
exec(v1: 5, v2: 6, fn: {$0 + $1})
// 5: 简写4
exec(v1: 7, v2: 8, fn: +)
}
尾随闭包
- 将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强代码的可读性
-
- 尾随闭包 是一个被 书写在 函数调用 括号 后面的 闭包表达式
func test3() {
exec(v1: 8, v2: 7) { a, b in
a + b
}
// or { 书写在 函数调用 括号 后面的 闭包表达式}
exec(v1: 9, v2: 10) {
$0 + $1
}
}
- 如果 闭包表达式 是函数的唯一 实参,且使用了尾随闭包的 语法,则在函数名后面的
()
可省略
// fn:就是尾随闭包
func exec1(fn:(Int,Int)->Int) {
print(fn(1,2))
}
exec1(fn: {$0 + $1})
exec1() {$0 + $1}
exec1{$0 + $1}
什么是逃逸闭包
- 闭包有可能在函数结束后调用,闭包调用 逃离了函数的作用域,需要通过
@escaping
声明
注意:逃逸闭包 不可以 捕获 inout 参数
原因是: 逃逸闭包不确定 何时开始执行,有可能 在执行逃逸闭包时,可变参数已经被程序回收,造成野指针访问
什么是自动闭包
自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。
它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。
这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? {
return v1 > 0 ? v1 : v2()
}
getFirstPositive(10, 20)
- 为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行
- @autoclosure 会自动将 20 封装成闭包 { 20 }
- @autoclosure 只支持 () -> T 格式的参数
- @autoclosure 并非只支持最后1个参数
- 有@autoclosure、无@autoclosure,构成了函数重载
如果你想要自动闭包允许逃逸,就同时使用 @autoclosure 和 @escaping 标志。
Swift 中的存储属性与计算属性
存储属性(Stored Property)
- 类似于成员变量这个概念
- 存储在实例对象的内存中
- 结构体、类可以定义存储属性
- 枚举不可以定义存储属性
关于 存储属性, Swift 中有个明确的规定
- 在创建 类 或者 结构体 的实例时,必须为所有的存储属性设置一个合适的初始值
-
- 可以在初始化器里为存储属性设置一个初始值
- 可以分配一个默认的属性值作为属性定义的一部分
计算属性(Computed Property)
- 本质就是方法(函数)
- 不占用实例对象的内存
- 枚举、结构体、类都可以定义计算属性
计算属性(Computed Property)
○本质就是方法(函数)
○不占用实例的内存
○枚举、结构体、类 都可以定义计算属性
理解计算属性与存储属性:
如果两个属性之间存在一定的逻辑关系,使用计算属性,原因如下:
●如果都用存储属性的话,其逻辑对应关系可能有误
●而使用计算属性,则可以准确的描述 这种逻辑关系
具体案例参考 下面的 Cicle 中的 radius(半径) 和 diameter(直径) 的逻辑关系
同时因为计算属性不占用实例的内存,可以有效的节省实例的内存空间
●set 传入的新值 默认叫做 newValue,也可以自定义
struct Cicle {
/// 存储属性
var radius :Double
/// 计算属性
var diameter: Double {
get {
radius * 2
}
set (jkNewValue){
radius = jkNewValue / 2.0
}
}
}
- 只读计算属性:只有
get
, 没有set
struct Cicle {
/// 存储属性
var radius :Double
/// 计算属性
/*
var diameter: Double {
get {
radius * 2
}
}
*/
// 上述代码与下面的代码等价
var diameter: Double {radius * 2}
}
var cicle = Cicle.init(radius: 12)
print(cicle.radius)//12.0
print(cicle.diameter)//24.0
// cicle.diameter = 10.0 //Cannot assign to property: 'diameter' is a get-only property
- 定义计算属性只能用 var,不可以是 let
-
- let 表示常量
- 计算属性的值是可能会发生变化的(包括只读计算属性)
什么是[延迟存储属性](Lazy Stored Property)
- 使用
lazy
可以定义一个 延迟存储属性,在第一次用到属性的时候才会进行初始化(类似 OC 中的懒加载)
注意点:
- lazy 属性 必须是 var
-
- 因为,let 属性 必须在实例的初始化方法 完成之前 就拥有值
-
如果多条线程同时第一次访问 lazy 属性,无法保证 属性 只被 初始化 一次 (即线程不是安全的)
-
使用
lazy
可以定义一个 延迟存储属性,在第一次用到属性的时候才会进行初始化(类似 OC 中的懒加载)
注意点:
- lazy 属性 必须是 var
-
- 因为Swift 规定:let 属性 必须在实例的初始化方法 完成之前 就拥有值
- 如果多条线程同时第一次访问 lazy 属性,无法保证 属性 只被 初始化 一次 (即线程不是安全的)
class Car {
init() {
print("car has init")
}
func run() {
print("car running")
}
}
class Person {
lazy var car = Car.init()
init() {
print("person has init")
}
func go_out() {
car.run()
}
}
let p = Person.init() //person has init
print("*******")
p.go_out()// car has init ----> car running
- 当结构体 包含 一个延迟存储属性时,只有 var 才能访问延迟 存储属性
-
- 因为延迟属性 初始化时 要改变 结构体的内存
struct Point {
var x = 0
var y = 0
lazy var z = 0
}
let p = Point.init()
print(p.z)//Cannot use mutating getter on immutable value: 'p' is a 'let' constant
什么是 类型 属性?
- 类型属性(Type Property) :通过类型去访问
-
- 存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量,底层采用了 gcd_once 操作)
- 计算类型数据(Computed Type Property):
属性可分为
●实例属性(Instance Property):通过实例去访问
○存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有1分
○计算实例属性(Computed Instance Property):不占用实例的内存,本质是方法
●类型属性(Type Property) :通过类型去访问
○存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量,底层采用了 gcd_once 操作,保证只初始化一次)
○计算类型数据(Computed Type Property):
注意:
存储类型属性不会 占用 实例对象 的内存,整个程序运行过程中,就只有1份内存
存储类型属性 本质就是全局变量(该全局变量加了一些类型控制,只能通过类型去访问),
可以通过 static 定义类型属性
如果 是类, 也可以使用 关键字 class
结构体 就只能使用 关键字 static
struct Car {
static var count:Int = 0
init(){
Car.count += 1
}
}
let c1 = Car.init()
let c2 = Car.init()
let c3 = Car.init()
print(Car.count)//3
类型属性的细节:
●不同于 存储实例属性 ,必须给 存储类型属性 设定初始值
○因为类型没有想实例那样的 init 初始化器来初始化 存储属性
●存储类型属性 默认就是 lazy。会在第一次使用的时候 初始化
○就算 被 多个线程 同时访问,保证只会被 初始化 一次,线程是安全的
○存储类型 属性 也可以是 let
●枚举类型 也可以 定义 类型属性(存储类型属性,计算类型属性)
Swift 中如何定义单例模式
可以通过类型属性+let+private
来写单例; 代码如下如下:
// 方式一
public class SingleManager{
public static let shared = SingleManager()
private init(){}
}
// 方式二
public class SingleManager{
public static let shared = {
//...
//...
return SingleManager()
}()
private init(){}
}
// 上述两个方法等价,一般推荐 方式二
Swift 中 下标是什么?
- 使用
subscript
可以给任意类型(枚举,结构体,类) 增加下标的功能
subscript 的语法 类似于 实例方法、计算属性,本质就是方法(函数)
func xiabiaoTest() {
class Point {
var x = 0.0
var y = 0.0
subscript (index:Int) -> Double {
set{
if index == 0 {
x = newValue
} else if index == 1 {
y = newValue
}
}
get{
if index == 0 {
return x
} else if index == 1 {
return y
}
return 0
}
}
}
let p = Point()
p[0] = 11.1 // 调用subscript
p[1] = 22.2 // 调用subscript
print(p.x)//11.1 不会调用 subscript
print(p.y)//22/2 不会调用 subscript
print(p[0])//11.1 // 调用subscript
print(p[1])//22.2 // 调用subscript
}
简要说明Swift中的初始化器?
一图胜千言 针对类
- 结构体会默认生成 含有参数的初始化器,一旦自定义初始化器,默认的初始化器则不可用
- 类默认只会生成无参的指定初始化器
- 类、结构体、枚举都可以定义初始化器
- 类有2种初始化器: 指定初始化器(designated initializer)、便捷初始化器(convenience initializer)
什么是可选链?
可选链是一个调用和查询可选属性、方法和下标的过程,它可能为 nil 。
- 如果可选项包含值,属性、方法或者下标的调用成功;
- 如果可选项是 nil ,属性、方法或者下标的调用会返回 nil 。
- 多个查询可以链接在一起,如果链中任何一个节点是 nil ,那么整个链就会得体地失败。
可选链(Optional Chaining)
如果 可选项 不会nil ,调用 方法 ,下标,属性成功,结果会被包装成 可选项,反之调用失败,返回nil
class Car { var price = 0}
class Dog { var weight = 0}
class Person {
var name:String = ""
var dog :Dog = Dog()
var car :Car? = Car()
func age() -> Int { 18 }
func eat() {print("Person Eat")}
subscript (index:Int) ->Int {index}
}
var person :Person? = Person()
var age1 = person!.age() //Int
var age2 = person?.age() // Int?
var name = person?.name // String?
var index = person?[6] // Int?
func getName() -> String {"jackie"}
/*
如果 person 对象 是 nil ,将不会调用 getName() 方法
*/
person?.name = getName()
- 如果结果本来就是可选项,不会进行再次包装
if let _ = person?.eat() {
/*
Person Eat
eat success
*/
print("eat success")
} else {
print("eat failure")
}
- 多个
?
可以连接在一起,其中任何一个节点 如果为 nil,那么整条链就会 调用失败
var dog = person?.dog // Dog?
var weight = person?.dog.weight // Int?
var price = person?.car?.price // Int?
什么是运算符重载?
类、结构体、枚举可以为现有的运算符提供自定义的实现,这个操作叫做:运算符重载
struct Point {
var x: Int,y: Int
static func + (p1: Point,p2: Point) -> Point {
Point(x: p1.x + p2.x ,y: p1.y + p2.y)
}
}
let p = Point(x:10,y: 20) + Point(x: 30,y: 40)
print(p) //Point(x: 40, y: 60)
Swift中函数的柯里化
将一个 接受 多个参数的 函数,变成 只接受 一个参数的一系列 操作
示例
func add(_ v1: Int,_ v2: Int) -> Int {
v1 + v2
}
func difference(_ v1: Int,_ v2: Int) -> Int {
v1 - v2
}
func add2(_ v1: Int,_ v2: Int,_ v3 :Int ,_ v4 :Int) -> Int {
v1 + v2 - v3 + v4
}
- 伪柯里化
func currying_add(_ v1:Int) -> (Int) -> Int {
return {$0 + v1}
}
/*
将任意一个 接受两个 参数的函数 柯里化
*/
func curring_fun_tow_params1(_ f1 :@escaping (Int,Int) -> Int, _ v1 :Int) -> (Int) -> Int {
// return {
// f1($0,v1)
// }
return { (v2) in
return f1(v1 , v2)
}
}
print(add(10, 20)) // 30
print(currying_add(10)(20)) // 30 // 30
print(curring_fun_tow_params1(add, 10)(20))
- 正宗柯里化
func curring_fun_two_params2<A,B,C>(_ f1: @escaping (A,B) -> C) -> (A) -> ((B) -> C) {
/*
return {
(a) in // a = 3
return {
(b) in // b = 8
return f1(a,b)
}
}
*/
{ a in { b in f1(a, b)} }
}
let result = curring_fun_two_params2(add)(3)(5)
print("result == ",result) //8
- 柯里化拓展
/*
-> (A) -> (B) -> (C) -> (D) -> E
实际是 一连串 闭包的 组合 如下所示:
-> (A) -> ( (B) -> ((C) -> ((D) -> E)) )
传入 A > 一个 闭包 (B) -> ( (C) -> ((D) -> E) )
传入 B >> 一个 闭包 (C) -> ( (D) -> E )
传入 C >> 一个闭包 (D) -> E
*/
//func curring_fun_more_params<A,B,C,D,E>(_ fn: @escaping (A,B,C,D) -> (E)) -> (A) -> ((B) -> ((C) -> ((D) -> E))) {
func curring_fun_more_params<A,B,C,D,E>(_ fn: @escaping (A,B,C,D) -> (E)) -> (A) -> (B) -> (C) -> (D) -> E {
/*
return {
(a) in
return {
(b) in
return {
(c) in
return {
d in
return fn(a,b,c,d)
}
}
}
}
*/
{a in { b in { c in { d in fn(a,b,c,d)}}}}
}
let resutl2 = curring_fun_more_params(add2)(10)(20)(30)(40)
print(resutl2) // 40
let resutl2_func = curring_fun_more_params(add2)
let resutl2_func_value = resutl2_func(10)(20)(30)(40)
print(resutl2_func_value) // 40
记录一次swift函数式编程的探讨
本文适合哪些人?
本文针对的是已经有一部分Swift开发的基础,同时对函数式范式比较感兴趣的开发者。 当然,如果只对函数式范式感兴趣,我觉得这篇文章也值得一看。
函数式编程是什么?
首先来看这个词语”Functional Programming“,它是什么?
当需要去查一个专业术语的定义的时候,我的第一反应是来查询Wikipedia:
❝
In computer science, fucnitonal programming is a programming paradigm where programs are constructed by applying and composing fucntions.
在这个定义里,有一个很熟悉的词——programming paradigm
, 一般翻译为编程范式,可是我对这个翻译还是有些迷糊,于是我又在wikipedia中查找这个词语的含义:
❝
Programming paradigms are a way to classify programming languages based on their features.
编程范式(编程范例)是一种基于语言自身的特性来给编程语言分类的方式。
同时wikipedia中还总结了常见的编程范式的分类:
- imperative
-
- procedural
- object-oriented
- declarative
-
- functional
- logic
- mathematical
- reactive
那么究竟什么是编程范式呢?我们知道编程是一门工程学,它的目的是去解决问题,而解决问题可以有很多的方法,编程范例就是代表着解决问题的不同思路。如果说我们是编程世界的造物主的话,那么编程范例应该就是我们创造这个世界的方法论。所以我非常喜欢台湾那边对programming paradigm
的翻译:程式設計法。
为什么我要强调编程范例是什么东西,而且还分门别类的列举了出来这些编程范例呢?
因为编程本身是抽象的,编程范例其实就是我们如何抽象这个世界的方法,我只是想通过这个具体的定义来说明函数式本身就是一种方法论。 所以我们学习的时候没必要害怕它,遇到引用透明,副作用,科里化,函子,单子,惰性求值等等等等这些概念的时候,畏惧的原因只是不熟悉而已,就想我们学习面向对象的时候:继承,封装,多态,动态绑定,消息传递等等等等,这些概念我们一开始也不熟悉,所以当我们熟悉了函数式这些概念的时候,一切自然水到渠成。 在我们熟悉的面向对象的编程范式中,我们知道它的思想是:一切皆对象,而在纯函数式的编程范式中,可以说:一切皆函数。在函数式编程中,函数是一等公民,那什么是一等公民呢?就是它可以作为参数,返回值,也可以赋值给变量,也就是说它的地位其实是和Int,String, Double等基本类型是一样的,换言之,要像使用基本类型一样去使用它!
不同的思想就是创建世界的方法论的不同之处,这里我举个例子,那就是状态,比如登录的各种状态,维护状态会大大增加系统的复杂性,特别是状态很多的时候,而且引入状态这个概念之后,会带来很多复杂的问题:状态持久化,环境模型等等等,而如果使用面向对象的编程范例,可以将每一个状态都定义为一个对象,如C#中的状态机的实现,而在函数式编程里呢? 在SICP中提到,状态是随着时间改变的,所以状态是否可以使用f(t)来表示呢?这就是使用函数式的思路来抽象状态。
当然,我这里并不是说只能使用一种编程范式,我也并不鼓吹函数式就一直是好的,但是掌握函数式可以让我们在解决问题的时候提供更多的选择,更有效率的解决问题,事实上,我们解决问题(创造世界)肯定会使用很多种方法论即多种编程范式,一般情况下,更现代的编程语言都支持多范式编程,这里用swift里的RxSwift来举例:
public class Observable<Element> : ObservableType {
internal init()
public func subscribe<Observer>(_ observer: Observer) -> Disposable where Element == Observer.Element, Observer : RxSwift.ObserverType
public func asObservable() -> Observable<Element>
}
// 观察者
final internal class AnonymousObserver<Element> : ObserverBase<Element> {
internal typealias EventHandler = (Event<Element>) -> Void
internal init(_ eventHandler: @escaping EventHandler)
override internal func onCore(_ event: Event<Element>)
}
extension ObservableType {
public func flatMap<Source>(_ selector: @escaping (Element) throws -> Source) -> Observable<Source.Element> where Source : RxSwift.ObservableConvertibleType
}
extension ObservableType {
public func map<Result>(_ transform: @escaping (Element) throws -> Result) -> Observable<Result>
}
它的Observable和Observer都抽象成了类,并且添加了相应的行为,承担了相应的职责,这是面向对象范式;它实现了OberveableType协议,并且拓展了该协议,添加了大量的默认实现,这是面向协议范式;它实现了map,和flatMap方法,可以说Observable是一个函数单子(Monad),同时也提供了大量的操作符可供使用和组合,这是函数式范式;同时,总所周知,Reactive框架是一个响应式的框架,所以它也是响应式范式......
更何况,编程能力不就是抽象能力的体现吗?所以我认为掌握函数式是非常必要的!那么具体来说为什么重要呢?
在1984年的时候,John Hughes 有一篇很著名的论文《Why Functional Programming Matters》, 它解答了我们的疑问。
为什么函数式编程重要?
通常网络上的一些文章都会总结它的优点:它没有赋值,没有副作用,没有控制流等等等等,不同的只是它们对于各个关键词诸如引用透明,无副作用的种种解释,单是这只是列出了很多函数式程序 "没有" 什么,却没有说它 “有” 什么,所以这些优点其实没有太大的说服力。而且我们实际上去写程序的时候,也不可能特意去写一个 缺少了赋值语句或者特别引用透明的程序,这也不是衡量质量的尺度,那么真正重要的是什么呢?
在这篇论文中提到,模块化设计是成功的程序化设计的关键,这一观点已经被普遍接受了,但有一点经常容易被忽略,那就是编写一个模块化程序解决问题的时候,程序员首先要把问题分解为子问题,然后解决这些子问题并把解决方案合并。程序员能够以什么方式分解问题,直接取决于他能以什么方式把解决方案粘起来。而函数式范式其实提供给我们非常重要的粘合剂,它可以让我们设计一些更小、更简洁、更通用的模块,同时使用黏合剂粘合起来。
那么它提供了哪些黏合剂呢?这篇论文介绍了两种:
黏合函数:高阶函数
❝
The first of the two new kinds of glue enables simple functions to be glued together to make more complex ones.
黏合简单的函数变为更复杂的函数。这样的好处是我们模块化的颗粒度是更细的,可以组合的复杂函数也是更多的。如果非要做一个比喻的话,我觉得就像乐高的基础组件:
这种聚合就是一个泛化的高阶函数和一些特化函数的聚合,这样的高阶函数一旦定义,很多操作都可以很容易地编写出来。
黏合程序:惰性求值
❝
The other new kind of glue that functional languages provide enables whole programs to be glued together.
函数式语言提供的另一种黏合剂就是可以使得程序黏在一起。假设有这么一个函数:
g(f(input))
传统上,需要先计算f,然后再计算g,这是通过将f的输出存储在临时文件中实现的,这种方法的问题是临时文件会占用太大的空间,会让程序之间的黏合变得不太现实。而函数式语言提供的这一种解决方案,程序f和g严格的同步运行,只有当g视图读取输入时,f才启动。这种求值方式尽可能得少运行,因此被称为 "惰性求值" 。它将程序模块化为一个产生大量可能解的生成器与一个选取恰当解的选择器的方案变得可行。
大家如果有时间还是应该去读读这一篇论文,在论文中,它讲述了三个实例:牛顿-拉夫森求根法,数值微分,数值积分,以及启发性搜索,并使用函数式来实现它们,非常的精彩,这里我就不复述这些实例了。最后我再引用一下该论文的结论:
❝
在本文中,我们指出模块化是成功的程序设计的关键。以提高生产力为目标的程序语言,必须良好地支持模块化程序设计。但是,新的作用域规则和分块编译的技巧是不够的——“模块化”不仅仅意味着“模块”。我们分解程序的能力直接取决于将解决方案粘在一起的能力。为了协助模块化程序设计,程序语言必须提供优良的黏合剂。函数式程序语言提供了两种新的黏合剂——高阶函数与惰性求值。
一颗枣树(例子)
这个例子我参考了Objc.io的《函数式Swift》书籍中关于如何使用函数式的方式来封装滤镜的案例。
Core Image是一很强大的图像处理框架,但是它的API是弱类型的 —— 可以通过键值编码来配置图像滤镜,这样就导致很容易出错,所以可以使用类型来避免这些原因导致的运行时错误,什么意思呢?就是说我们可以封装一些基础的滤镜Filter, 并且还可以实现它们之间的聚合方式。这就是上述论文中介绍的函数式编程提供的黏合剂之一:使简单的函数可以聚合起来形成复杂的函数。
首先确定我们的滤镜类型,该函数应该接受一个图像作为参数并返回一个新的图像:
typalias Filter = (CIImage) -> CIImage
在这里引用一段书中的原话:
❝
我们应该谨慎地选择类型。这比其他任何事情都重要,因为类型将左右开发流程。
然后可以开始定义函数来构件特定的基础滤镜了:
/// sobel提取边缘滤镜
func sobel() -> Filter {
return { image in
let sobel: [CGFloat] = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
let weight = CIVector(values: sobel, count: 9)
guard let filter = CIFilter(name: "CIConvolution3X3",
parameters: [kCIInputWeightsKey: weight,
kCIInputBiasKey: 0.5,
kCIInputImageKey: image]) else { fatalError() }
guard let outImage = filter.outputImage else { fatalError() }
return outImage.cropped(to: image.extent)
}
}
/// 颜色反转滤镜
func colorInvert() -> Filter {
return { image in
guard let filter = CIFilter(name: "CIColorInvert",
parameters: [kCIInputImageKey: image]) else { fatalError() }
guard let outImage = filter.outputImage else { fatalError() }
return outImage.cropped(to: image.extent)
}
}
/// 颜色变色滤镜
func colorControls(h: NSNumber, s: NSNumber, b: NSNumber) -> Filter {
return { image in
guard let filter = CIFilter(name: "CIColorControls", parameters: [kCIInputImageKey: image, kCIInputSaturationKey: h, kCIInputContrastKey: s, kCIInputBrightnessKey: b]) else { fatalError() }
guard let outImage = filter.outputImage else { fatalError() }
return outImage.cropped(to: image.extent)
}
}
直接黏合
基础组件已经有了,接下来就可以堆积木了。如果有一个滤镜需要:先提取边缘 -> 颜色反转 -> 颜色变色,那么我们可以实现如下:
let newFilter: Filter = { image in
return colorControls(h: 97, s: 8, b: 85)(colorInvert()(sobel()(image)))
}
上述做法有一些问题:
- 可读性差:无法代码即注释,无法很容易的知道滤镜的执行顺序
- 不易拓展:API不友好,添加新的滤镜时,需要考虑顺序和括号,很容易出错
自定义函数黏合
首先我们解决可读性差的问题,因为直接使用嵌套调用方法,所以会可读性差。所以我们要避免嵌套调用,直接定义combine方法来组合滤镜:
func compose(filter filter1: @escaping Filter, with filter2: @escaping Filter) -> Filter {
return { image in
filter2(filter1(image))
}
}
// sobel -> invertColor
let newFilter1: Filter = compose(sobel(), colorInvert()) // 左结合的
这是左结合的,所以可读性是OK的,但是如果有三个滤镜组合呢?四个滤镜组合呢?要定义那么多方法吗? 巧了,还真有人是这么干的:
如果大家去看RxSwift的话,就会看见它组合多个Observable的函数: zip
, combineLastest
,每一个方法簇都提供了支持多个参数的组合方法,可是这就意味着我们在这个案例也是可以这样做的,但是这显然不是最好的解决方案。
如果使用combine这里三个滤镜组合的方案:
let newFilter2: Filter = compose(compose(sobel(), colorInvert()), colorControls(h:97, s:8, b:85)))
可读性还行,但是还是在添加新的滤镜的时候容易出错,不那么容易拓展。如果要再组合多个滤镜,那么就需要多个combine函数嵌套调用。
自定义操作符黏合
如果对应到数学领域的话,其实这几个滤镜的组合不就是四则运算中的 +
吗?一层一层效果的叠加,当然,确切地说,从效果上和 +
更相似,但是从特性来说更符合减法 -
的,都是向左结合,而且都不满足交换律。
所以我们可以自定义操作符来处理滤镜的结合:
infix operator >>>
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in
filter2(filter1(image))
}
}
当然还有一个小问题,就是如果有三个滤镜组合的话,会报错,因为我们没有指定它组合的方式(左结合,还是右结合)所以这里我们让它继承加法的优先级,因为它和加法一样都是左结合的:
infix operator >>>: AdditionPrecedence // 让它继承+操作符的优先级, 左结合
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in
filter2(filter1(image))
}
}
那接下来我们愉快地使用它吧:
let filter = sobel() >>> colorInvert() >>> colorControls(h: 97, s: 8, b: 85)
let outputImage = filter(inputImage)
imageView.image = UIImage(ciImage: outputImage)
函数式Swift.001.jpeg
那么这里来总结一下这一波过程,假设需求是存在的:
我们定义了很多基础滤镜层(Filter),接下来肯定需要组合基础滤镜为我们实际需求需要的滤镜,有的滤镜可能是有三个基础滤镜组合的,有的需要五个基础滤镜组合,当然极限情况下,可能还有需要十个滤镜组合的。
所以我们需要定义不同滤镜组合的黏合函数, 我们一共经历了三个组合方案的变迁:
- 直接组合
- 定义compose函数
- 自定义操作符
当然,诸君也可以使用更好的组合方案,如果可以希望留个言,共同探讨探讨。
还有一颗也是枣树(例子)
接下来这个例子,是一个我们使用Objective-C编程的时候经常会遇到的问题,需求如下:第二行数据必须等待第一行请求结束之后才可以开始请求。
那么开始吧!
首先我们来看最容易的实现方案:
@objc func syncData() {
self.statusLabel.text = "正在同步火影忍者数据"
WebAPI.requestNaruto { (firstResult) in
if case .success(let result) = firstResult {
self.sectionOne = result.map { $0 as? String ?? "" }
DispatchQueue.main.async {
self.tableView.reloadSections([0], with: .automatic)
self.statusLabel.text = "正在同步海贼王数据"
WebAPI.requestOnePiece { (secondResult) in
if case Result.success(let result) = secondResult {
self.sectionTwo = result.map { $0 as? String ?? "" }
DispatchQueue.main.async {
self.statusLabel.text = "同步海贼王数据成功"
self.tableView.reloadSections([1], with: .automatic)
}
}
}
}
}
}
}
熟悉吗?当然熟悉,直接在第一个请求的callback中直接进行第二个请求,但是请注意,这和OC写的有区别吗?我们这样和写和简单的人肉翻译机有区别吗?我们写的是Swift这个多范式的编程语言吗?
回到例子,我们就事论事,我觉得这样写会有几个问题:
- 数据修改和UI修改耦合在了一起
- 多重嵌套
- 违背了OCP(Open Closed Principle)法则:应该对修改闭合,对拓展开放
- 丑!
解决数据和UI耦合
从重要性的角度,我觉得应该先解决第4个问题,但是出于节奏,我们还是从第一个问题开始解决吧~
@objc func syncDataThere() {
// 嵌套函数
func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
DispatchQueue.main.async {
self.statusLabel.text = text
if reload.isReload { self.tableView.reloadSections([reload.section], with: .automatic) }
}
}
updateStatus(text: "正在同步火影忍者数据", reload: (false, 0))
requestNaruto {
updateStatus(text: "正在同步海贼王数据", reload: (true, 0))
self.requestOnePiece {
updateStatus(text: "同步数据成功", reload: (true, 1))
}
}
}
这里我把网络请求和数据处理都封装到了网络请求中,而且使用了swift的特性:嵌套函数,剥离了一部分重复代码,这样整个请求就变得非常清晰明了了,而且数据和UI就隔离开来了,并没有耦合在一起。
可是嵌套的问题还是存在,如何解决呢?
解决多重嵌套
还记得我介绍的第一棵枣树吗?我使用了自定义操作符来解决了函数调用的嵌套,这里其实也是一样的思路,但是要更复杂些。
这里我还需要重复引用一下《函数式Swift》中的那句话:
❝
我们应该谨慎地选择类型。这比其他任何事情都重要,因为类型将左右开发流程。
第一步抽象
这里有两个类型需要抽象,第一是执行单个语句的函数(这里是更新UI),第二个是对应网络请求的函数
infix operator ->> AdditionPrecedence
typealias Action = () -> Void
typealias Request = (@escaping Action) -> Void
第二步抽象
那么如何将原来的函数拆解为使用类型表示的函数呢?
func syncDataF() {
......
requestNaruto {
updateStatus(text: "正在同步海贼王数据", reload: (true, 0))
self.requestOnePiece {
updateStatus(text: "同步数据成功", reload: (true, 1))
}
}
)
我们由上往下,那么抽象的过程应该就是
(Request, Action) -> Request
第一个请求 和 回调中的第一个Action
,但是第一个请求还没有结束,所以返回的还是Request
(Request, Request) -> Request
处理了第一个Action
的第一请求 + 第二个请求, 但是请求还是没有结束,所以返回的还是Request
(Request, Action) -> Action
第二个请求加上最后需要处理的Action
, 完毕!
所以结果如下:
@objc func syncDataFour() {
func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
DispatchQueue.main.async {
self.statusLabel.text = text
if reload.isReload {
self.tableView.reloadSections([reload.section], with: .automatic)
}
}
}
updateStatus(text: "正在同步火影忍者数据", reload: (false, 0))
// 我们来拆解一下函数:要把函数抽象出来,这一点非常的重要
// (Request, Action) -> Request
// (Request, Request) -> Request
// (Request, Action) -> Action
// 通过这样的拆解方式就可以开始定义方法了
let task: Action =
requestNaruto
->> { updateStatus(text: "正在同步海贼王数据", reload: (true, 0)) }
->> requestOnePiece
->> { updateStatus(text: "同步数据成功", reload: (true, 1)) }
task()
}
结果呢?我解决了嵌套的问题,很好,很完美,可是也很天真。
解决OCP问题
即使我们使用了自定义操作符,也没有解决OCP问题,因为如果我们要添加请求的话,我们还是需要修改原来的方法,依然违背了OCP法则。
那么怎么解决呢?
嗯嗯,具体的,请各位自己去试验吧!
我在文章尾部添加了相应的引用信息,这个例子是基于2016年的国内的Swift大会中翁阳的分享《Swift, 改善既有代码的设计》,如果有时间,希望大家可以去看看这个分享。
在分享中,他使用了面向协议的思路解决了OCP问题,很抽象,很精彩。
总结
很开心诸位看到了这里,我觉得这篇文章的能量密度应该不会浪费你们的时间。
在这边文章中,我首先是追问了函数式编程,以及编程范式的定义,只是想告诉大家:函数式编程之所以复杂只是因为我们不熟悉,同时它也应该是我们必须的工具。
然后我介绍了《Why Functional Programming Matters》这篇论文,它说明了为什么函数式编程重要,提到函数式范式的两大武器:高阶函数和惰性求值。
最后我使用了两颗枣树来给大家看一看Swift语言结合函数式的思想可以有哪些奇妙的化学反应。
那么这一次Swift的一次函数式之旅就结束了。但是还是想补充几句,每一年的WWDC其实Swift都更新了很多的内容,Swift本身也一直在增加新的特性,一直在稳健的迭代着,如果我们还是使用Objective-C的思维去写Swift的话,其实本身是落后于语言发展的。
最后引用王安石的《游褒禅山记》中的一段话:
❝
而世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也。
与君共勉!
项目管理:Prince2整体知识点梳理
PRINCE2,即PRoject IN Controlled Environment(受控环境中的项目)是一种结构化的项目管理方法论,由英国政府内阁商务部(OGC)推出,是英国项目管理标准。
PRINCE2
PRINCE2 作为一种开放的方法论,是一套结构化的项目管理流程,描述了如何以一种逻辑性的、有组织的方法,按照明确的步骤且基于商业论证对项目进行管理。
如果组织选择PRINCE2作为项目管理的标准,就可以在商业变革、组织结构、信息技术、重组与并购、研究、产品开发等多种商业活动领域,极大地提升组织的能力与成熟度。
项目的五大特征
项目评价指标
三七四二
七大原则、七大主题、七大流程,四层组织架构,两种工具技术
PRINCE2的"三七四二":
苹果的产品经理设计的App Clip是有意为之,还是必然趋势,详解 App Clip技术之谜
苹果在 WWDC2020 上发布了 App Clip,有媒体叫做“苹果小程序”。虽然 Clip 在产品理念上和小程序有相似之处,但是在技术实现层面却是截然不同的东西。本文会针对 Clip 的技术层面做全面的介绍。
实现方式:native 代码、native 框架、native app 一样的分发
在实现上,Clip 和原生的 app 使用一样的方式。在 UI 框架上同时支持 UIKit 和 SwiftUI,有些开发者认为只能使用 SwiftUI 开发,这点是错误的。Clip 的定位和 watch app、app extension 类似,和 app 在同一个 project 里,是一个单独的 target。只是 Clip 并没有自己的专属 framework(其实有一个,但是主要包含的是一些特色 api),使用的框架和 app 一致,可以认为是一个精简版的原生 App。
Clip 不能单独发布,必须关联一个 app。因此发布的流程和 app 和一样的,在 apple connect 上创建一个版本,和 app 一起提交审核。和 app 在技术上的最大区别只是大小限制在 10MB 以内,因为 Clip 的基础就是希望用户可以最迅速的被用户使用,如果体积大了就失去了产品的根本。
产品定位:用完即走
苹果对 Clip 的使用场景非常明确:在一个特定的情境里,用户可以快速的使用 app 的核心服务。是小程序内味了!
坦率的说,很难说 Clip 的理念是苹果原创的,在产品的定位上和微信小程序如出一辙。尤其是微信小程序在国内已经完全普及了,微信小程序初始发布的时候也被苹果加了多条限制。其中一条就是小程序不能有虚拟商品支付功能。现在回头看苹果自己的 Clip 可以完美支持 apple pay,很难说苹果没有私心。
触手可及
Clip 使用一段 URL 标识自己,格式遵从 universal link。因为苹果对 Clip 的使用场景非常明确,所以在 Clip 的调起方式做了严格限制。Clip 的调用只能是用户主动要发起才能访问,所以不存在用户在某个 app 里不小心点了一个按钮,就跳转下载了 Clip。
Clip 的发起入口有以下几种:
- NFC
- 二维码
- Safari 中关联了 Clip 的网页
- 苹果消息应用
- Siri 附近建议和苹果地图
NFC 和二维码的入口很容易理解,必须用户主动拿出手机靠近 NFC、打开相机扫描。苹果专属的 Clip 码生成工具在年底才会开放。
Safari 中发起和之前的 universal link 类似,在网站配置了关联的 Clip 信息后,会有一个 banner 提示打开应用。
因为 Clip 提交 app store 审核的信息里也会配置好相关的 url,因此如果在 message 里发了 clip 的链接,操作系统也会在应用里生成一个 Clip 的卡片,用户如果需要可以主动点击。
Siri 附近建议和苹果地图(在 connect 中可以配置 clip 的地理位置)。场景和前面的二维码类似,如果我在地图上看到一个商家,商家有提供服务的 Clip,我可以在地图或者 Siri 建议里直接打开 Clip。
再次总结一下 Clip 的入口限制:只能是用户主动发起才能访问。虽然 Clip 的入口是一段 universal link,在代码里的处理方式也和 universal link 一致,但是为了 Clip 不被滥用,Clip 的调起只能是操作系统调起。App 没有能力主动调起一个 Clip 程序。
无需安装、卸载
因为 Clip 的大小被限制在了 10MB 以下,在当下的网络状态下,可以实现快速的打开。为了给用户使用非常轻松的感觉,在 UI 上不会体现“安装”这样的字眼,而是直接“打开”。预期的场景下用户打开 Clip 和打开一个网页类似。因此在用户的视角里就不存在软件的安装、卸载。
Clip 的生命周期由操作系统全权接管。如果 Clip 用户一段时间后没有使用,操作系统就会自动清除掉 Clip,Clip 里存储的数据也会被一并清除。因此虽然 Clip 提供了存储的能力,但是程序不应该依赖存储的数据,只能把存储当做 cache 来使用,操作系统可能自动清除缓存的数据。
横向比较:PWA、Instant Apps、小程序
Instant Apps
18 年正式发布的 Android Instant apps 和 Clip 在技术上是最接近的。Instant apps 中文被翻成“免安装应用”,在体验上也是希望用户能够最低成本的使用上 app,让用户感受不到安装这个步骤。Instant apps 也可以通过 url 标识(deep link),如果在 chrome 里搜索到应用的网站,chrome 如果识别到域名下有关联应用,可以直接“打开”。消息中的链接也可以被识别。只是 Instant apps 发布的早,国外用户也没有使用二维码的习惯,所以入口上不支持二维码、NFC。
两者的根本区别还是在定位上,Instant apps 提出的场景是提供一个 app 的试用版。因此场景是你已经到了 app 的下载页面,这个时候如果一个 app 几百兆你可能就放弃下载了,但是有一个极简的试用版,就会提高你使用 app 的可能。这个场景在游戏 app 里尤其明显,一方面高质量的游戏 app 体积比较大。另一方面,如果是一个付费下载的应用,如果有一个免费的试用版,也可以增加用户的下载可能。在苹果生态里很多应用会提供一个受限的免费 lite 版本也是一样的需求。
但是 Instant apps 在国内没有产生任何影响。因为政策的原因,Google Play 不支持在国内市场使用。国内的安卓应用市场也是鱼龙混杂,对于 Instant apps 也估计也没有统一支持。另外国内的安卓生态也和欧美地区区别比较大,早期安卓市场上收费的应用很少,对于用户而言需要试用免费 app 的场景很少。另外大厂也可能会推出专门的急速版应用,安装后利用动态化技术下发代码,应用体积也可以控制在 10 MB 以内。
Clip 则是非常明确的面向线下提供服务的场景,在应用能力上可以接入 sign in with apple,apple pay。这样一个全新的用户,可以很快速的使用线下服务并且进行注册、支付。用户体验会好的多。安卓因为国内生态的原因,各个安卓厂商没有统一的新用户可以快速注册的接口,也没有统一的支付接口,很难提供相匹敌的体验。如果开发者针对各个厂商单独开发,那成本上就不是“小程序”了。
Progressive Web App(PWA)
Progressive Web App 是基于 web 的技术。在移动互联网兴起之后,大家的流量都转移到了移动设备上。然而在移动上的 web 体验并不好。于是 W3C 和谷歌就基于浏览器的能力,制定了一套协议,让 web app 可以拥有更多的 native 能力。
PWA 不是特指某一项技术,而是应用了多项技术的 Web App。其核心技术包括 App Manifest、Service Worker、Web Push。
PWA 相当于把小程序里的代码直接下载到了本地,有了独立的 app 入口。运行的时候基于浏览器的能力。但是对于用户感受和原生 app 一样。
我个人对 PWA 技术很有好感,它的初衷有着初代互联网般的美好。希望底层有一套协议后,用户体验还是没有边界的互联网。然而时代已经变了。PWA 在中国基本上是凉了。
PWA 从出生就带了硬伤,虽然谷歌希望有一套 web 标准可以运行在移动设备上,但是对于苹果的商业策略而言,这并不重要。因此 PWA 的一个协议,从制定出来,再到移动设备(iOS)上支持这个特性,几年就过去了。而且对于移动用户而言,可以拥有一个美好的 web app 并不是他们的痛点。
总结起来 PWA 看着美好,但似乎更多是对于 web 开发者心中的美好愿景。在落实中遇到了很多现实的问题,技术支持的不好,开发者就更没有动力在这个技术上做软件生态了。
微信小程序
前面提过在产品理念上小程序和 Clip 很相似,甚至说不定 Clip 是受了小程序的启发。在市场上,小程序是 Clip 的真正对手。
小程序基于微信的 app,Clip 基于操作系统,因此在能力上 Clip 有优势。小程序的入口需要先打开微信,而 Clip 可以通过 NFC 靠近直接激活应用。对于开发者而言,Clip 可以直接获得很多原生的能力(比如 push),如果用户喜欢可以关联下载自己的原生应用。在小程序中,微信出于商业原因开发者不能直接跳转到自有 app,小程序的能力也依赖于微信提供的接口。
对于从 Clip 关联主 app 苹果还挺重视的,提供了几个入口展示关联 app。
首先在 clip 的展示页就会显示:
每次使用 Clip 时也会有一个短暂的浮层展示:
开发者也可以自己通过 SKOverlay 来展示:
不过如果开发者没有自己的独立 app,那么也就只能选择小程序了。小程序发展到现在场景也比最早提出的线下服务更加多了,反而类似 Instant apps,更像一个轻量级的 app。
考虑到国内很多小程序的厂商都没有自己的独立 app,因此 clip 对于这部分群体也并没有什么吸引力。不过对于线下服务类,尤其有支付场景的,Clip 在用户体验上会比小程序好一些。
总结,Clip 的业务场景和小程序有一小部分是重叠的,小程序覆盖的场景还是更多一些。两者在大部分时候并不是互斥式的竞争关系,即便在一些场景下 Clip 有技术优势,商家也不会放弃小程序,因为还有安卓用户嘛。还是看商家在某些场景里,是否愿意为用户多提供一种更好的交互方式。
对比原生 app 的技术限制
虽然 Clip 可以直接使用 iOS framework,但是因为 Clip 的使用场景是新用户的初次、简短、当下(in-the-moment experience)的使用,相比原生 app 苹果还是进行了一些限制。
App 不能访问用户的隐私信息:
- 运动和健身数据
- Apple Music 和多媒体文件
- 通讯录、信息、照片、文件等数据
不过为了能够提供给用户更加轻便的体验,通过专门为 Clip 设计了免申请的通知、定位权限。不过也有限制:免申请的通知只在 8 个小时内有效。位置只能获取一次。如果 app 需要重度使用这两类权限就还是和原来一样,可以弹窗申请。
某些高级应用能力也会受限,需要在完整的应用中才能使用:
- 不能请求追踪授权
- 不能进行后台请求任务
- 没在激活状态蓝牙连接会断开
总的而言虽然有一些限制,但是这些限制的出发点是希望开发者关注 Clip 的正确使用场景。对于 Clip 所提倡的使用场景里,苹果提供的能力是完全够用的。
一些技术细节
可以建立一个共享 targets 的 Asset catalog 来共用图片资源。
在 Clip 中申请的授权,在下载完整应用后会被同步到应用中。
通过 App Group Container 来共享 clip 和 app 的数据。
image
Clip 的 url 可以配置参数:
在 App Store connect 中还可以针对指定的参数配置不一样的标题和图片。比如一家连锁咖啡店,可能不同的店你希望弹出的标题图片是不一样的,可以进行单独的配置。
总结
苹果给定义的 Clip 的关键词是:lightweight、native、fast、focused、in-the-moment experience。
Clip 在特定的线下场景里有着相当好的用户体验。对于已经拥有独立 app 的公司来说,开发一个 clip 应用的成本并不高。我个人还是期待这样一个好的技术可以被更多开发者接纳,可以提供给用户更好的体验。对于小程序,clip 的场景窄的多,两者并不是直接竞争关系。我更愿意看做是特定场景下,对于小程序原生能力不足的一种补充。
青山不改,绿水长流。谢谢大家!!!
送你一份2023年Java核心篇JVM(虚拟机)面试题整理,收集不易,且行且珍惜!!!
Java内存区域
说一下 JVM 的主要组成部分及其作用?
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
●Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
●Execution engine(执行引擎):执行classes中的指令。
●Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
●Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
下面是Java程序运行机制详细说明
Java程序运行机制步骤
●首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
●再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
●运行字节码的工作是由解释器(java命令)来完成的。
从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
说一下 JVM 运行时数据区
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
●程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
●Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
●本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
●Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
●方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
说一下堆栈的区别?
物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
PS:
1静态变量放在方法区
2静态的对象还是放在堆。
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
队列和栈是什么?有什么区别?
队列和栈都是被用来预存储数据的。
●操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
●可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
●操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。
HotSpot虚拟机对象探秘
对象的创建
说到对象的创建,首先让我们看看 Java 中提供的几种对象创建方式:
下面是对象创建的主要流程:
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行
为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
●指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
●空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
●对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
●把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。
对象的访问定位
Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
指针: 指向对象,代表一个对象在内存中的起始地址。
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
句柄访问
Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
直接指针
如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
内存溢出异常
Java会存在内存泄漏吗?请简单描述
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
#垃圾收集器
简述Java垃圾回收机制
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
GC是什么?为什么要GC
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存
回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动
回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
垃圾回收的优点和原理。并考虑2种回收机制
java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题。
由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。
垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。
垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。
程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。
垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
Java 中都有哪些引用类型?
●强引用:发生 gc 的时候不会被回收。
●软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
●弱引用:有用但不是必须的对象,在下一次GC时会被回收。
●虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
怎么判断对象是否可以被回收?
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:
●引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
●可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
在Java中,对象什么时候可以被垃圾回收
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
JVM中的永久代中会发生垃圾回收吗
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区
(译者注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)
说一下 JVM 有哪些垃圾回收算法?
●标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
●复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
●标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
●分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
标记-清除算法
标记无用对象,然后进行清除回收。
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:
●标记阶段:标记出可以回收的对象。
●清除阶段:回收被标记的对象所占用的空间。
标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。
优点:实现简单,不需要对象进行移动。
缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
标记-清除算法的执行的过程如下图所示
复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程如下图所示
标记-整理算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
优点:解决了标记-清理算法存在的内存碎片问题。
缺点:仍需要进行局部对象移动,一定程度上降低了效率。
标记-整理算法的执行过程如下图所示
分代收集算法
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代,如图所示:
说一下 JVM 有哪些垃圾回收器?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
●Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
●ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
●Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
●Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
●Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
●CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
●G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
●新生代回收器:Serial、ParNew、Parallel Scavenge
●老年代回收器:Serial Old、Parallel Old、CMS
●整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
●把 Eden + From Survivor 存活的对象放入 To Survivor 区;
●清空 Eden 和 From Survivor 分区;
●From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
内存分配策略
简述java内存分配与回收策率以及Minor GC和Major GC
所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面我们介绍了内存回收,这里我们再来聊聊内存分配。
对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则:
对象优先在 Eden 区分配
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。
●Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
●Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。
前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
长期存活对象将进入老年代
虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。
虚拟机类加载机制
简述java类加载机制?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
描述一下JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
1启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
2扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
4用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
说一下类装载的执行过程?
类装载分为以下 5 个步骤:
●加载:根据查找路径找到相应的 class 文件然后导入;
●验证:检查加载的 class 文件的正确性;
●准备:给类中的静态变量分配内存空间;
●解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
●初始化:对静态变量和静态代码块执行初始化工作。
什么是双亲委派模型?
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
类加载器分类:
●启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
●其他类加载器:
●扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
●应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
JVM调优
说一下 JVM 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
●jconsole:用于对 JVM 中的内存、线程和类等进行监控;
●jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
常用的 JVM 调优的参数都有哪些?
●-Xms2g:初始化推大小为 2g;
●-Xmx2g:堆最大内存为 2g;
●-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
●-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
●–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
●-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
●-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
●-XX:+PrintGC:开启打印 gc 信息;
●-XX:+PrintGCDetails:打印 gc 详细信息。
初探dyld动态链接器流程对iOS又精进了一步
前言:作为一个开发者,如果你熟悉很多语言的开发,你会发现大部分语言的一个共同点,main函数。我们都是从main函数开始,去关注程序的编写、编译和执行过程。main函数之前,系统有没有做一些其他的工作,做了哪些工作呢,一起来看看?
一、应用启动分析
1、程序从编写到执行的过程
1、代码怎么加载到内存?
2、静态库、动态库怎么加载到内存?
3、objc_init -> objc 在哪里执行的?
我们可以想一下以上的过程是怎么发生的,各个环节都是怎么加载到内存的?
2、静态库和动态库
我们经常在项目中使用静态库和动态库,其中系统提供的UIKit,Foundation库,WebKit库等等,这些是动态库,比如我们经常使用的自定义的静态Framework,.a文件,就是属于静态库。那么,静态库和动态库是怎么区分的?
●动态库形式:.dylib和.framework
●静态库形式:.a和.framework
如上图所示,我们分析:
1、静态库:链接时,静态库会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝
2、动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存
但是,系统的动态库怎么加载到内存呢?通过什么方式?这里就用到了一个工具dyld动态链接器。
3、动态链接器
3.1 动态链接器的工作过程
上面是dyld的加载工作流程图,通过它主要进行了动态库的注册和动态库的加载过程。
二、dyld过程初探
●通过上面原理的整体分析后,我们接下来就进行应用程序代码执行逻辑的分析
在main函数的入口位置,加上断点,运行后程序停在断点位置,经过堆栈打印(bt为lldb堆栈打印命令),发现程序是崩溃在了lldb中,这里我们并不能获取更多信息去跟踪。
于是,我们通过编程经验,想到了在程序执行main函数之前,会提前执行load函数的加载,那么就做一下尝试,在ViewController中假如load函数,并添加断点,运行程序。
结果很顺利,我们断点停在了ViewController的load方法,通过堆栈打印,发现了关于dyld的一系列函数过程。下面就引出我们探究的主题:dyld(动态链接器),我们的函数追踪,也将按照这样一个顺序去进行!
1、dyld简介
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。
2、dyld的源码
这里我们选择最新的版本进行研究,技术嘛,总要与时俱进,下载完这些,先不急,先来一个苹果的官方视频介绍,关于dyld2,到dyld3过程的更新、特性,然后我们在下一章介绍 dyld 的探究过程。
3、苹果官方关于dyld的介绍视频
App Startup Time: Past, Present, and Future https://developer.apple.com/videos/play/wwdc2017/413/
三、总结
我们这里介绍了应用启动过程的大概过程分析,主要有:
1、动态库与静态库的概念和理解
2、dyld的概念和链接过程解析
3、我们是如何知道程序是在main函数之前进入了dyld
关于dyld的详细执行过程,下一节单独讲解!
有了这份葵花宝典心法“2023年MySQL面试心经”,让你的面试如鱼得水!!!
1、MySQL 中有哪几种锁?
(1)表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最 高,并发度最低。
(2)行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最 低,并发度也最高。
(3)页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表 锁和行锁之间,并发度一般。
2、MySQL 中有哪些不同的表格?
共有 5 种类型的表格:
(1)MyISAM
(2)Heap
(3)Merge
(4)INNODB
(5)ISAM
3、简述在 MySQL 数据库中 MyISAM 和 InnoDB 的区别
MyISAM:
(1)不支持事务,但是每次查询都是原子的;
(2)支持表级锁,即每次操作是对整个表加锁;
(3)存储表的总行数;
(4)一个 MYISAM 表有三个文件:索引文件、表结构文件、数据文件;
(5)采用菲聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。
InnoDb:
(1)支持 ACID 的事务,支持事务的四种隔离级别;
(2)支持行级锁及外键约束:因此可以支持写并发;
(3)不存储总行数:
(4)一个 InnoDb 引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为 2G),受操作系统文件大小的限制;
(5)主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持 B+树结构,文件的大调整。
4、MySQL 中 InnoDB 支持的四种事务隔离级别名称,以及逐级之间的区别
SQL 标准定义的四个隔离级别为:
(1)read uncommited :读到未提交数据
(2)read committed:脏读,不可重复读
(3)repeatable read:可重读
(4)serializable :串行事物
5、CHAR 和 VARCHAR 的区别?
(1)CHAR 和 VARCHAR 类型在存储和检索方面有所不同
(2)CHAR 列长度固定为创建表时声明的长度,长度值范围是 1 到 255 当 CHAR值被存储时,它们被用空格填充到特定长度,检索 CHAR 值时需删除尾随空格。
6、主键和候选键有什么区别?
表格的每一行都由主键唯一标识,一个表只有一个主键。
主键也是候选键。按照惯例,候选键可以被指定为主键,并且可以用于任何外键引用。
7、myisamchk 是用来做什么的?
它用来压缩 MyISAM 表,这减少了磁盘或内存使用。
MyISAM Static 和 MyISAM Dynamic 有什么区别?
在 MyISAM Static 上的所有字段有固定宽度。动态 MyISAM 表将具有像 TEXT,BLOB 等字段,以适应不同长度的数据类型。
MyISAM Static 在受损情况下更容易恢复。
8、如果一个表有一列定义为 TIMESTAMP,将发生什么?
每当行被更改时,时间戳字段将获取当前时间戳。
列设置为 AUTO INCREMENT 时,如果在表中达到最大值,会发生什么情况?
它会停止递增,任何进一步的插入都将产生错误,因为密钥已被使用。
怎样才能找出最后一次插入时分配了哪个自动增量?
LAST_INSERT_ID 将返回由 Auto_increment 分配的最后一个值,并且不需要指定表名称。
9、你怎么看到为表格定义的所有索引?
索引是通过以下方式为表格定义的:
SHOW INDEX FROM
10、LIKE 声明中的%和_是什么意思?
%对应于 0 个或更多字符,_只是 LIKE 语句中的一个字符。
如何在 Unix 和 MySQL 时间戳之间进行转换?
UNIX_TIMESTAMP 是从 MySQL 时间戳转换为 Unix 时间戳的命令
FROM_UNIXTIME 是从 Unix 时间戳转换为 MySQL 时间戳的命令
**11、列对比运算符是什么?
** 在 SELECT 语句的列比较中使用=,<>,<=,<,> =,>,<<,>>,<=>,AND,OR 或 LIKE 运算符。
12、BLOB 和 TEXT 有什么区别?
BLOB 是一个二进制对象,可以容纳可变数量的数据。TEXT 是一个不区分大小写的 BLOB。
BLOB 和 TEXT 类型之间的唯一区别在于对 BLOB 值进行排序和比较时区分大小写,对 TEXT 值不区分大小写。
13、MySQL_fetch_array 和 MySQL_fetch_object 的区别是什么?
以下是 MySQL_fetch_array 和 MySQL_fetch_object 的区别:
MySQL_fetch_array() – 将结果行作为关联数组或来自数据库的常规数组返回。
MySQL_fetch_object – 从数据库返回结果行作为对象。
14、MyISAM 表格将在哪里存储,并且还提供其存储格式? 每个 MyISAM 表格以三种格式存储在磁盘上:
(1)·“.frm”文件存储表定义
(2)·数据文件具有“.MYD”(MYData)扩展名
(3)索引文件具有“.MYI”(MYIndex)扩展名
15、MySQL 如何优化 DISTINCT?
DISTINCT 在所有列上转换为 GROUP BY,并与 ORDER BY 子句结合使用。
SELECT DISTINCT t1.a FROM t1,t2 where t1.a=t2.a;
16、如何显示前 50 行?
在 MySQL 中,使用以下代码查询显示前 50 行:SELECT*FROMLIMIT 0,50;
17、可以使用多少列创建索引?
任何标准表最多可以创建 16 个索引列。
18、NOW()和 CURRENT_DATE()有什么区别?
NOW()命令用于显示当前年份,月份,日期,小时,分钟和秒。
CURRENT_DATE()仅显示当前年份,月份和日期。
19、什么是非标准字符串类型?
(1)TINYTEXT
(2)TEXT
(3)MEDIUMTEXT
(4)LONGTEXT
20、什么是通用 SQL 函数?
(1)CONCAT(A, B) – 连接两个字符串值以创建单个字符串输出。通常用于将两个或多个字段合并为一个字段。
(2)FORMAT(X, D)- 格式化数字 X 到 D 有效数字。
(3)CURRDATE(), CURRTIME()- 返回当前日期或时间。
(4)NOW() – 将当前日期和时间作为一个值返回。
(5)MONTH(),DAY(),YEAR(),WEEK(),WEEKDAY() – 从日期值中提取给定数据。
(6)HOUR(),MINUTE(),SECOND() – 从时间值中提取给定数据。
(7)DATEDIFF(A,B) – 确定两个日期之间的差异,通常用于计算年龄
(8)SUBTIMES(A,B) – 确定两次之间的差异。
(9)FROMDAYS(INT) – 将整数天数转换为日期值。
21、MySQL 支持事务吗?
在缺省模式下,MySQL 是 autocommit 模式的,所有的数据库更新操作都会即时提交,所以在缺省情况下,MySQL 是不支持事务的。
但是如果你的 MySQL 表类型是使用 InnoDB Tables 或 BDB tables 的话,你的MySQL 就可以使用事务处理,
使用 SETAUTOCOMMIT=0 就可以使 MySQL 允许在非 autocommit 模式,在非autocommit 模式下,你必须使用 COMMIT 来提交你的更改,或者用 ROLLBACK来回滚你的更改。
22、MySQL 里记录货币用什么字段类型好
NUMERIC 和 DECIMAL 类型被MySQL 实现为同样的类型,这在 SQL92 标准允许。
他们被用于保存值,该值的准确精度是极其重要的值,例如与金钱有关的数据。当声明一个类是这些类型之一时,精度和规模的能被(并且通常是)指定。
例如:salary DECIMAL(9,2)在这个例子中,9(precision)代表将被用于存储值的总的小数位数,而 2(scale)代 表将被用于存储小数点后的位数。因此,在这种情况下,能被存储在 salary 列中的值的范围是从-9999999.99 到9999999.99。
23、MySQL 有关权限的表都有哪几个?
MySQL 服务器通过权限表来控制用户对数据库的访问,权限表存放在 MySQL 数据库里,由 MySQL_install_db 脚本初始化。
这些权限表分别 user,db,table_priv,columns_priv 和 host。
24、列的字符串类型可以是什么?
字符串类型是:
(1)SET2
(2)BLOB
(3)ENUM
(4)CHAR
(5)TEXT
25、MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?
(1)设计良好的数据库结构,允许部分数据冗余,尽量避免 join 查询,提高效率。
(2)选择合适的表字段数据类型和存储引擎,适当的添加索引。
(3)MySQL 库主从读写分离。
(4)找规律分表,减少单表中的数据量提高查询速度。
(5)添加缓存机制,比如 memcached,apc 等。
(6)不经常改动的页面,生成静态页面。
(7)书写高效率的 SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE.
26、锁的优化策略
(1)读写分离
(2)分段加锁
(3)减少锁持有的时间
(4)多个线程尽量以相同的顺序去获取资源不能将锁的粒度过于细化,不然可能会出现线程的加锁和释放次数过多,反而效率不如一次加一把大锁。
27、索引的底层实现原理和优化
B+树,经过优化的 B+树
主要是在所有的叶子结点中增加了指向下一个叶子节点的指针,因此 InnoDB 建议为大部分表使用默认自增的主键作为主索引。
28、什么情况下设置了索引但无法使用
(1)以“%”开头的 LIKE 语句,模糊匹配
(2)OR 语句前后没有同时使用索引
(3)数据类型出现隐式转化(如 varchar 不加单引号的话可能会自动转换为 int 型)
29、实践中如何优化 MySQL
最好是按照以下顺序优化:
(1)SQL 语句及索引的优化
(2)数据库表结构的优化
(3)系统配置的优化
(4)硬件的优化
30、优化数据库的方法
(1)选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置 NOTNULL,例如’省份’、’性别’最好适用 ENUM
(2)使用连接(JOIN)来代替子查询
(3)适用联合(UNION)来代替手动创建的临时表
(4)事务处理
(5)锁定表、优化事务处理
(6)适用外键,优化锁定表
(7)建立索引
(8)优化查询语句
31、简单描述 MySQL 中,索引,主键,唯一索引,联合索引的区别,对数据库的性能有什么影响(从读写两方面)
索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
普通索引(由关键字 KEY 或 INDEX 定义的索引)的唯一任务是加快对数据的访问速度。普通索引允许被索引的数据列包含重复的值。
如果能确定某个数据列将只包含彼此各不相同的值,在为这个数据列创建索引的时候就应该用关键字 UNIQUE 把它定义为一个唯一索引。
也就是说,唯一索引可以保证数据记录的唯一性。主键,是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字 PRIMARY KEY 来创建。索引可以覆盖多个数据列,如像 INDEX(columnA, columnB)索引,这就是联合索引。
索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件。
32、数据库中的事务是什么?
事务(transaction)是作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功,则认为事务成功,即使只有一个操作失败,事务也不成功。如果所有操作完成,事务则提交,其修改将作用于所有其他数据库进程。
如果一个操作失败,则事务将回滚,该事务所有操作的影响都将取消。
事务特性:
(1)原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。
(2)一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态。
(3)隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务。
(4)持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。或者这样理解:事务就是被绑定在一起作为一个逻辑工作单元的 SQL 语句分组,如果任何一个语句操作失败那么整个操作就被失败,以后操作就会回滚到操作前状态,或者是上有个节点。
为了确保要么执行,要么不执行,就可以使用事务。要将有组语句作为事务考虑,就需要通过 ACID 测试,即原子性,一致性,隔离性和持久性。
33、SQL 注入漏洞产生的原因?如何防止?
SQL 注入产生的原因:程序开发过程中不注意规范书写
sql 语句和对特殊字符进行过滤,导致客户端可以通过全局变量 POST 和 GET 提交一些 sql 语句正常执行。
防止 SQL 注入的方式:
开启配置文件中的 magic_quotes_gpc 和 magic_quotes_runtime 设置执行 sql 语句时使用 addslashes 进行 sql 语句转换Sql 语句书写尽量不要省略双引号和单引号。
过滤掉 sql 语句中的一些关键词:update、insert、delete、select、 * 。
提高数据库表和字段的命名技巧,对一些重要的字段根据程序的特点命名,取不易被猜到的。
34、为表中得字段选择合适得数据类型
字段类型优先级:
整形>date,time>enum,char>varchar>blob,text优先考虑数字类型,其次是日期或者二进制类型,最后是字符串类型,同级别得数据类型,应该优先选择占用空间小的数据类型
35、存储时期
Datatime:以 YYYY-MM-DD HH:MM:SS 格式存储时期时间,精确到秒,占用 8 个字节得存储空间,datatime 类型与时区无关Timestamp:以时间戳格式存储,占用 4 个字节,范围小 1970-1-1 到 2038-1-19,显示依赖于所指定得时区,默认在第一个列行的数据修改时可以自动得修改timestamp 列得值Date:(生日)占用得字节数比使用字符串.datatime.int 储存要少,使用 date 只需要 3 个字节,
存储日期月份,还可以利用日期时间函数进行日期间得计算Time:存储时间部分得数据注意:不要使用字符串类型来存储日期时间数据(通常比字符串占用得储存空间小,在进行查找过滤可以利用日期得函数)
使用 int 存储日期时间不如使用 timestamp 类型
36、对于关系型数据库而言,索引是相当重要的概念,请回答有关索引的几个问题:
(1)索引的目的是什么?
快速访问数据表中的特定信息,提高检索速度创建唯一性索引,保证数据库表中每一行数据的唯一性。
加速表和表之间的连接使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间
(2)索引对数据库系统的负面影响是什么?
负面影响:创建索引和维护索引需要耗费时间,这个时间随着数据量的增加而增加;索引需要占用物理空间,不光是表需要占用数据空间,每个索引也需要占用物理空间;当对表进行增、删、改、的时候索引也要动态维护,这样就降低了数据的维护速度。
(3)为数据表建立索引的原则有哪些?
在最频繁使用的、用以缩小查询范围的字段上建立索引。在频繁使用的、需要排序的字段上建立索引
(4)什么情况下不宜建立索引?
对于查询中很少涉及的列或者重复值比较多的列,不宜建立索引。对于一些特殊的数据类型,不宜建立索引,比如文本字段(text)等
37、解释 MySQL 外连接、内连接与自连接的区别
先说什么是交叉连接: 交叉连接又叫笛卡尔积,它是指不使用任何条件,直接将一个表的所有记录和另一个表中的所有记录一一匹配。内连接 则是只有条件的交叉连接,根据某个条件筛选出符合条件的记录,不符合条件的记录不会出现在结果集中,即内连接只连接匹配的行。外连接 其结果集中不仅包含符合连接条件的行,而且还会包括左表、右表或两个表中的所有数据行,这三种情况依次称之为左外连接,右外连接,和全外连接。
左外连接,也称左连接,左表为主表,左表中的所有记录都会出现在结果集中,对于那些在右表中并没有匹配的记录,仍然要显示,右边对应的那些字段值以NULL 来填充。
右外连接,也称右连接,右表为主表,右表中的所有记录都会出现在结果集中。左连接和右连接可以互换,MySQL 目前还不支持全外连接。
38、Myql 中的事务回滚机制概述
事务是用户定义的一个数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的工作单位,事务回滚是指将该事务已经完成的对数据库的更新操作撤销。
要同时修改数据库中两个不同表时,如果它们不是一个事务的话,当第一个表修改完,可能第二个表修改过程中出现了异常而没能修改,此时就只有第二个表依旧是未修改之前的状态,而第一个表已经被修改完毕。而当你把它们设定为一个事务的时候,当第一个表修改完,第二表修改出现异常而没能修改,第一个表和第二个表都要回到未修改的状态,这就是所谓的事务回滚
# 39、SQL 语言包括哪几部分?每部分都有哪些操作关键字?
SQL 语言包括数据定义(DDL)、数据操纵(DML),数据控制(DCL)和数据查询(DQL) 四个部分。
数据定义:Create Table,Alter Table,Drop Table, Craete/Drop Index 等
数据操纵:Select ,insert,update,delete
数据控制:grant,revoke
数据查询:select
40、完整性约束包括哪些?
数据完整性(Data Integrity)是指数据的精确(Accuracy)和可靠性(Reliability)。分为以下四类:
(1)实体完整性:规定表的每一行在表中是惟一的实体。
(2)域完整性:是指表中的列必须满足某种特定的数据类型约束,其中约束又包括取值范围、精度等规定。
(3)参照完整性:是指两个表的主关键字和外关键字的数据应一致,保证了表之间的数据的一致性,防止了数据丢失或无意义的数据在数据库中扩散。
(4)用户定义的完整性:不同的关系数据库系统根据其应用环境的不同,往往还需要一些特殊的约束条件。用户定义的完整性即是针对某个特定关系数据库的约束条件,它反映某一具体应用必须满足的语义要求。
与表有关的约束:包括列约束(NOT NULL(非空约束))和表约束(PRIMARY KEY、foreign key、check、UNIQUE) 。
41、什么是锁?
数据库是一个多用户使用的共享资源。
当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。
加锁是实现数据库并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。
基本锁类型:锁包括行级锁和表级锁
42、什么叫视图?游标是什么?
视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,视图通常是有一个表或者多个表的行或列的子集。
对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。游标:是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。
43、什么是存储过程?用什么来调用?
存储过程是一个预编译的 SQL 语句,优点是允许模块化的设计,就是说只需创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次 SQL,使用存储过程比单纯 SQL 语句执行要快。可以用一个命令对象来调用存储过程。
44、如何通俗地理解三个范式?
第一范式:1NF 是对属性的原子性约束,要求属性具有原子性,不可再分解;
第二范式:2NF 是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;
第三范式:3NF 是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。。
范式化设计优缺点:
优点:可以尽量得减少数据冗余,使得更新快,体积小
缺点:对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引优化
反范式化:
优点:可以减少表得关联,可以更好得进行索引优化
缺点:数据冗余以及数据异常,数据得修改需要更多的成本
45、什么是基本表?什么是视图?
基本表是本身独立存在的表,在 SQL 中一个关系就对应一个表。
视图是从一个或几个基本表导出的表。视图本身不独立存储在数据库中,是一个虚表
46、试述视图的优点?
(1) 视图能够简化用户的操作
(2) 视图使用户能以多种角度看待同一数据;
(3) 视图为数据库提供了一定程度的逻辑独立性;
(4)视图能够对机密数据提供安全保护。
47、 NULL 是什么意思
NULL 这个值表示 UNKNOWN(未知):它不表示“”(空字符串)。对 NULL 这个值的任何比较都会生产一个 NULL 值。您不能把任何值与一个 NULL 值进行比较,并在逻辑上希望获得一个答案。使用 IS NULL 来进行 NULL 判断
48、主键、外键和索引的区别?
主键、外键和索引的区别
定义:
主键——唯一标识一条记录,不能有重复的,不允许为空外键——表的外键是另一表的主键, 外键可以有重复的, 可以是空值索引——该字段没有重复值,但可以有一个空值
作用:
主键——用来保证数据完整性外键——用来和其他表建立联系用的索引——是提高查询排序的速度
个数:
主键—— 主键只能有一个外键—— 一个表可以有多个外键索引—— 一个表可以有多个唯一索引
49、你可以用什么来确保表格里的字段只接受特定范围里的值?
Check 限制,它在数据库表格里被定义,用来限制输入该列的值。触发器也可以被用来限制数据库表格里的字段能够接受的值,但是这种办法要求触发器在表格里被定义,这可能会在某些情况下影响到性能。
50、说说对 SQL 语句优化有哪些方法?(选择几条)
(1)Where 子句中:where 表之间的连接必须写在其他 Where 条件之前,那些可以过滤掉最大数量记录的条件必须写在 Where 子句的末尾.HAVING 最后。
(2)用 EXISTS 替代 IN、用 NOT EXISTS 替代 NOT IN。
(3) 避免在索引列上使用计算
(4)避免在索引列上使用 IS NULL 和 IS NOT NULL
(5)对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
(6)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
(7)应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
支言片语剖析Java中Lambda表达式之美
摘要:此篇文章主要介绍Java8 Lambda 表达式产生的背景和用法,以及 Lambda 表达式与匿名类的不同等。本文系OneAPM工程师编译整理。
Java是一流的面向对象语言,除了部分简单数据类型,Java 中的一切都是对象,即使数组也是一种对象,每个类创建的实例也是对象。在 Java 中定义的函数或方法不可能完全独立,也不能将方法作为参数或返回一个方法给实例。
从 Swing 开始,我们总是通过匿名类给方法传递函数功能,以下是旧版的事件监听代码:
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
//Event listener implementation goes here...
}
});
在上面的例子里,为了给 Mouse 监听器添加自定义代码,我们定义了一个匿名内部类 MouseAdapter 并创建了它的对象,通过这种方式,我们将一些函数功能传给 addMouseListener 方法。
简而言之,在 Java 里将普通的方法或函数像参数一样传值并不简单,为此,Java 8 增加了一个语言级的新特性,名为Lambda 表达式。
为什么 Java 需要 Lambda 表达式?
如果忽视注解(Annotations)、泛型(Generics)等特性,自 Java 语言诞生时起,它的变化并不大。Java 一直都致力维护其对象至上的特征,在使用过 JavaScript 之类的函数式语言之后,Java 如何强调其面向对象的本质,以及源码层的数据类型如何严格变得更加清晰可感。其实,函数对 Java 而言并不重要,在 Java 的世界里,函数无法独立存在。
在函数式编程语言中,函数是一等公民,它们可以独立存在,你可以将其赋值给一个变量,或将他们当做参数传给其他函数。JavaScript 是最典型的函数式编程语言。点击此处以及此处可以清楚了解 JavaScript 这种函数式语言的好处。函数式语言提供了一种强大的功能——闭包,相比于传统的编程方法有很多优势,闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。Java 现在提供的最接近闭包的概念便是 Lambda 表达式,虽然闭包与 Lambda 表达式之间存在显著差别,但至少 Lambda 表达式是闭包很好的替代者。
在 Steve Yegge 辛辣又幽默的博客文章里,描绘了 Java 世界是如何严格地以名词为中心的,如果你还没看过,赶紧去读吧,写得非常风趣幽默,而且恰如其分地解释了为什么 Java 要引进 Lambda 表达式。
Lambda 表达式为 Java 添加了缺失的函数式编程特点,使我们能将函数当做一等公民看待。尽管不完全正确,我们很快就会见识到 Lambda 与闭包的不同之处,但是又无限地接近闭包。在支持一类函数的语言中,Lambda 表达式的类型将是函数。但是,在 Java 中,Lambda 表达式是对象,他们必须依附于一类特别的对象类型——函数式接口(functional interface)。我们会在后文详细介绍函数式接口。
Mario Fusco 的这篇思路清晰的文章介绍了为什么 Java 需要 Lambda 表达式。他解释了为什么现代编程语言必须包含闭包这类特性。
Lambda 表达式简介
Lambda 表达式是一种匿名函数(对 Java 而言这并不完全正确,但现在姑且这么认为),简单地说,它是没有声明的方法,也即没有访问修饰符、返回值声明和名字。
你可以将其想做一种速记,在你需要使用某个方法的地方写上它。当某个方法只使用一次,而且定义很简短,使用这种速记替代之尤其有效,这样,你就不必在类中费力写声明与方法了。
Java 中的 Lambda 表达式通常使用(argument) -> (body)
语法书写,例如:
(arg1, arg2...) -> { body }
(type1 arg1, type2 arg2...) -> { body }
以下是一些 Lambda 表达式的例子:
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 };
Lambda 表达式的结构
让我们了解一下 Lambda 表达式的结构。
- 一个 Lambda 表达式可以有零个或多个参数
- 参数的类型既可以明确声明,也可以根据上下文来推断。例如:
(int a)
与(a)
效果相同 - 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:
(a, b)
或(int a, int b)
或(String a, int b, float c)
- 空圆括号代表参数集为空。例如:
() -> 42
- 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:
a -> return a*a
- Lambda 表达式的主体可包含零条或多条语句
- 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致
- 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空
什么是函数式接口
在 Java 中,Marker(标记)类型的接口是一种没有方法或属性声明的接口,简单地说,marker 接口是空接口。相似地,函数式接口是只包含一个抽象方法声明的接口。
java.lang.Runnable
就是一种函数式接口,在 Runnable 接口中只声明了一个方法void run()
,相似地,ActionListener 接口也是一种函数式接口,我们使用匿名内部类来实例化函数式接口的对象,有了 Lambda 表达式,这一方式可以得到简化。
每个 Lambda 表达式都能隐式地赋值给函数式接口,例如,我们可以通过 Lambda 表达式创建 Runnable 接口的引用。
Runnable r = () -> System.out.println("hello world");
当不指明函数式接口时,编译器会自动解释这种转化:
new Thread(
() -> System.out.println("hello world")
).start();
因此,在上面的代码中,编译器会自动推断:根据线程类的构造函数签名public Thread(Runnable r) { }
,将该 Lambda 表达式赋给 Runnable 接口。
以下是一些 Lambda 表达式及其函数式接口:
Consumer<Integer> c = (int x) -> { System.out.println(x) };
BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);
Predicate<String> p = (String s) -> { s == null };
@FunctionalInterface是 Java 8 新加入的一种接口,用于指明该接口类型声明是根据 Java 语言规范定义的函数式接口。Java 8 还声明了一些 Lambda 表达式可以使用的函数式接口,当你注释的接口不是有效的函数式接口时,可以使用 @FunctionalInterface 解决编译层面的错误。
以下是一种自定义的函数式接口: @FunctionalInterface public interface WorkerInterface {
public void doSomeWork();
}
根据定义,函数式接口只能有一个抽象方法,如果你尝试添加第二个抽象方法,将抛出编译时错误。例如:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
public void doSomeMoreWork();
}
错误:
Unexpected @FunctionalInterface annotation
@FunctionalInterface ^ WorkerInterface is not a functional interface multiple
non-overriding abstract methods found in interface WorkerInterface 1 error
函数式接口定义好后,我们可以在 API 中使用它,同时利用 Lambda 表达式。例如:
//定义一个函数式接口
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
public class WorkerInterfaceTest {
public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}
public static void main(String [] args) {
//invoke doSomeWork using Annonymous class
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker invoked using Anonymous class");
}
});
//invoke doSomeWork using Lambda expression
execute( () -> System.out.println("Worker invoked using Lambda expression") );
}
}
输出:
Worker invoked using Anonymous class
Worker invoked using Lambda expression
这上面的例子里,我们创建了自定义的函数式接口并与 Lambda 表达式一起使用。execute() 方法现在可以将 Lambda 表达式作为参数。
Lambda 表达式举例
学习 Lambda 表达式的最好方式是学习例子。
线程可以通过以下方法初始化:
//旧方法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
//新方法:
new Thread(
() -> System.out.println("Hello from thread")
).start();
事件处理可以使用 Java 8 的 Lambda 表达式解决。下面的代码中,我们将使用新旧两种方式向一个 UI 组件添加 ActionListener:
//Old way:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("The button was clicked using old fashion code!");
}
});
//New way:
button.addActionListener( (e) -> {
System.out.println("The button was clicked. From Lambda expressions !");
});
- ````js`
以下代码的作用是打印出给定数组中的所有元素。注意,使用 Lambda 表达式的方法不止一种。在下面的例子中,我们先是用常用的箭头语法创建 Lambda 表达式,之后,使用 Java 8 全新的双冒号(::)操作符将一个常规方法转化为 Lambda 表达式:
```js
//Old way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
System.out.println(n);
}
//New way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
//or we can use :: double colon operator in Java 8
list.forEach(System.out::println);
在下面的例子中,我们使用断言(Predicate)函数式接口创建一个测试,并打印所有通过测试的元素,这样,你就可以使用 Lambda 表达式规定一些逻辑,并以此为基础有所作为:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Main {
public static void main(String [] a) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
System.out.println("Print all numbers:");
evaluate(list, (n)->true);
System.out.println("Print no numbers:");
evaluate(list, (n)->false);
System.out.println("Print even numbers:");
evaluate(list, (n)-> n%2 == 0 );
System.out.println("Print odd numbers:");
evaluate(list, (n)-> n%2 == 1 );
System.out.println("Print numbers greater than 5:");
evaluate(list, (n)-> n > 5 );
}
public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.println(n + " ");
}
}
}
}
输出:
Print all numbers: 1 2 3 4 5 6 7
Print no numbers:
Print even numbers: 2 4 6
Print odd numbers: 1 3 5 7
Print numbers greater than 5: 6 7
下面的例子使用 Lambda 表达式打印数值中每个元素的平方,注意我们使用了 .stream() 方法将常规数组转化为流。Java 8 增加了一些超棒的流 APIs。java.util.stream.Stream接口包含许多有用的方法,能结合 Lambda 表达式产生神奇的效果。我们将 Lambda 表达式x -> x*x
传给 map() 方法,该方法会作用于流中的所有元素。之后,我们使用 forEach 方法打印数据中的所有元素:
//Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
int x = n * n;
System.out.println(x);
}
//New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
下面的例子会计算给定数值中每个元素平方后的总和。请注意,Lambda 表达式只用一条语句就能达到此功能,这也是 MapReduce 的一个初级例子。我们使用 map() 给每个元素求平方,再使用 reduce() 将所有元素计入一个数值:
//Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = 0;
for(Integer n : list) {
int x = n * n;
sum = sum + x;
}
System.out.println(sum);
//New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get();
System.out.println(sum);
Lambda 表达式与匿名类的区别
使用匿名类与 Lambda 表达式的一大区别在于关键词的使用。对于匿名类,关键词this
解读为匿名类,而对于 Lambda 表达式,关键词this
解读为写就 Lambda 的外部类。
Lambda 表达式与匿名类的另一不同在于两者的编译方法。Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有函数,它使用 Java 7 中新加的invokedynamic
指令动态绑定该方法,关于 Java 如何将 Lambda 表达式编译为字节码,Tal Weiss 写了一篇很好的文章。
到此为止啦,亲们!
Mark Reinhold,甲骨文的首席架构师,将 Lambda 表达式描述为该编程模型最大的提升——比泛型(generics)还强大。事实的确如此,Lambda 表达式赋予了 Java程序员相较于其他函数式编程语言缺失的特性,结合虚拟扩展方法之类的特性,Lambda 表达式能写出一些极好的代码。
希望这篇文章能让您对Java 8的新特性所有了解。
原文地址:http://viralpatel.net/blogs/Lambda-expressions-java-tutorial/
OneAPM for Java能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问OneAPM 官方博客。
TCP|IP:一文告诉你互联网域名原理
1.4 互联网的地址
**
互联网上的每个接口必须有一个唯一的INTERNET地址(也称作IP地址)。I P地址长32BIT。INTERNET地址并不采用平面形式的地址空间,如1、2、3等。IP地址具有一定的结构,五类不同的互联网地址格式如图1 - 5所示。
这些3 2位的地址通常写成四个十进制的数,其中每个整数对应一个字节。这种表示方法称作“点分十进制表示法(DOTTED DECIMAL NOTATION)”。例如,作者的系统就是一个B类地址,它表示为:140.252.13.33。
区分各类地址的最简单方法是看它的第一个十进制整数。图1-6列出了各类地址的起止范围,其中第一个十进制整数用加黑字体表示。
需要再次指出的是,多接口主机具有多个IP地址,其中每个接口都对应一个IP地址。
由于互联网上的每个接口必须有一个唯一的IP地址,因此必须要有一个管理机构为接入互联网的网络分配IP地址。这个管理机构就是互联网络信息中心( INTERNET NETWORK INFORMATION CENTRE),称作INTERNIC。INTERNIC只分配网络号。主机号的分配由系统管理员来负责。
INTERNET注册服务(IP地址和DNS域名)过去由NIC来负责,其网络地址NIC.DDN.MIL。
1993年4月1日,INTERNIC成立。现在,NIC只负责处理国防数据网的注册请求,所有其他的INTERNET用户注册请求均由INTERNIC负责处理,其网址是:RS.INTERNIC.NET。
事实上INTERNIC由三部分组成:注册服务(RS.INTERNIC.NET),目录和数据库服务(DS.INTERNIC.NET),以及信息服务(IS.INTERNIC.N E T)。有关INTERNIC的其他信息参见习题1.8。
有三类IP地址:单播地址(目的为单个主机)、广播地址(目的端为给定网络上的所有主机)以及多播地址(目的端为同一组内的所有主机)。第12章和第13章将分别讨论广播和多播的更多细节。
在3.4节中,我们在介绍IP选路以后将进一步介绍子网的概念。图3-9给出了几个特殊的IP地址:主机号和网络号为全0或全1。
懂得了IOS的EXC_BAD_ACCESS,SIGSEGV,SIGBUS,EXC_ARITHETIC,SIGABRT崩溃类型,连苹果公司都拿你没办法
iOS中的崩溃类型
在这里了解一下XCode用来表示各种崩溃类型的术语,补充一些这方面的各知识。崩溃通常是指操作系统向正在运行的程序发送的信号,所以我们在查看崩溃日志时,常常看到如下错误摘要:Application received signal SIGSEGV。一般来说,常见的崩溃类型有以下几种:
1、 EXC_BAD_ACCESS
在访问一个已经释放的对象或向它发送消息时,EXC_BAD_ACCESS就会出现。造成EXC_BAD_ACCESS最常见的原因是,在初始化方法中初始化变量时用错了所有权修饰符,这会导致对象过早地被释放。举个例子,在viewDidLoad方法中为UIViewController创建了一个包含元素的NSArray,却将该数组的所有权修饰符设成了assign而不是strong。现在在viewWillAppear中,若要访问已经释放掉的对象时,就会得到名为EXC_BAD_ACCESS的崩溃。
这个崩溃发生时,查看崩溃日志,却往往得不到有用的栈信息。还好,有一个方法用来解决这个问题:NSZombieEnabled。
这是一个环境变量,用来调试与内存相关的问题,跟踪对象的释放过程。启用了NSZombieEnabled的话,它会用一个僵尸实现来去你的默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。
所以,当在应用中启用NSZombie而不是让应用直接崩溃时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接收到的消息,然后跳入调试器,这样你就可以查看到底哪时出了问题。
僵尸在RAC出现以前作用很大。但自从有了ARC,如果你在对象的所有权方面比较注意,那么通常不会碰到内存相关的崩溃。
2、 SIGSEGV
段错误信息(SIGSEGV)是操作系统产生的一个更严重的问题。当硬件出现错误、访问不可读的内存地址或向受保护的内存地址写入数据时,就会发生这个错误。
硬件错误这一情况并不常见。当要读取保存在RAM中的数据,而该位置的RAM硬件有问题时,你会收到SIGSEGV。SIGSEGV更多是出现在后两种情况。默认情况下,代码页不允许进行写操作,而数据而不允许进行执行操作。当应用中的某个指针指向代码页并试图修改指向位置的值时,你会收到SIGSEGV。当要读取一个指针的值,而它被初始化成指向无效内存地址的垃圾值时,你也会收到SIGSEGV。
SIGSEGV错误调试起来更困难,而导致SIGSEGV的最常见原因是不正确的类型转换。要避免过度使用指针或尝试手动修改指针来读取私有数据结构。如果你那样做了,而在修改指针时没有注意内存对齐和填充问题,就会收到SIGSEGV。
3、 SIGBUS
总线错误信号(SIGBUG)代表无效内存访问,即访问的内存是一个无效的内存地址。也就是说,那个地址指向的位置根本不是物理内存地址(它可能是某个硬件芯片的地址)。SIGSEGV和SIGBUS都羽毛球EXC_BAD_ACCESS的子类型。
4、 SIGTRAP
SIGTRAP代表陷阱信号。它并不是一个真正的崩溃信号。它会在处理器执行trap指令发送。LLDB调试器通常会处理此信号,并在指定的断点处停止运行。如果你收到了原因不明的SIGTRAP,先清除上次的输出,然后重新进行构建通常能解决这个问题。
5、 EXC_ARITHETIC
当要除零时,应用会收到EXC_ARITHMETIC信号。这个错误应该很容易解决。
6、 SIGILL
SIGILL代表signal illegal instruction(非法指令信号)。当在处理器上执行非法指令时,它就会发生。执行非法指令是指,将函数指针会给另外一个函数时,该函数指针由于某种原因是坏的,指向了一段已经释放的内存或是一个数据段。有时你收到的是EXC_BADINSTRUCTION而不是SIGILL,虽然它们是一回事,不过EXC*等同于此信号不依赖体系结构。
7、 SIGABRT
SIGABRT代表SIGNAL ABORT(中止信号)。当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制;必要的话,它能要求进程进行清理工作。在调试造成此信号的底层错误时,并没有什么妙招。Cocos2d或UIKit等框架通常会在特定的前提条件没有满足或一些糟糕的情况出现时调用C函数abort(由它来发送此信号)。当SIGABRT出现时,控制台通常会输出大量的信息,说明具体哪里出错了。由于它是可控制的崩溃,所以可以在LLDB控制台上键入bt命令打印出回溯信息。
8、 看门狗超时
这种崩溃通常比较容易分辨,因为错误码是固定的0x8badf00d。(程序员也有幽默的一面,他们把它读作Ate Bad Food。)在iOS上,它经常出现在执行一个同步网络调用而阻塞主线程的情况。因此,永远不要进行同步网络调用。
别等了赶紧上车:零基础入门学习汇编语言第4版|8086汇编入门,能让你受益匪浅
1基础知识
机器语言是机器指令的集合,由0和1组成,但是很长很复杂,汇编语言因此产生。
汇编语言的主体是汇编指令。汇编指令是机器指令的便于记忆的书写格式。
程序员写完汇编指令通过编译器转换为机器码,机器码再传到计算机执行。
汇编语言有以下三类:
1汇编指令:助记符,有对应机器码
2.伪指令:没有对应机器码,编译器执行计算机不执行
3.其他符号:+ -等由编译器识别,没有对应机器码
汇编语言的核心是汇编指令,决定了汇编语言的特性
CPU是计算机的核心部件,他控制整个计算机的运作并运算,指令和数据在存储器中存放,也就是内存。CPU离不开内存。内存中指令和数据没区别,都是二进制。CPU来识别是信息还是指令。
一个存储单元存储1Byte
CPU从内存中读写书,要指定地址,指定进行哪种操作,CPU通过总线连接其他芯片,传输信息
存储单元的地址(地址信息)->地址总线
器件的选择,读或写的命令(控制信息)->控制总线
读或写的数据(数据信息)->数据总线
地址总线
一根导线有两种稳定状态代表0和1,那么10根导线就有2^10次方个不同数据,从0到1023
地址总线的宽度决定了CPU的寻址能力
数据总线
8根数据总线可传送一个8位二进制数据 8bits = 1byte
数据总线的宽度决定了CPU与其他器件进行数据传送时的一次数据量
控制总线
控制总线的宽度决定CPU对外部器件的控制能力
主板上都是核心器件,CPU、存储器等,CPU通过总线向接口卡发送指令,接口卡控制外设进行工作
随机存储器RAM 只读存储器ROM
BIOS(Basic input/putput system)
CPU将各类存储器看作一个逻辑存储器,所有的物理存储器被看作一个由若干个存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。
内存地址空间的大小受CPU地址总线宽度的限制。不同计算机系统内存地址分配情况不同
2.寄存器
CPU由运算器、控制器、寄存器等器件构成,器件靠内部总线连,与之前总线(外)不同
寄存器程序员可以用指令读写
8086CPU的14个寄存器
AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW
通用寄存器:
用来存放一般性数据 AX BX CX DX
16位寄存器可以拆成两个8位寄存器使用 AH AL,高8位和低8位
两字节byte构成一个字word,一个字节8bit放在8位寄存器
汇编指令
mov ax,18
mov ah,78
add ax,8
mov ax,bx
add ax,bx
...
16位寄存器只能存放4位十六进制数据,1044CH最高位的1就不能保存再ax中004CH
独立使用AL这个寄存器如果进位是不会储存在AH中
数据传送和运算时 指令的两个操作对象的位数应当一致
物理地址
所有内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址
CPU向地址总线发出物理地址前,要在内部形成这个物理地址
16位的CPU能够一次性处理、传输、暂时存储16位地址
8086CPU有20位地址总线,内部用两个16位地址合成一个20位 的物理地址
物理地址 = 段地址 x 16(基础地址) + 偏移地址
段地址 x 16 常用说法左移4位(二进制位)相当于16进制左移1位
X 进制左移1位相当于乘 X
因为内部结构是这样,为了达到20位寻址能力,利用两个16位地址可以达到目的
接着段地址,内存没有分段,段的划分来自cpu
段地址 x 16是16的倍数,所以一个段的起始地址也是16的倍数;偏移地址位16位,16位地址的寻址能力位64kb,所以一个段的长度最大为64kb
CPU可以用不同的段地址和偏移地址形成同一个物理地址
8086的4个段寄存器CS、DS、SS、ES。当访问内存由这四个段寄存器提供内存单元段地址
CS为代码段寄存器 IP为指令指针寄存器
任意时刻,设CS内容为M,IP中内容为N,8086CPU从内存M x 16 + N单元开始,读取指令并执行
读取一条指令后,IP中的值自动增加(指令长度),以使CPU可以读取下一条指令
CPU将CS:IP指向的内存地址单元内容看作指令
同时修改CS、IP的内容可以用 jmp 段地址 :偏移地址
仅修改IP可用 jmp 某一合法寄存器(用寄存器中的值修改IP)?
8086机编程时,可以根据需要,将一组内存单元定义为一个段。
将长度为N<=64的代码,存在一组地址连续、起始地址为16的倍数的内存单元作代码段
用CS:IP指向的内容就能让代码段的内容执行
3.寄存器(内存访问)
在内存中存储时,内存单元是一个字节byte单元,则一个字Word要用两个地址连续的内存单元来存放,低位字节存放在低地址单元,高位字节存放在高地址单元
字单元:由两个地址连续的内存单元组成,起始地址为N的字单元简称为N地址字单元
0地址字单元4E21H,1地址字单元124EH......
DS和[address]
8086中有一个DS寄存器,通常用来存放要访问数据的段地址
mov bx,1000H
mov ds,bx
mov al,[0]
将10000H(1000:0)中的数据读到al中
这里[...]表示一个内存单元,括号里面表示偏移地址,指令执行时8086CPU自动读取ds中数据作为内存单元的段地址
8086CPU不支持将数据直接送入段寄存器,只好用一个寄存器中转
16位结构,有16根数线,所以一次可以传送16位数据,也就是一个字
mov add sub都是带有两个操作对象的指令,而jump具有一个
编程时根据需要定义数据段,可以在具体操作的时候用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元
栈
栈是一种具有特殊访问方式(最后进入空间的数据,最先出去)的存储空间
栈的两个基本操作:入栈和出栈,入栈就是将新的元素放到栈顶,出栈就是将栈顶元素取出一个
栈的操作规则被称为:LIFO(Last In First Out,后的进先出来)
编程时,可以将一段内存当作栈
push ax 将ax中数据入栈
pop ax 从栈顶取出数据到ax
操作都是以字为单位进行的
8086CPU中任意时刻,段寄存器SS(栈的段地址):寄存器SP(偏移地址)指向栈顶元素,push指令和pop指令执行时,CPU从SS和SP中得到栈顶地址
push ax两步走
1> SP=SP-2,SS:SP指向当前栈顶前面的单元,作为新栈顶
2> 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶
入栈时,栈顶从高地址向低地址方向增长。
pop ax两步走
1> 将SS:SP指向的内存单元处数据送入ax
2> SP=SP+2,SS:SP指向当前栈顶下面的单元,作为新的栈顶
值得注意的时,出栈后pop操作前的栈顶元素仍然存在,但是它已经不在栈中,再次push后会在那里写入新数据覆盖
由上面数据不在栈中进而可以思考栈顶超界的问题
8086CPU不保证我们对栈的操作不会超界
当我们把一段内存当作栈空间,当栈满时再执行push栈顶超出栈空间,栈空间外数据被覆当栈空时再次执行pop栈顶超出了栈空间,而超出的地方的数据会被覆盖,自己需要注意
用栈暂存以后需要恢复寄存器中的内容时,出栈顺序和入栈顺序相反
push和pop实质上是一种内存传送指令
编程时根据需要可定义栈段
段的综述
对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中内容当作数据来访问
对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令
对于栈段,将它的地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU进行栈操作时,将我们定义的栈段当作栈空间用
由此可见CPU将内存中内容当作什么,是因为相应的段寄存器指向了那里
4.第一个程序
一个源程序从写出到执行的过程
第一步:编写汇编源程序
第二步:对源程序进行编译链接
使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用链接程序对目标文件进行链接,生成可在操作系统中直接运行的可执行文件。
可执行文件包含两部分内容
1)程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序定义的数据)
2)相关的的描述信息(比如,程序多大、占多少内存空间等)
第三步:执行可执行文件中的程序
操作系统依照可执行文件的描述信息,将可执行文件中的机器码和数据载入内存,开始相关初始化,然后由CPU执行
源程序
汇编语言写的源程序,包括伪指令和汇编指令,其中伪指令由编译器来处理,程序是指源程序中由计算机执行、处理的指令或数据
伪指令:
1>segment 和 ends成对使用,功能是定义一个段,segment 说明一个段开始,ends 说明一个段结束
格式: 段名 segment
.
段名 ends
2>end是一个汇编程序的结束标志,编译器碰到它就结束编译,注意区分ends
3>assume 含义为假设。它假设某一段寄存器和程序中的某一个用segment...ends定义的段相关联。
比如code segment ... code ends就定义了一个名为code的段
在程序开头,用assume cs:code 将用作代码段的段code和CPU中的段寄存器cs 联系起来
DOS(一个单任务操作系统)
一个程序p2在可执行文件中,则必须有一个正在运行的程序p1,将p2从可执行文件中加载入内存,将CPU的控制权交给p2,p2才能运行。p2开始运行后,p1暂停运行
而当p2运行完,CPU控制权应交还给p1
这个过程叫做:程序返回
任何通用的操作系统,都要提供一个称为shell(外壳)的程序,用户使用这个程序来操作计算机系统进行工作
DOS启动时,先完成其他重要初始化工作,然后运行command.com,command.com运行后,执行完其他的相关任务后,在屏幕上显示出由当前盘符和当前路径组成的提示符
比如C:\
用户输入的指令 cd dir 等由command执行
执行一个程序,command 首先根据文件名找到可执行文件,然后将可执行文件加载入内存,设置CS:IP指向程序的入口。此后,command 暂停运行,CPU运行程序。程序运行结束后,返回到command中,command 再次显示由当前盘符和当前路径组成的提示符,等待用户输入
在DEBUG中,command将debug加载入内存,而debug将程序加载入内存,所以程序结束后返回到debug中,Q可以返回到command
mov ax,4c00h
int 21h
这两条指令所实现的就是程序返回,在程序末尾使用
edit
编辑程序
masm
汇编编译器,接收默认文件扩展名为 .asm,如果不是就要将文件扩展名写出
输入源程序文件名要指明路径,除非它就在当前路径下
masm 1t.asm / masm 1t
简化过程,最后加上 ; 忽略中间文件的生成
link
链接器,接收默认文件扩展名 .obj,如果不是就要将文件扩展名写出
对编译生成的目标文件进行链接,从而得到可执行程序。 输入目标文件名要指明路径,除非它就在当前路径下
link 1t.obj / link 1t
简化过程,最后加 ; 忽略中间文件的生成
学习汇编主要目的,通过用汇编语言进行编程而深入地理解计算机底层的基本工作机理,达到可以随心所欲控制计算机的目的。汇编语言编程用到的工具在操作系统是运行,暂时不做过多探究
Debug
数据在Debug中默认所有数据用十六进制表示
遇到int 21h时要用P命令执行
载入.EXE
DOS系统中.EXE文件中程序加载,cx 中存放了程序的长度
.exe装入内存后,程序被装入内存的什么地方?
5.[BX]和loop指令
1.bx
用[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如寄存器)中指出
[bx]同样也表示一个内存单元,它的偏移地址在bx中
inc bx的含义是bx中的内容加1,执行后bx = 2
2.loop
正如它的意思循环
loop指令的格式是: loop 标号
CPU执行 loop指令时两步走(这里圆括号代表一个寄存器或内存单元的内容)
1>(cx)=(cx)-1
2>判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行
通常我们用loop指令来实现循环功能,cx中存放循环次数
程序框架如下
mov cx,循环次数
s:
循环执行的程序段
loop s
调试/执行程序时
大于9FFFh的十六进制数据A000H、A001H...FFFFH在书写时以字母开头,但在汇编源程序中,数据不能以字母开头,所以前面要加0,比如0A001H
在程序执行时 loop s 中的标号 s 已经变为了一个地址
我们只想跟踪循环的过程时,可以用DUBUG里命令G来达到目的,一次执行完标号前的内容,g 0012表示执行程序到当前代码段(段地址在cs中)的0012h处
当进入循环后,我们想要循环一次执行完,可以用p命令来达到目的,DEBUG就会自动重复执行循环中指令,直到(cx)=0为止
Debug和汇编编译器masm对指令不同处理
在Debug中,mov ax,[0] 表示将ds:0处的数据送入ax中
但在汇编源程序中,这个指令被编译器当作指令mov ax,0处理
Dubug将它解释为idata是一个内存单元
编译器将[idata]解释为 idata
目前的方法是将偏移地址送入bx寄存器中,用[bx]的方式来访问内存单元
但是这样比较麻烦,还有一种方法是在[ ]的前面显式地给出段地址所在的段寄存器
段前缀
用于显式地指明内存单元的段地址的ds: cs: ss: es:,在汇编语言中成为段前缀
比如访问2000:0单元
mov ax,2000h
mov ds,ax
mov al,ds:[0]
loop和[bx]的联合应用
在实际编程中,经常会遇到用同一种方法处理地址连续的内存单元中的数据问题。我们需要每次循环的时候,按照同一种方法来改变要访问的内存单元的地址
mov al,[bx] 中bx就可以看作一个代表内存单元地址的变量,我们可以通过改变 bx 中的数值,改变访问的内存单元
计算ffff:0~ffff:b单元中的数据和,结果存储在ds中
1.运算和的结果是字节型数据,范围在0-255之间,12个结果相加不会大于65535,dx能放下
2.不能将ffff:0~ffff:b中的数据累加到ds中,在这里面数据是8位的,不能直接加到16位寄存器dx中,也不能累加到dl中,dl会进位丢失
解决方案
用一个16位寄存器做中介,将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,0
mov dx,0
mov cs,12
s:
mov al,[bx]
mov ah,0
add dx,ax
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
一段安全的空间
8086模式中,随意向一段空间写入内容很危险,这段空间可能存放重要的系统数据或代码
dos和其他合法程序一般都不会使用0:200~0:2ff的256字节空间,使用这段空间是安全的,谨慎起见我们还可以debug查看
6.包含多个段的程序
在代码段中使用数据
之前提到的那一段安全的空间只有256字节,我们需要超过256个字节的空间该怎么办?在操作系统的环境中,合法地通过操作系统取得地空间都是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及系统自己的空间相冲突。
程序取得所需空间的两种方法:
1.在加载程序的时候为程序分配
2.在程序执行的过程中向系统申请
我们若要一个程序在被加载的时候取得所需的空间,则必须要在源程序中做出说明
当可执行文件中的程序被加载入内存时,这些定义的数据同时也被加载入内存。与此同时,我们要处理的数据自然而然地获得了存储空间
dw 0123h,"dw"的含义是定义字形数据即define word
编程计算0123h、0456h、0789h、0abch、0defh、0cbah、0987h的和,结果存在ax中
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0cbah,0987h
start:mov bx,0
mov ax,0
mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start
start这个标号在end后出现。伪指令end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。end指令指明了程序的入口在标号start处
伪指令end描述了程序的结束和程序的入口。在编译、链接后,由"end start"指明的程序入口,被转化为一个入口地址,存储在可执行文件的描述信息中
利用这种方法可以安排程序框架
assume cs:code
code segment
数据
start:
代码
code ends
end start
在代码段中使用栈
我们需要栈空间,当然也要由系统分配,正如上面定义数据,数据就能载入内存。所以可以在程序中通过定义数据来取得一段空间,然后将这段空间作为栈空间使用
dw 0,0,0,0,0,0,0,0
之后合理设置栈顶ss:sp,这段空间就可以当作栈空间
所以描述dw的作用时,可以说用它定义数据,也可以说用它开辟内存空间
将数据、代码、栈放入不同空间
上述内容将他们放在一起程序显得混乱,用到栈空间也小,代码不长,放在一个段没问题(8086模式一个段的容量不能大于64kb)
所以考虑用多个段存放数据、代码
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
code segment
start:mov ax,stack
mov ss,ax
mov sp,20h
mov ax,data
mov ds,ax
mov bx,0
mov cx,8
s:push [bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0:pop [bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
code ends
end start
定义多个段的方法和定义一个段方法一样
对段地址的引用:在程序中段名就相当于一个标号,它代表了段地址
我们定义了三个段,作用如同的名字含义,但是计算机不知道
我们只需设置start的位置在code段(cs:ip),ss:sp指向栈顶,ds指向data段,其他寄存器如bx存放data段中数据的偏移地址,即可按照我们的含义分段了
7.更灵活的定位内存地址的方法
and指令:逻辑与指令,按位进行与运算(对应位全1为1,不然为0)
例如指令
mov al,01100011B
and al, 00111011B
执行后al = 00100011B
通过该指令可将操作对象的相应位设位0,其他位不变
or指令:逻辑或指令,按位进行或运算(对应位有1为1,全0为0)
例如指令
mov al,01100011B
or al, 00111011B
执行后al = 01111011B
通过该指令可将操作对象的相应位设为1,其他位不变。
我们要把人能看懂的信息存储在计算机中,就要对其及进行编码,将其转化为二进制信息进行存储。计算机要将存储的信息显示出来看就需要对其进行解码。
一个文本编辑过程中,就包含着按照ASCII编码规则进行编码和解码:
键入"a"计算机用ASCII码的规则对其进行编码,将转化为61H存储在内存指定的空间中;文本编辑软件从内存中取出61H,将其送到显卡的显存中;工作在文本模式下的显卡,用ASCII码的规则解释显存中的数据,61H当作字符"a",显卡驱动显示器,将字符"a"图像画在屏幕上
汇编程序中'......'的方式指明数据是以字符的形式给出的,编译器将其转换对应ASCII码
db 'unIX'相当于db 75H,6EH,49H,58H
大小写转换问题
首先分析:每个小写字母的ASCII码值比大写字母ASCII码值大20H,通过此方法可以将小写字母转化为大写,可这要先判断字母本身是否是大小写,但目前水平没有达到
再分析:字母ASCII码的二进制,除第五位(从0开始计数)外,大写字母和小写字母其他各位都一样。大写字母第五位为0,小写字母第五位为1
因此可以用and 和 or指令将第五位置0或置1来改变大小写
[bx+idata]
[bx]可以指明一个内存单元,[bx+idata]更灵活的指明内存单元,偏移地址(bx)+idata
段地址在ds中
db 'BaSiC'
我们要把这个字符串全部转为大写,可以把这个字符串看作一个数组,首地址就是B
SI和DI
si和di是8086CPU和bx功能相近的寄存器,si和di不能分成两个8位寄存器来使用。
[bx+si]和[bx+di]
都表示一个内存单元,偏移地址是(bx)+(si),段地址在ds中
[bx+si+idata]和[bx+di+idata]
都表示一个内存单元,偏移地址位(bx)+(si)+idata,段地址在ds中
以上就是CPU提供的多种寻址方式
当我们需要汇编中的循环嵌套时,可以将外层循环的cx数值保存起来,在执行外层循环loop指令前,再恢复外层循环cx数值。
一般来说,在需要暂存数据的时候,我们应该用栈
寻址方式的适当使用,是我们可以以更合理的结构来看待所要处理的数据。而为所要处理的看似杂乱的数据设计一种清晰的数据结构是程序设计的一个关键问题
如何去做个让领导爱听您汇报工作的项目经理
如何做好汇报?
常听到项目经理这样的抱怨, “我们公司从上到下整体项目意识都不强,领导不重视,部门不支持,员工不配合。需要资源的时候得不到,临到发现问题的时候却要让我们背锅。”
遇到问题,我们的第一反应究竟应该是手指冲外,认为问题都是别人的?还是先冷静思考,尽力去寻找解决方案呢?
这是两种截然不同的人生态度。我很欣赏诗人泰戈尔的人生哲学, “世界以痛吻我,我要报之以歌。” 与其抱怨环境不好,不如做出积极的改变去影响周围的环境。
领导不重视,是不是你用了错误的方式来和他沟通?你是否站在他的立场思考过他最想听的是什么?你是否过于关注细节,没有准确清晰地表达你想要得到的支持?......
今天,我们还是用妍妍的故事,来审视一下,如何用“结构化表达”让领导爱听你的审核和工作汇报,进而引发重视,并让其它部门的同事更支持你一些呢?
1
季度刚刚结束,妍妍接到总裁办小美的邮件,请他第二天到总裁办公室汇报一下这次项目的关键结果,大约10分钟。妍妍非常紧张,平时每次会议上的项目报告,都是讲给各个部门经理和管理者听的,总裁以往并没有参加过,这次点名要单独听他汇报项目结果,这还是第一次。
于是,妍妍连夜把项目报告的主要内容做了调整,准备了40多张PPT,包括20个项目的具体内容。可是刚讲了2页,妍妍就感觉到总裁有点不耐烦了。讲到第5页的时候,他直接打断程程说: “不要讲PPT了,直接说重点。” 妍妍当场就懵了,杵在那里,站也不是,坐也不是。
为什么会这样呢?总裁单独辟出时间来听项目结果报告,足以说明他对这件事情的重视程度,可10分钟转瞬即逝,光项目就有20多个,一分钟一个也不止10分钟啊!妍妍心里也很委屈,我说的明明都是重点项目么。那究竟该怎么说才能达到汇报的目的呢?
2
你认为,总裁不满意,真的是因为程程的项目报告中“没有重点”吗?其实,并非总裁听不懂标准的术语,妍妍报告的也确实都是重点,但在有限的10分钟里,妍妍该如何抓住不同听众的关注点,将这样一件看似复杂的事情清晰准确地表达出来呢?
好,咱们引出一个新的概念 —— SCQA架构 ,也就是我刚才提到的 “结构化表达”。
那么什么才是SCQA结构化表达呢?麦肯锡咨询顾问芭芭拉·明托在《金字塔原理》这本书中,提出了一个“结构化表达”工具:SCQA架构。
SCQA是四个英文单词的缩写:
S,即情境(Situation);
C,即复杂性,常意译为冲突(Complication);
Q,即问题(Question);
A,即答案(Answer)。
3
我应该从一个大家常遇到的问题讲起,这叫做 ”场景导入“; 在这个场景下,你会怎么做?还有没有别的做法?这就是 “打破认知”; 接下来就会谈到核心内容,解释这一讲最本质的 ”核心逻辑“; 当然了,只有逻辑和理论还不行,还需要有具体应用的案例帮助消化理解,比如说,《亮甲广告》来分析SCQA:
得了灰指甲 —— 陈述背景S
一个传染俩 —— 在这个背景下发生了冲突C
问我怎么办?—— 站在对方的角度,提出疑惑Q
马上用亮甲!—— 给出解决方案A,这是文案要表达的重点
很多广告用的都是这个套路:用一个观众已经知道的“故事”来建立跟观众的链接(他们有这方面的背景经历和情感痛点),接着用冲突引起共鸣,最后用问题引出解决方案——快买我们的产品!我们将这种作法,称做 “举一反三”; 很多课程结束前有一个“回顾总结” 的环节,这会帮你回顾中心内容,甚至提炼一个朗朗上口,特别好记的金句口诀,比如:历史课程中朝代歌”就这个意思。
其实啊,这个“起承转合五步法”的前三步“场景导入,打破认知,核心逻辑”,就是SCQA架构中的“标准式(SCA)”。先陈述背景情景,再表述冲突,然后给出解决方案。
当然,芭芭拉·明托的 “结构化表达”工具SCQA架构,不仅可以用在共学小课中,这四个字母还变形组合出另外三招,它能帮助你在很多沟通的场合,例如工作汇报,年度总结,报告改进建议等场景下,有效地表达观点和事实,至少不至于被老板打断说“直接说重点”。
好,那我们就分别来看看是哪三种招式。
第一 招 ,结论先行法(ASC):答案 - 背景 - 冲突**
**回到最开始的案例,鉴于时间有限,程程的审核报告汇总可以试着采用下面的方式。
“总裁,我们这次审核得出的最重要的建议是:关于把维修部门的员工从单独的支持部门,打散分配到各个设备使用部门的提议。” ****听, 这就是开门见山,直接抛出答案。 ****
“公司自创立以来,维修技术工人都在专门的支持部门,所有的工作统一安排,工人对维修经理负责,这在公司设备都比较新的几年里可以提升管理效率并且降低人工成本。”听, 这就是背景,把维修部门的组织结构现状做一个完整的交代。 ****
“但是,随着咱们公司设备的逐年老化,日常点检和维护成为设备维修技工的主要工作,前面几次审核都出现了由于设备管理职责不清晰,维修人力资源不足,维修活动调度不及时,生产活动受到设备状态的制约,导致生产停顿带来订单交付滞后和客户索赔的情况。”听, 这就是冲突。 ****
你看,用 “答案 - 背景 - 冲突” 的结论先行法和总裁沟通,他还会不会说“直接说重点”了?你的第一句就是重点。
第二 招 ,突出忧虑式(CSA):冲突 - 背景 - 答案
突出忧虑式,关键在于强调冲突,引发听众的忧虑,从而激发对背景的关注和对答案的兴趣。这一招呢,医生说特别管用。
“哎哟,你这病不轻啊!”这就是冲突。 听到这句话的病人,估计没有人心里不咯噔一下的。
“还好,能治。美国刚刚有一项最新研究成果,通过了FDA认证。”这就是背景。 听到这句话,你一颗悬到嗓子眼的心,总算是放下来了。
“就是 …… 有点贵。”看, 这就是答案。 这时候,估计再贵你也无所谓了。
作为组织健康医生的审核员,这应该也是一个经常被使用的模型,特别是那些对公司的运营存在巨大风险,又需要投入一定资金来改善的审核发现,这个模式通常会起作用。
第三 招 ,突出信心式(QSCA):问题 - 背景 - 冲突 - 答案 ****我们再来举个例子,
“今年我们公司在管理和经营上面临的最大的威胁是什么?”看, 这是一个问题。
“在过去的几年,公司的规模不断突破记录,并且拥有了市场领先的技术和产品。这些,都使我们成为同行业内的标杆企业,各方的眼睛都紧盯着我们。” 这是一个背景。
“但是,我们拥有了足以占据市场的规模,却没有面对市场意外变化的应急计划,一旦发生任何政策或市场的变化,我们目前的优秀业绩将会面临严重打击。”听, 这是一个冲突。
“因此,面对这些潜在威胁,我们公司需要立即成立项目小组,建立并完善一套应对市场和供应链变化的应急机制。“OK, 这是一个答案。
好,小结一下今天的内容,麦肯锡咨询顾问芭芭拉·明托这位神人,教我们认识了结构化表达的SCQA架构。她在《金字塔原理》这本书中,提出了一个“结构化表达”工具:SCQA架构,用标准式(SCA),结论先行式(ASC),突出忧虑式(CSA)和突出信心式(QSCA),提高表 达 的结构性, 让表达更有重点!
半年后,妍妍通过刻意练习结构式表达技巧,不仅在不符合项的开具上更有层次和逻辑,还在工作汇报中得到了总裁的表扬,进一步提升了管理层和其他部门对项目报告的重视程度。机会不是等来的,而是靠努力赚来的!演讲与表达,决不是站在台上的侃侃而谈,它与我们的工作,生活、学习息息相关。期待你将SCQA的心法运用到你的表达中,并形成一种思维习惯。
青山不改,绿水常流,谢谢大家支持!!!
一发就不可收拾:万能的工作报告与复盘汇总让您的领导为您点赞不断(适用于阶段/季度/年中/年终等各类工作总结)
青山不改,绿水常流,谢谢大家支持!!!
带你一起分析mySQL执行sql原理,领略Oracle公司架构师的核心设计思想
1、把MySQL当个黑盒子一样执行SQL语句
我们知道执行了insert语句之后,在表里会多出来一条数据;执行了update语句之后,会对表里的数据进行更改;执行了delete语句之后,会把表里的一条数据删除掉;执行了select语句之后,会从表里查询一些数据出来。
如果语句性能有点差?没关系,在表里建几个索引就可以了!可能这就是目前行业内很多工程师对数据库的一个认知,完全当他是一个黑盒子,来建表以及执行SQL语句。
既然开始学习如何优化,就要打破这种把数据库当黑盒子的认知程度,要深入底层,去探索数据库的工作原理以及生产问题的优化手段!
2、一个不变的原则:网络连接必须让线程来处理
现在假设我们的数据库服务器的连接池中的某个连接接收到了网络请求,假设就是一条SQL语句,那么大家先思考一个问题,谁负责从这个连接中去监听网络请求?谁负责从网络连接里把请求数据读取出来?
我想很多人恐怕都没思考过这个问题,但是如果大家对计算机基础知识有一个简单了解的话,应该或多或少知道一点,那就是网络连接必须得分配给一个线程去进行处理,由一个线程来监听请求以及读取请求数据,比如从网络连接中读取和解析出来一条我们的系统发送过去的SQL语句,如下图:
3、SQL接口:负责处理接收到的SQL语句
接着我们来思考一下,当MySQL内部的工作线程从一个网络连接中读取出来一个SQL语句之后,此时会如何来执行这个SQL语句呢?
其实SQL是一项伟大的发明,他发明了简单易用的数据读写的语法和模型,哪怕是产品经理,或者是运营专员,甚至是销售专员,及时他们不会技术,也能轻松学会使用SQL语句。
但如果你要去执行这个SQL语句,去完成底层数据的增删改查,那这就是一项极度复杂的任务了!
所以MySQL内部首先提供了一个组件,就是SQL接口(SQL Interface),他是一套执行SQL语句的接口,专门用于执行我们发送给MySQL的那些增删改查的SQL语句。
因此MySQL的工作线程接收到SQL语句之后,就会转交给SQL接口去执行,如下图:
4、查询解析器:让MySQL能看懂SQL语句
接着下一个问题来了,SQL接口怎么执行SQL语句呢?你直接把SQL语句交给MySQL,他能看懂和理解这些SQL语句吗?
我们来举个例子,现在有一个这样的SQL语句:
这个SQL语句,我们用人脑是直接就可以处理一下,只要懂SQL语法的人,立马就知道他是什么意思,但是MySQL自己本身也是一个系统,是一个数据库管理系统,他没法直接理解这些SQL语句!
所以此时就有一个关键的组件要出场了,那就是查询解析器
这个查询解析器(Parser)就是负责对SQL语句进行解析的,比如对上面那个SQL语句进行一下拆解,拆解成一下几个部分:
1我们现在要从users表中查询数据
2查询id字段的值等于1的那行数据
3对查出来的那行数据要提取里面的id,name,age三个字段
所谓的SQL解析,就是按照既定的SQL语法,对我们按照SQL语法规则编写的SQL语句进行解析,然后理解这个SQL语句要干什么事情,如下图:
5、查询优化器:选择最优的查询路径
当我们通过解析器理解了SQL语句要干什么之后,接着会找查询优化器(Optimizer)来选择一个最优的查询路径。
可能有的同学这里就不太理解什么是最优的查询路径了,这个看起来有点抽象,当然,这个查询优化器的工作原理,后续我们会重点分析下,大家现在不用去纠结他的原理。
就用我们刚才说的这个例子,我们现在理解了一个SQL要干这么一个事情:我们现在要从“users”表里查询数据,查询“id”字段的值等于1的那行数据,对查出来的那行数据要提取里面的“id, name, age” 三个字段。
事情是明白了,但是到底应该怎么来实现呢?
要完成这件事情我们有以下这几个查询路径(纯属用于理解例子,不代表真实MySQL原理,但是通过这个例子,大家应该能理解最优查询路径的意思):
思路1.直接定位到“users”表中的“id” 字段等于1的一行数据,然后查出来那行数据的“id, name, age”三个字段的值就可以了 思路2.先把"users"表中的每一行数据的“id, name, age”三个字段的值都查出来,然后从这批数据里面过滤出来“id”字段等于1的那行数据的“id, name, age”三个字段
上面这就是一个最简单的SQL语句的两种实现路径,其实我们会发现,要完成这个SQL语句的目标,两个路径都可以做到,但是哪一种最好呢?显然感觉上是第一种查询路径更好一些。
所以查询优化器大概就是干这个的,他会针对你编写的几十行、几百行甚至上千行复杂的SQL语句生成查询路径树,然后从里面选择一条最优的查询路径出来。
相当于他会告诉你,你应该按照一个什么样的步骤和顺序,去执行哪些操作,然后一步一步的把SQL语句就给完成了。如下图:
6、调用存储引擎接口,真正执行SQL语句
最后一步,就是把查询优化器选择的最优查询路径,也就是到底应该按照一个什么样的顺序和步骤去执行这个SQL语句的计划,把这个计划交给底层的存储引擎去真正的执行。这个存储引擎是MySQL的架构设计中很有特色的一个环节。
不知道大家是否思考过,真正在执行SQL语句的时候,要不然是更新数据,要不然是查询数据,那么数据你觉得存放在哪里?
说白了,数据库也不是什么神秘莫测的东西,可以把他理解为本身就是一个类似平时写的图书馆管理系统、电信计费系统、电商订单系统之类的系统罢了。
数据库自己就是一个编程语言写出来的系统而已,然后启动之后也是一个进程,执行他里面的各种代码,也就是我们上面所说的那些东西。所以对数据库而言,我们的数据要不然是放在内存里,要不然是放在磁盘文件里,没什么特殊的地方!
所以我们来思考一下,假设我们的数据有的存放在内存里,有的存放在磁盘文件里,如下图所示:
那么现在问题来了,我们已经知道一个SQL语句要如何执行了,但是我们现在怎么知道哪些数据在内存里?哪些数据在磁盘里?我们执行的时候是更新内存的数据?还是更新磁盘的数据,是先查询哪个磁盘文件,再更新哪个磁盘文件?
是不是感觉一头雾水
所以这个时候就需要存储引擎了,存储引擎其实就是执行SQL语句的,他会按照一定的步骤去查询内存缓存数据,更新磁盘数据,查询磁盘数据,等等,执行诸如此类的一系列的操作,如下图所示:
MySQL的架构设计中,SQL接口、SQL解析器、查询优化器其实都是通用的,他就是一套组件而已。
但是储存引擎的话,他是支持各种各样的存储引擎的,比如我们常见的InnoDB、MyISAM、Memory等等,我们是可以选择使用哪种存储引擎来负责具体的SQL语句执行的。
当然现在MySQL一般都是使用InnoDB储存引擎的,至于存储引擎的原理,后续我们也会深入一步一步分析,大家不必着急。
7、执行器:根据执行计划调用储存引擎的接口
那么看完存储引擎之后,我们回过头来思考一个问题,存储引擎可以帮助我们去访问内存以及磁盘上的数据,那么是谁来调用储存引擎的接口呢?
其实我们现在还漏了一个执行器的概念,这个执行器会根据优化器选择的执行方案,去调用存储引擎的接口按照一定的顺序和步骤,就把SQL语句的逻辑给执行了。
举个例子,比如执行器可能会先调用存储引擎的一个接口,去获取“users”表中的第一行数据,然后判断一下这个数据的"id"字段的值是否等于我们期望的一个值,如果不是的话,那就继续调用存储引擎的接口,去获取“users”表的下一行数据。
就是基于上述的思路,执行器就会去根据我们的优化器生成的一套执行计划,然后不停的调动存储引擎的各种接口去完成SQL语句的执行计划,大致就是不停的更新或者提取一些数据出来,如下图所示:
2023年新的一份Java面试中的葵花宝典敬请收下,解决你的面试困扰
HashMap面试题
HashMap与HashTable的区别
1.HashMap线程不安全 HashTable 线程是安全的采用synchronized
2.HashMap允许存放key 为null HashTable 不允许存放key 为null
3.在多线程的情况下,推荐使用ConcurrentHashMap 线程安全 且效率非常高
HashMap底层是如何实现的
在HashMap1.7版本中底层是基于数组+链表实现的,如果发生Hash冲突概率
比较大,会存放到同一个链表中,链表如果过长 会从头查询到尾部 效率非常低。
所以在HashMap1.8版本 (数组容量>=64&链表长度大于8) 就会将该链表转化红黑树。
HashMap根据Key查询时间复杂度?
1.Key没有产生冲突 时间复杂度是为o(1); 只需要查询一次
2.Key产生冲突 采用链表存放则为O(N) 从头查询到尾部
3.key产生冲突采用红黑树存放则为O(LogN)
HashMap底层是有序存放的吗?
是无序的,因为Hash 算法是散列计算的 没有顺序,如果需要顺序
可以使用LinkedHashMap集合采用双向链表存放。
HashMap7扩容产生死循环问题有了解过吗?
其实这个JDK官方不承认这个bug,因为HashMap本身是线程不安全的,不推荐在
多线程的情况下使用,是早期阿里一名员工 发生在多线程 的情况下使用HashMap1.7 扩容会发生死循环问题,因为HashMap1.7 采用头插入法 后来在在HashMap1.8 改为尾插法 。
如果是在多线程的情况下 推荐使用ConcurrentHashMap
HashMap Key 为null 存放在 什么位置
存放在数组 index为0的位置。
ConcurrentHashMap 底层是如何实现?
1.传统方式 使用HashTable保证线程问题,是采用synchronized锁将整个HashTable中的数组锁住,
在多个线程中只允许一个线程访问Put或者Get,效率非常低,但是能够保证线程安全问题。
2.多线程的情况下 JDK官方推荐使用ConcurrentHashMap
ConcurrentHashMap 1.7 采用分段锁设计 底层实现原理:数组+Segments分段锁+HashEntry链表实现
大致原理就是将一个大的HashMap 分成n多个不同的小的HashTable
不同的key 计算index 如果没有发生冲突 则存放到不同的小的HashTable中 ,从而可以实现多线程
同时做put操作,但是如果多个线程同时put操作 key 发生了index冲突落到同一个小的HashTable中
还是会发生竞争锁。
3.ConcurrentHashMap 1.7 采用 Lock锁+CAS乐观锁+UNSAFE类 里面有实现 类似于synchronized
锁的升级过程。
4.ConcurrentHashMap 1.8版本 put操作 取消segment分段设计 直接使用Node数组来保存数据
index没有发生冲突使用cas锁 index 如果发生冲突则 使用 synchronized
分布式解决方案
请问你在生产环境中,如何搜索日志的呢?
回答:
- 传统的方式采用tail 搜索文件日志,如果服务器是集群的
使用tail 指令搜索日志效率是非常低; - 所有我们构建分布式ELK+Kafka采集日志 ,统一将我们的日志输出到
ES中,通过可视化界面查询。
3.或者整合skywalking监控服务报警,直接通过skywalking定位服务追踪链
报错信息。
4.在一些较大的互联网企业中,保证服务器端安全性,生产环境一般是不允许直接连接生产环境
服务器,而是通过日志采集系统查看日志。
生产环境中,你遇到了报错的问题 你是如何定位的?
回答:
- 传统的方式 在生产环境中遇到报错问题,我们是通过搜索日志的方式,排查具体的错误。
2.采用aop形式拦截系统错误日志,在将这些错误日志调用微信公众号接口 主动告诉给我们的开发人员
生产环境发生了故障。 - 我们公司采用apm系统 skywalking ,监控整个微服务 如果服务在一段时间
内发生了故障或者报错 会主动调用微信模板接口通知给开发人员 生产环境发生了故障。在通过skywalking 追踪 链可以直接查看到具体的错误信息内容
调用接口的时候,如果服务器端一直没有及时响应 怎么解决?
1.如果调用接口发生了响应延迟:是因为我们http请求是采用同步的形式,基于请求与响应模型如果服务器端没有及时响应给客户端,客户端就会认为接口超时,接口发生了超时客户端会不断重试 ,重试的过程中 会导致 幂等性问题
幂等性问题(需要保证业务唯一性。)
2.如果接口响应非常慢,就需要对代码做优化例如 加上缓存减轻db查询压力、减少GC回收频率
2.如果接口代码在怎么优化 就是执行非常耗时时间,因为采用mq异步的形式 不能够使用 同步形式。
举例子:接口代码里面 需要调用非常多接口 在响应客户端
接口代码:
1.调用征信报告接口---15s-30s
生产环境服务器宕机,如何解决呢?
- 我们公司生产环境,会对我们服务器 实现多个节点集群,如果某台服务器
发生了宕机 会自动实现故障转移,保证服务的高可用。
- 如果服务器宕机 我们可以在服务器上安装keepalived 监听java进程,如果该java进程发生了宕机 会自动尝试重启该java进程,这是属于软件层面。如果是物理机器比如关机了,可以使用硬件方式自动重启服务器 例如向日葵
3.如果服务器发生了宕机,尝试重启n多次还是失败,我们可以使用容器快速动态的实现扩容(docker或者k8s)
4.重启该服务,如果重启多次还是失败 则会发送短信模板的形式通知给运维人员。
注意:千万不要回答 直接重启服务器端。
SpringCloud
为什么不选择dubbo?却选择SpringCloud?
- dubbo 属于RPC框架;
- SpringCloud 不属于RPC框架,属于微服务全家桶框架,提供非常多
在分布式微服务架构中遇到难题
2.1服务治理---nacos eureka zk
2.2分布式配置中心 nacos springcloud config 携程阿波罗
3.3分布式事务 lcn(被淘汰)、seata
3.4服务追踪 zipkin /skwalking
3.5服务保护 hystry、sentinel
3.6微服务网关 zuul gateway
....
feign客户端 rpc框架 类似于 dubbo
dubbo rpc框架 底层 netty 封装dubbo 协议
整合分布式解决方案---自己单独整合 单一功能
而springcloud 提供了成熟一套微服务解决方案。
dubbox 属于当当网提供 http协议接口
feign 调用接口 http协议
开放平台(阿里、腾讯) http协议 跨平台
dubbo与feign 都是面向接口 调用 底层思想原理都是相同。
底层采用动态代理技术。
服务正在发布中?如何不影响用户使用?
服务正在发布中,该jar中正在启动...
客户端访问的时候,一直阻塞等待。
1.使用nginx 故障转移即可。
2.灰度发布 先发布一小部分 如果没有问题 在让所有用户都可以访问。
灰度发布 nginx+nacos gateway+nacos(推荐)
对方调用你接口响应比较慢?你会怎么排查?
项目搭建服务追踪监控系统
1.zipkin /skwalking 通过平台形式可以查询该接口响应速度时间。
对方调用你接口响应比较慢 多个维度思考?
带宽→服务处理(cpu)→数据库或者Redis→网络IO操作(例如调用别人接口)
1.走外网传输数据 会有带宽限制呢
2.请求如果达到服务端,服务足够线程处理请求 如果服务器没有足够的线程
处理该请求? 导致客户端会阻塞等待?
解决办法:
1.调整最大线程数
2.调整最大线程数 治标不治本,对接口做限流操作 例如服务器端没有足够
线程处理的时候(策略服务熔断 降级 限流策略。)
3.服务cpu处理性能(多核cpu) 体现多线程同时处理 降低cpu上下文切换的次数。
如果发生了上下文切换会导致其他的线程 会短暂阻塞 有需要从新被cpu调度。
4.判断sql语句查询是否比较慢、做mysql调优 快速响应结果
5.网络IO操作(例如调用别人接口)代码在怎么优化还是比较慢,将耗时的操作
采用异步的形式处理 例如多线程(消耗cpu的资源) 建议使用MQ。
开发者不小心删除了生产环境数据?怎么恢复呢?
1.正常的情况下 在生产环境中 没有delete或者rm -rf 通过update
隐藏的形式, 后期淘汰策略删除。
2.构建mysql主从集群环境 可以通过备份节点恢复数据,一主一从。
3.如果执行delete 我们是可以通过binlog 快速恢复数据。
你在开发过程中,遇到哪些难题?你是怎么解决的呢
如果在面试的过程中被面试官问到:你在开发过程中,遇到哪些难题?
不要答:空指针异常、常见错误异常。
遇到问题→你是如何分析的?→如何排查的?→最终是怎么解决的?
1.分布式事务
2.分布式幂等
例如 我们公司提供了一个接口,被其他公司进行调用。
他的公司在调用我们公司接口的过程中,我们的接口响应超时了,
最终触发了客户端重试了,重试的过程当中请求的参数都是相同的,导致我们接口
会重复执行业务逻辑。
解决办法: 全局id 业务上防重复、 在db层面去重复 例如 创建唯一约束
3.定时任务调度
例如:我们项目在生产环境中做定时任务,如果集群的情况下 定时任务重复执行。
解决该问题
1.在打jar包的时候 加上一个开关 只让一个jar包执行定时任务
2.整合分布式任务调度平台 xxljob 最终分片执行 定时任务集群的执行
定时任务1 【】跑批 1-10万 定时任务2 11-20万
4.数据同步延迟问题
我们公司 使用canal 解决mysql与redis+kafka 数据同步问题
发现就是在并发的情况下同步非常延迟,我们整合kafka分区模型
根据每张表都有自己独立的topic主题,每个topic 主题有自己独立
分区 每个分区有自己独立消费者 ,解决消息顺序一致性问题。
6.安全性问题
7生产环境发生cpu飙高、内存泄漏
.......真实业务场景当中遇到难题