之前8月份为了准备面试复习了Android的一些原理知识,并陆陆续续的总结了一些面试相关的东西,因为太久没写面试之类的博客了,今天就想做一个Android面试知识的分享。
但是无奈本人太蔡了(灬ꈍ ꈍ灬),在北京、深圳(远程视频面试)面试了十多家大厂就只拿到了乐视和小米的offer,综合来说小米的薪资比要比乐视高一点。听了朋友建议,选择了小米,打算着以后准备往音视频方向转型。
首先从Android原理基础入手复习,然后就是一些面试进阶涨薪需要掌握的知识整理,(包括了大厂的面试官百问不厌的Android拓展的知识点)已收集整理成PDF,分享出来希望能帮到大家在金九银十之间找到一个好的工作。
一. Android相关
1. mvc mvp mvvm三种架构模式(腾讯)
mvc:业务逻辑、数据、界面分离的一种模式,简单的来说,就是通过controller来操作model层的数据,并且返回给view显示。
activity不是标准的controller,随着界面逻辑交互的复杂度提升,activity类的职责不断增加,变得臃肿。
view和model相互耦合,不利于开发。
mvp:主要是提出了presenter层,作为view和model之间沟通的桥梁。
程序逻辑放在presenter中处理,完全将view和model进行了分离,不允许他们之间沟通。
mvvm:主要是将presenter改为了viewmodel,和mvp类似,不同的是viewmodel跟view和model进行双向绑定。
使用了data binding
2. Android系统结构层次
Android系统架构分为5层,从下到上依次为 Linux内核层,硬件抽象层,系统运行库层(Native),应用框架层,应用层。
Linux内核层:Android的核心基于Linux内核,在此基础上添加了Android的专用驱动(比如Binder)、系统的安全性、内存管理、进程管理等等。
硬件抽象层(HAL):有了核心还不行,你得需要运行到相应的硬件上才能实现自己的价值吧。而硬件抽象层就是硬件和Linux内核之间的接口,目的就是将硬件抽象化,保护硬件厂商的知识产权(Linux是有开源协议的)
系统运行库层:怎么操纵硬件,显示图像到屏幕?这一层就是干这个的,它分为两部分,分别是C++程序库和Android运行时。
- C++程序库:被Android系统中的不同组件使用,可以通过应用框架层被开发者使用,下面是主要的程序库:
image
Android Runtime:ART虚拟机(5.0之后,Dalvik虚拟机被ART取代),ART在应用第一次安装的时候,就会将字节码预编译成机器码存储到本地,这样应用每次运行就无须执行编译了(Dalvik是每次打开都要即时编译),典型的以空间换时间
应用框架层:Framework层,这层代码是用java编写的,为开发人员提供了API。
应用层
3. Activity活动的启动模式及应用场景(网易、百度)
- standard: 默认的模式,新建一个Activity就在栈中新建一个activity实例。
- singleTop:栈顶复用模式,与standard相比栈顶复用可以有效减少activity重复创建对资源的消耗 。 登录页面 ,wxpay等支付页面
- singleTask:栈内单例模式,栈内只有一个activity实例,栈内已存activity实例,在其他activity中start这个* **activity,Android直接把这个实例上面其他activity实例踢出栈GC掉。主页面 ,WebView页面、扫一扫页面 ,付款界面
- singleInstance:开辟一个新的栈存放activity。系统Launcher、锁屏键、来电显示等系统应用 。
4. Android进程间通信的方式(小米、滴滴)
1\. Broadcast广播,当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据
2\. Content Provider,多个应用程序之间数据共享的方式(跨进程共享数据) ,应用程序可以完成对数据的增删改查。Android系统本身也提供了很多的Content Provider,比如音频,视频,联系人等信息。
3\. 通过AIDL文件,其中AIDL也是通过binder实现进程间通信的。
4\. socket
image
1、传统的IPC通信方式
Android系统是基于Linux内核的,Linux提供了管道、消息队列、共享内存和socket等IPC机制。那为什么Android还要提供Binder来实现IPC呢?主要是基于性能、稳定性和安全性方面的考虑。
性能:socket作为通用接口,传输效率低,开销大,主要用到跨网络进程通信。消息队列、共享内存和管道采用存储-转发模式,数据拷贝至少需要两次,共享内存虽然无需拷贝,但是控制复杂,难以使用。而binder只需要拷贝一次,性能上只次于共享内存。
稳定性:Binder 基于 C/S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。
安全性:Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。传统的 IPC 只能由用户在数据包中填入 UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标识只有由 IPC 机制在内核中添加。其次传统的 IPC 访问接入点是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
2、传统IPC通信原理
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。
一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝
接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
3、Binder IPC实现
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
5. ContentProvider的设计模式
6. 多线程的实现方法(synchronized和lock的异同)
- 继承Thread类创建线程
- 实现Runnable接口创建线程,推荐使用这种方式,可以复用runnable
- 实现Callbale接口,通过FutureTask包装器来创建一个带返回值的线程
synchronized
在用法上,它是java的关键字,一般我们不太需要关注他的锁的释放,代码执行完毕或者报错会自动释放锁,并且无法判断锁的状态。
lock
是一个接口,我们使用ReentrantLock 比较多,有多个获取锁的方式,可以trylock直接返回获取成功或者失败,线程不用一直等待。在finally中必须要释放该锁。
7. 说一下View的事件分发机制
为什么要有事件分发
注:引用G神的博客
Android中的view是树形结构的,view可能会重叠在一起,当我们点击的地方有多个view的时候,这个时间该给谁,这就是为什么要有事件分发。
先来看看view的树形结构:
image
上面多出来两个东西是phonewindow
和decorview
,其中,主题颜色和标题栏内容等主要就是decorview来负责显示的,那PhoneWindow
是做什么的呢?
PhoneWindow
继承window
,并且是window
唯一的实现类,window
是一个抽象类,是所有视图的最顶层容器,视图的外观和行为都归他管,不论是背景显示,标题栏还是事件处理都是他管理的范畴,它其实就像是View界的太上皇。
`DecorView` 是 `PhoneWindow` 的一个内部类,其职位相当于小太监,就是跟在 `PhoneWindow` 身边专业为 `PhoneWindow` 服务的,除了自己要干活之外,也负责消息的传递,`PhoneWindow` 的指示通过 `DecorView` 传递给下面的 View,而下面 View 的信息也通过 `DecorView` 回传给 `PhoneWindow`
image
事件分发、拦截、消费
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | X | √ | X |
事件消费 | onTouchEvent | √ | √ | √ |
Activity作为原始的事件分发者,不需要拦截事件,如果需要这个事件不分发下去就行了。
同样的,view在事件传递的最末端,也不需要拦截事件,不处理回传回去就行了。
事件在收集之后最先传递给Activity,然后依次向下传递:
Activity -> PhoneWindow -> DectorView -> ViewGroup -> ... -> view
image
如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃 :
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View
image
上面的模式是一个非常经典的责任链模式
8. 说一下View从app启动到显示在界面上的绘制流程
在activity的attach方法里面,会创建一个PhoneWindow。
在onCreate中调用setContentView,setContentView
是window
的一个抽象方法,真正实现类是PhoneWindow
:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
//1.初始化
//创建DecorView对象和mContentParent对象 ,并将mContentParent关联到DecorView上
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();//Activity转场动画相关
}
//2.填充Layout
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);//Activity转场动画相关
} else {
//将Activity设置的布局文件,加载到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
//让DecorView的内容区域延伸到systemUi下方,防止在扩展时被覆盖,达到全屏、沉浸等不同体验效果。
mContentParent.requestApplyInsets();
//3\. 通知Activity布局改变
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//触发Activity的onContentChanged方法
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
核心方法就两个:installDecor() 和 mLayoutInflater.inflate(layoutResID, mContentParent) ;
installDecor会创建一个DecorView
对象,该对象将作为整个应用窗口的根视图。然后配置不同窗口修饰属性(style theme等)。
mLayoutInflater.inflate就是解析xml,深度优先地递归解析xml,一层层添加到root view上,最终返回root view.解析的部分大致包含两点:1.解析出View对象,2.解析View对应的Params,并设置给View。
9. 知道什么会引起ANR吗 怎么避免
有四种情况会造成ANR发生:
- 5秒内无法响应屏幕触摸事件或键盘输出
- 在执行前台广播的
onReceive()
函数时10秒没有处理完成,后台为20秒 - 前台服务20秒内,后台服务在200秒内没有执行完成
ContentProvider
的publish
在10s内没进行完
如何避免:
尽量避免在主线程中做耗时操作。 多线程==>引出如何实现多线程,线程池的使用
如何分析ANR:
- 产生anr之后,会在
data/anr/
目录下生成一个文件traces.txt
Logcat
中查看- Java线程调用分析
- DDMS分析
10. 有做过app的性能优化吗
app启动加速:一个app的启动分为三种不同的状态,其中,我们只需要对第一种状态做优化。对于App来说, 我们可以控制的启动时间线无外乎
Application的onCreate
,首屏Activity的渲染。- 冷启动:App没有启动过或者App进程被kill,系统中不存在该App进程。此时App的启动需要创建App进程,加载相关资源,启动main thread,初始化首屏Activity等。
- 热启动:App进程只是处于后台, 系统只是将其从后台带到前台, 展示给用户。屏幕会显示一个空白的窗口(颜色基于主题), 直至activity渲染完毕。
- 温启动:介于冷启动和热启动之间, 一般来说在以下两种情况下发生,
- 用户back退出了App, 然后又启动. App进程可能还在运行, 但是activity需要重建
- 用户退出App后, 系统可能由于内存原因将App杀死, 进程和activity都需要重启, 但是可以在onCreate中将被动杀死锁保存的状态(saved instance state)恢复
布局优化 减少不必要的嵌套
- 尽量不要嵌套使用RelativeLayout
- 尽量不要在嵌套的LinearLayout中都使用weight属性
- ConstraintLayout
- 善用TextView的Drawable减少布局层级
响应优化
内存优化 bitmap的使用,options的jusdecodeBounds属性,设置只解析bitmap的宽高等,然后使用insimplesize对bitmap进行压缩。在android2.3的时代,bitmap的回收需要调用recycler方法,并且置空,但是之后只需要进行置空操作。
加载一张大图:使用BitmapRegionDecode进行局部解码,
- decodeRegion(Rect rect, BitmapFactory.Options options) 指定rect区域获取图像,options参数不支持inPurgeable,其他都支持
lru算法的实现=> LinkedHashMap
电池使用优化
网络优化
博客上写出的文章只能写出个大概,无法显示笔记的完整性,需要完整的《Android面试笔记》PDF的朋友可以去——————我的【Github】打包获取
地址:https://github.com/733gh/xiongfan2.0/tree/main
flutter
一步手机的刷新率为60hz,当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(如VSync), 60Hz的屏幕就会一秒内发出 60次这样的信号。而这个信号主要是用于同步CPU、GPU和显示器的。
一般地来说,计算机系统中
,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照同步信号从帧缓冲区取帧数据传递给显示器显示。
kotlin
面试几家之后发现都没有问,所以略过
12.框架源码相关
看源码要带着问题出发,选好一条路走到黑,不然会有种无从下手的感觉。
retrofit:主要精髓是动态代理,以及各种设计模式。可以将接口包装成一个http请求,以调用java方法的方式请求api,
okhttp:看了retrofit还有什么理由不看okhttp?okhttp设计非常巧妙,如果你知道osi网络七层架构,理解起来就很容易了,也是分层管理的思想。
recyclerview:现在做Android开发都用上它了吧。
Fresco/Glide:主要是,这几个图片加载库的区别。如果是你,怎么设计图片加载库?多级缓存都问烂了,这儿其实还有个知识点,多图加载肯定使用到线程池,会让你讲怎么用,原理是什么,等等。
Butterknife: 这儿要注意的地方是,有些人认为原理是利用反射,但是其实不是,它用到的是APT技术,也就没有反射效率低的问题
二. JAVA相关
1. 说一下JAVA的GC以及内存模型
GC:垃圾回收器,自动释放垃圾占用的空间,让创建的对象不需要像c/c++那样手动delete、free掉。
GC是在什么时候,对什么东西,做了什么事情?
什么时候
Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
minor gc/full gc的触发条件、OOM的触发条件,降低GC的调优的策略 (深入理解jvm)
**minor gc **:当新生代的eden区满了的时候,会触发gc
full gc: 当老年代空间不足,方法区空间不足,minor gc进入老年代的时候。大对象
对什么东西
从gc root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象
做了什么事情
JVM将堆分成了二个大区新生代(Young)和老年代(Old),新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace和ToSpace组成。
Young中的98%的对象都是死朝生夕死,所以将内存分为一块较大的Eden和两块较小的Survivor1、Survivor2,JVM默认分配是8:1:1,每次调用Eden和其中的Survivor1(FromSpace),当发生回收的时候,将Eden和Survivor1(FromSpace)存活的对象复制到Survivor2(ToSpace)
注:分成三块是为了充分利用内存。原来是只有一块内存,在这上面做标记清除算法,但是gc之后会存在大量不连续的空间,所以有人提出将内存一分为二,将第一块内存存活的对象转移到第二块内存,然后将第一块内存gc。
新生代的GC(Minor GC):新生代通常存活时间较短基于Copying算法进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。
老年代的GC(Major GC/Full GC):老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。
jvm内存模型(注意和java内存模型区分,java内存模型和并发编程相关)
- 程序计数器:存放每个程序下一步将执行的jvm指令,如果是native方法,则不会存储任何信息
- jvm栈:是线程私有的,每个线程创建的同时都会创建jvm栈
- 堆:JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
- 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
- Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
- TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
- 方法区(持久代):存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。jdk1.8中被移除了,改用metaspace
- 本地方法栈:JVM采用本地方法栈来支持native方法的执行,此区域用于存储每个native方法调用的状态
- 运行常量池:存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。JVM在加载类时会为每个class分配一个独立的常量池,但是运行时常量池中的字符串常量池是全局共享的。
2. Java内存模型
_参考:__深入理解java虚拟机_*
Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。
3. JAVA的类加载器
类从被加载到虚拟机内存中开始,到卸载出内存中为止,它的整个生命周期如下:
- 加载:主要是在内存中生成代表这个类的java.lang.class对象
- 验证:确保class文件中的信息符合当前虚拟机的要求,并且不会危害虚拟机本身
- 准备:为类变量分配内存并设置类变量初始值的阶段
- 解析(不确定顺序)将常量池中的符号引用替换为直接引用的过程
- 初始化:在准备阶段是设置初始值,而在这个阶段是根据我们制定的设置值
- 使用(不确定顺序)
- 卸载
类加载模型
双亲委派模型,从java虚拟机的角度讲,只存在两种不同的类加载器,启动类加载器和所有的其他类加载器,启动类加载器使用C++语言实现,是虚拟机的一部分,其他的类加载器由java语言实现,独立于虚拟机外,对于java开发人员来说,类加载器划分得更细:
- 启动类加载器(Bootstrap ClassLoader):加载虚拟机识别的类库
- 扩展类加载器:
- 应用程序类加载器
双亲委派的工作过程:如果一个类加载器收到了类加载的请求,首先它不会自己去尝试加载这个类,而是把这个请求委派给父类去完成,最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求的时候,子加载器才会尝试自己去加载。
Android中常用的类加载器有:DexClassLoader
和PathClassLoader
,这两个类都继承自BaseDexClassLoader
,
PathClassLoader
用于加载内部的Dex文件,DexClassLoader
多传了一个optimizedDirectory参数 ,可以用来加载外部的apk文件(插件化技术的核心)
4. 热更新原理
代码修复主要有两大方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案,这两种方案各有优劣。
底层替换方案限制比较多,但是时效性最好,加载快,立即见效
类加载方案时效性差,需要冷启动才能见效,但修复范围广,限制少。
底层替换方案
直接在已加载类中替换掉原有方法,即在原来类基础上进行修改,因此无法实现增减原有类方法或字段,这样会破坏原有类的结构。
类加载方案
在app重新启动后让Classloader加载新的类。因为当app运行到一半时,所需发生变更的类已经被加载过,而在Android上无法对一个类进行卸载操作,若不重启,原来的类还存储于虚拟机中,新类无法被加载。因此只有在下次重启时,在业务逻辑运行之前抢先加载补丁中的新类,这样后续访问此类时,才会Resolve为新类,从而达到热修复的目的。
5. 线程池有哪些?使用场景
池化技术
简单的来讲就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率。比较典型的池化技术有线程池、连接池、内存池、对象池等。
为什么使用线程池
- 在任务众多的情况下,系统要为每一个任务创建一个线程,而任务执行完毕后会销毁每一个线程,所以会造成线程频繁地创建与销毁
- 多个线程频繁地创建会占用大量的资源,并且在资源竞争的时候就容易出现问题,同时这么多的线程缺乏一个统一的管理,容易造成界面的卡顿。
- 多个线程频繁地销毁,会频繁地调用GC机制,这会使性能降低,又非常耗时。
常见的线程池:
ThreadPoolExecutor
:基本线程池FixedThreadPool
:可重用固定线程池,执行长期的任务,性能好很多CachedThreadPool
:按需创建,执行很多短期异步的小程序或者负载较轻的服务器SingleThreadPool
:单个核线的fixed,一个任务一个任务执行的场景ScheduledThreadPool
:定时延时执行,周期性执行任务的场景
上面的2-5的线程池,都是基于第一个基本线程池实现的,不同的地方在于核心线程数和存放任务的队列类型不同。
如何使用
在Executors
这个类中封装了很多方法。
有两种方法提交任务,submit和execute,其中submit就是将runnable包装成FutureTask 而已,最终调用的还是execuet,所以我们看execute的实现过程
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { //直接新建核心线程
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { //线程池是否在运行,添加到队列里等待
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command)) //线程池如果没有在运行,移除队列并拒绝任务
reject(command);
else if (workerCountOf(recheck) == 0) //核心线程满时,创建非核心线程执行任务
addWorker(null, false);
}
else if (!addWorker(command, false)) //拒绝
reject(command);
}
image
主要的工作是:
工作线程数小于核心线程数时,直接新建核心线程执行任务
大于核心线程数时,将任务添加进等待队列
核心线程满时,创建非核心线程执行任务
工作线程数大于最大线程数时,拒绝任务
笔记中所还网络,数据结构,算法的总结就不一一复述了,需要完整的《Android面试笔记》PDF的朋友可以去——————我的【Github】打包获取
总结
对于互联网大厂面试,我最后想要强调的一点就是心态真的很重要,是决定你在面试过程中发挥的关键,若不能正常发挥,很可能就因为一个小失误与offer失之交臂,所以一定要重视起来。
另外提醒一点,充分复习,是消除你紧张的心理状态的关键,但你复习充分了,自然面试过程中就要有底气得多。