来广营做网站公司,商丘网格通,网站的实施方案,单页加盟网站模板Android 字节飞书面经 文章目录 Android 字节飞书面经一面二面 一面
1. 线程是进程的一部分#xff0c;一个线程只能属于一个进程#xff0c;而一个进程可以有多个线程#xff0c;但至少有一个线程。
2. 根本区别#xff1a;进程是操作系统资源分配的基本单位#xff0c;…Android 字节飞书面经 文章目录 Android 字节飞书面经一面二面 一面
1. 线程是进程的一部分一个线程只能属于一个进程而一个进程可以有多个线程但至少有一个线程。
2. 根本区别进程是操作系统资源分配的基本单位而线程是任务调度和执行的基本单位
3. 在开销方面每个进程都有独立的代码和数据空间程序上下文程序之间的切换会有较大的开销线程可以看做轻量级的进程同一类线程共享代码和数据空间每个线程都有自己独立的运行栈和程序计数器PC线程之间切换的开销小。
4. 所处环境在操作系统中能同时运行多个进程程序而在同一个进程程序中有多个线程同时执行通过CPU调度在每个时间片中只有一个线程执行
5. 内存分配方面系统在运行的时候会为每个进程分配不同的内存空间而对线程而言除了CPU外系统不会为线程分配内存线程所使用的资源来自其所属进程的资源线程组之间只能共享资源。
6. 操作系统管理了系统的有限资源当有多个进程或多个进程发出的请求要使用这些资源时因为资源的有限性必须按照一定的原则选择进程请求来占用资源。**这就是调度。**
7. CPU的核和线程执行关系通常情况下一个核心对应一个物理线程但通过超线程技术一个核心可以对应两个线程也就是说它可以同时运行两个线程。**在待执行线程数很多时CPU核心数越高多线程执行效率也就越高但当执行线程数低于CPU核心数时多线程执行效率其实与CPU核心数关系不大而是更多的取决于线程访问其他资源的速度。**
8. **推荐阅读**[**深度好文|面试官进程和线程的所有知识点**](https://zhuanlan.zhihu.com/p/317144739)扩展 为什么要设计线程 调度与执行的基本单位是线程资源所有权包括程序、数据、文件等资源。一个进程拥有对这些资源的所有权是进程 线程的设计就是为了提高CPU、内存、I/O 设备的利用率CPU、内存、I/O 设备的速度是有极大差异的为了合理利用 CPU 的高性能平衡这三者的速度差异 执行与调度的基本单位是线程这样设计有什么好处 为了并发和隔离并发是为了尽量让硬件利用率提高线程是为了系统层面做到并发线程上下文切换效率比进程上下文切换会高很多这样可以提高并发效率。 隔离也是并发之后要解决的重要问题计算机的资源一般是共享的隔离要能保障崩溃了这些资源能够被回收不影响其他代码的使用。所以说一个操作系统只有线程没有进程也是可以的只是这样的系统会经常崩溃而已操作系统刚开始发展的时候和这种情形很像。 程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程进而提升整体处理性能。 一个线程有哪些数据段 个人觉得这个问题有点不对我没有查到相应的答案我感觉面试官是想问内存的分段内存分段情况 **代码段正文段**代码段code segment/text segment通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读, 某些架构也允许代码段为可写即允许修改程序。在代码段中也有可能包含一些只读的常数变量例如字符串常量等。数据段数据段data segment通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。BSS段BSS段bsssegment通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。堆heap堆是用于存放进程运行中被动态分配的内存段它的大小并不固定可动态扩张或缩减。当进程调用malloc等函数分配内存时新分配的内存就被动态添加到堆上堆被扩张当利用free等函数释放内存时被释放的内存从堆中被剔除堆被缩减栈(stack)栈又称堆栈 是用户存放程序临时创建的局部变量也就是说我们函数括弧“{}”中定义的变量但不包括static声明的变量static意味着在数据段中存放变量。除此以外在函数被调用时其参数也会被压入发起调用的进程栈中并且待到调用结束后函数的返回值也会被存放回栈中。由于栈的先进先出特点所以栈特别方便用来保存/恢复调用现场。从这个意义上讲我们可以把堆栈看成一个寄存、交换临时数据的内存区。 死锁的形式有其他的几种吗(除了多个线程同时挣抢资源 资源死锁每个进程都在等待其他进程释放资源而其他资源也在等待每个进程释放资源这样没有进程抢先释放自己的资源这种情况会产生死锁所有进程都会无限的等待下去。通信死锁两个或多个进程在发送消息时出现的死锁。进程 A 给进程 B 发了一条消息然后进程 A 阻塞直到进程 B 返回响应。假设请求消息丢失了那么进程 A 在一直等着回复进程 B 也会阻塞等待请求消息到来这时候就产生死锁。通信中一个非常重要的概念来避免超时(timeout) 如果超时时间到了但是消息还没有返回就会认为消息已经丢失并重新发送通过这种方式可以避免通信死锁。 怎么在程序上解决死锁 这个问题只需要破解四种死锁形成的条件之一就OK了互斥循环等待保持和等待非抢占式比如给每个线程设置一个占有资源的时间时间一到必须释放锁给线程设置优先级允许高优先级的抢占低优先级的线程 Java栈和堆区别什么是原子变量原子变量的代码实现 原子变量保证了该变量的所有操作都是原子的不会因为多线程的同时访问而导致脏数据的读取问题。源码实现
public class AtomicInteger extends Number implements java.io.Serializable {private static final Unsafe unsafe Unsafe.getUnsafe();private static final long valueOffset;static {try {//用于获取value字段相对当前对象的“起始地址”的偏移量valueOffset unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField(value));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;//返回当前值public final int get() {return value;}//递增加detlapublic final int getAndAdd(int delta) {//三个参数1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(valuedelta)。return unsafe.getAndAddInt(this, valueOffset, delta);}//递增加1public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) 1;}
...
}我们可以看到 AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的。
volatile保证线程的可见性多线程并发时一个线程修改数据可以保证其它线程立马看到修改后的值CAS 保证数据更新的原子性。
TCP和UDP的区别吗Java四种引用注解是干嘛的几种常用的注解 注解是用于对代码进行说明可以对包、类、接口、字段、方法参数、局部变量等进行注解主要作用 生成文档通过代码里标识的元数据生成javadoc文档。编译检查通过代码里标识的元数据让编译器在编译期间进行检查验证。编译时动态处理编译时通过代码里标识的元数据动态处理例如动态生成代码。运行时动态处理运行时通过代码里标识的元数据动态处理例如使用反射注入实例。 Java自带的标准注解包括Override、Deprecated和SuppressWarnings分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告用这些注解标明后编译器就会进行检查。元注解元注解是用于定义注解的注解包括Retention、Target、Inherited、DocumentedRetention用于标明注解被保留的阶段Target用于标明注解使用的范围Inherited用于标明注解可继承Documented用于标明是否生成javadoc文档。推荐阅读JAVA 注解的基本原理 线程池有什么用几种线程池说一下 线程池的作用 重用存在的线程池减少对象的创建消亡的开销性能佳可有效控制最大的并发线程数提高系统资源的使用率同时避免过多的资源竞争避免堵塞提供定时执行定期执行单线程并发数控制等功能 线程池的优先级核心线程池阻塞队列非核心线程池各任务队列 ArrayBlockingQueue内部维护了一个由固定大小的数组构成的数据缓冲队列数组大小在队列初始化的时候就需要制定可以制定访问策略ArrayBlockingQueue(int capacity, boolean fair) fairtrue 则按照FIFO的顺序访问阻塞队列若为false 则访问顺序是不确定的LinkedBlockingQueue:基于单向链表的阻塞队列内部维护了一个由单向链表构成的数据缓冲队列区别于ArrayBlockingQueueLinkedBlockingQueue在初始化的时候可以不设置容量大小其默认大小为**Ineger.MAX_VALUE **PriorityBlockingQueue基于优先级的阻塞队列我们可以实现Comparable接口我们实现Comparable接口我们重写CompareTo就可以定义优先级顺序。队列容量是无限的当线程数达到最大值时会对任务队列扩容。DelayQueue基于延迟时间优先级的阻塞队列DelayQueue必须实现Delayed接口Delayed继承自Comparable接口我们需重写Delayed.getDelay方法为元素的释放执行任务设置延迟getDelay方法的返回值是队列元素被释放前的保持时间如果返回0或一个负值就意味着该元素已经到期需要被释放因此我们一般用完成时间和当前系统时间作比较。SynchronousQueue基于同步的阻塞队列他内部并没有数据缓存空间元素只是会在试图取走的时候才有可能存在也就是说如果没有在插入元素时没有后续没有执行取出的操作那么插入的行为就会被阻塞。 系统预设的线程池 CachedTheadPool CachedThreadPool只有非核心线程池当任务提交后都会开辟一个非核心线程池来执行。闲置线程超过60s后都会被回收所有提交的任务都会被立即执行使用SynchronousQueueCachedTreadPool在执行大量短生命周期异步任务事可以显著提高程序性能。 FixedThreadPool 线程数固定核心线程数最大线程数线程闲置时间是0任务队列是LinkedBlockingQueue核心线程池没有超时限制任务队列容量没有大小限制FixedTreadPool使用于需要快速响应的场景 SingleTreadExecutor 核心线程池只有一个最大线程数也是1线程闲置时间为0任务队列为LinkedBlockingQueue任务数量没有大小限制如果单个线程在执行过程中因为某些错误而终止会创建新的线程代替他执行后续的任务 推荐阅读 大话Android多线程(五) 线程池ThreadPoolExecutor详解 说说反射Android事件分发机制讲一下Android的handler
二面 二面忘记录音了有些问题只记得一半 RecycleView的局部刷新机制我们往RecycleView进行差异化更新应该注意什么线程死锁后怎么解决当时我回答的是破坏死锁形成的四个条件比如设置时间片但是下一次怎么恢复现场操作系统的跨进程通信的方式你知道几种实现细节ANR的触发原理 输入事件超时5s广播类型超时前台15s后台60s服务超时前台20s后台200sActivity的生命周期超时不会ANR 导致ANR的原因 应用层耗时导致的ANR 函数阻塞如死循环主线程IO处理大量数据锁出错线程对锁的挣抢内存紧张系统分配给一个应用的内存是用上限的内存紧张会导致一些应用操作导致耗时 系统层ANR CPU被抢占内存紧张 分析堆栈信息trace文件查看CPU负载内存信息看主线程执行耗时操作线程被锁争抢 卡顿优化方案 布局优化 减少冗余或者嵌套的布局来降低视图的层级比如使用约束布局代替线性布局和相对布局使用ViewStub替代在启动过程中不需要显示的UI控件使用自定义View代替复杂的View叠加 减少主线程耗时操作 主线程不要直接操作数据库数据库的操作应该放在子线程中进行Sharedpreferences尽量使用apply操作少使用commit网络请求放在子线程不要在activity的OnResume和OnCreateView的OnDraw中进行耗时操作比如大量的计算 减少过度绘制列表优化 RecyclerView使用优化使用DiffUtil和notifyItemDataSetChanged进行局部更新 减少小对象的分配和回收 Android四大组件如何实现跨进程通信跨进程通信过程中如何实现数据共享除了mmap还有吗当时自己想到了socket但是socket不太熟给自己挖坑了 **使用Bundle **由于bundle实现了Parcelable接口可以在不同进程间传输。**使用文件共享**两个进程通过读一个文件来交换数据Android是基于Linux可以并发读写并发读的话可能导致读出的数据不是最新的并发写的话会把问题写错。文件共享适用于对数据同步要求不高的进程通信。多进程下不建议使用sharedpreferences会有很大概率丢失数据sharedpreferences是在一个个进程会有自己的缓存。**使用Messenger是一种轻量级的IPC方案底层是AIDL **服务端需要创建一个Service创建一个handler并通过他来创建一个Messenger对象然后在Service的OnBind的方法返回客户端需要绑定Service绑定成功后使用服务端返回的IBinder创建一个Messenger这样客户端就可以和服务端发送消息了消息类型是Messenger如果希望服务端也能发送消息给客户端在客户端创建一个handler用来处理服务端发送的消息使用这个handler创建一个Messenger将Messenger通过replyTo返回给服务器服务器拿到客户端的Messenger就可以进行通信了。**使用AIDL**服务端创建一个Service用来监听客户端的请求连接创建一个AIDL文件将暴露给客户端的接口在这个文件中声明最后在Service中声明实现AIDL的接口客户端绑定服务端的Service将服务端成功返回的Binder对象转为AIDL接口所属于的类型接着可以调用AIDL的方法**ContentProvider**可以理解为受约束的AIDL主要提供数据源的CRUD**Socket**服务器端先初始化Socket然后与端口绑定(bind)对端口进行监听(listen)调用accept阻塞等待客户端连接。在这时如果有个客户端初始化一个Socket然后连接服务器(connect)如果连接成功这时客户端与服务器端的连接就建立了 自定义view一定得重写measure layout draw 方法吗你觉得那个过程最耗时关于MeasureSpec你觉得我们可以自己手动去更改值吗我回答可以面试官反问如何可以手动更改值那岂不是MeasureSpec没什么用了吗 不一定自定义组合控件类型不需要重写这三个方法只需要初始化UI添加一些监听对外暴露一些方法继承系统的控件textview我们希望复用系统的onMeaseure 和 onLayout 所以我们只要重写onDraw继承View我们得重写**onMeasure 和 onDraw **在View的源码当中并没有对AT_MOST和EXACTLY两个模式做出区分也就是说View在wrap_content和match_parent两个模式下是完全相同的都会是match_parent显然这与我们平时用的View不同所以我们要重写onMeasure方法。继承ViewGroup我们得重写onMeasure和onLayout推荐阅读Android自定义View全解 在子线程中除了handler机制回到主线程更新数据还有其他吗 Handler: 子线程非UI线程调用handler对象sendMessagemsg方法将消息发送给关联LooperLooper将消息存储在MessageQueue消息队列里面。然后轮巡取出MessageQueue中的消息给UI线程中handler处理handler得到消息调用handleMessage方法处理消息从而可以更新Ui。 Activity.runOnUiThread
# Activity.runOnUiThread
/*** Runs the specified action on the UI thread. If the current thread is the UI* thread, then the action is executed immediately. If the current thread is* not the UI thread, the action is posted to the event queue of the UI thread.** param action the action to run on the UI thread*/public final void runOnUiThread(Runnable action) {if (Thread.currentThread() ! mUiThread) {mHandler.post(action);} else {action.run();}}* 看源码很明显知道当前线程是主线程的时候立马会执行run() 方法当前线程非主线程会把action入队列MessageQueue
- **View.post(Runable r)**public boolean post(Runnable action) {final AttachInfo attachInfo mAttachInfo;if (attachInfo ! null) {return attachInfo.mHandler.post(action);}// Postpone the runnable until we know on which thread it needs to run.// Assume that the runnable will be successfully placed after attach.getRunQueue().post(action);return true;
}先看第一种情况AttachInfo !null * font stylecolor:#52C41A;attachInfo.mHandler.post(action); /font由此便知Runablle是交给attachInfo的handler去处理了* 我们得知道**attchInfo是什么**AttchInfo是View的内部类其内部持有一个 Handler 对象从注释可知它是由 ViewRootImpl 提供的final static class AttachInfo {/*** A Handler supplied by a views {link android.view.ViewRootImpl}. This* handler can be used to pump events in the UI events queue.*/UnsupportedAppUsagefinal Handler mHandler;AttachInfo(IWindowSession session, IWindow window, Display display,ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,Context context) {···mHandler handler;···}···
}* 看看**attchInfo**初始化时机# ViewRootImpl
final ViewRootHandler mHandler new ViewRootHandler();public ViewRootImpl(Context context, Display display, IWindowSession session,boolean useSfChoreographer) {···mAttachInfo new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);···}private void performTraversals() {···if (mFirst) {···host.dispatchAttachedToWindow(mAttachInfo, 0);···}···performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);performLayout(lp, mWidth, mHeight);performDraw();···}可以跟踪到 ViewRootImpl 类。ViewRootImpl 内就包含一个 Handler 对象 mHandler并在构造函数中以 mHandler 作为构造参数之一来初始化 mAttachInfo。ViewRootImpl 的performTraversals()方法就会调用 DecorView 的 dispatchAttachedToWindow 方法并传入 mAttachInfo从而层层调用整个视图树中所有 View 的 dispatchAttachedToWindow 方法使得所有 childView 都能获取到 mAttachInfo 对象 * **继续看dispatchAttachedToWindow(mAttachInfo, 0)方法**UnsupportedAppUsage(maxTargetSdk Build.VERSION_CODES.P)void dispatchAttachedToWindow(AttachInfo info, int visibility) {mAttachInfo info;···}mAttachInfo 的赋值时机可以追踪到 View 的 dispatchAttachedToWindow 方法至此我们可以很清楚知道mAttachInfo的初始化流程
:::success
ViewRootImpl的构造方法里面开始初始化 mAttachInfo new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, thiscontext);这里传了mhandler说mAttachInfol的引用是指向ViewRoot Impl的handler的。#ViewRootImpl.performTraversals()会调用#View. dispatchAttachedToWindow(mAttachInfo, 0);再看#View. dispatchAttachedToWindow方法我们才发现真正的View.mAttachInfo赋值是在这里。此外performTraversals()方法也负责启动整个视图树的 Measure、Layout、Draw 流程只有当 performLayout 被调用后 View 才能确定自己的宽高信息。而 performTraversals()本身也是交由 ViewRootHandler 来调用的即整个视图树的绘制任务也是先插入到 MessageQueue 中后续再由主线程取出任务进行执行。由于插入到 MessageQueue 中的消息是交由主线程来顺序执行的所以 attachInfo.mHandler.post(action)就保证了 action 一定是在 performTraversals 执行完毕后才会被调用因此我们就可以在 Runnable 中获取到 View 的真实宽高了
::: * 看第二种情况font stylecolor:#F5222D;getRunQueue().post(action);/font* font stylecolor:#F5222D;getRunQueue() /fontfont stylecolor:#000000;方法返回的是/fontfont stylecolor:rgba(46, 36, 36, 0.87);HandlerActionQueue 我们先看一下代码/fontpublic class HandlerActionQueue {private HandlerAction[] mActions;private int mCount;public void post(Runnable action) {postDelayed(action, 0);}public void postDelayed(Runnable action, long delayMillis) {final HandlerAction handlerAction new HandlerAction(action, delayMillis);synchronized (this) {if (mActions null) {mActions new HandlerAction[4];}mActions GrowingArrayUtils.append(mActions, mCount, handlerAction);mCount;}}public void executeActions(Handler handler) {synchronized (this) {final HandlerAction[] actions mActions;for (int i 0, count mCount; i count; i) {final HandlerAction handlerAction actions[i];handler.postDelayed(handlerAction.action, handlerAction.delay);}mActions null;mCount 0;}}private static class HandlerAction {final Runnable action;final long delay;public HandlerAction(Runnable action, long delay) {this.action action;this.delay delay;}public boolean matches(Runnable otherAction) {return otherAction null action null|| action ! null action.equals(otherAction);}}···}很明显可以知道HandlerActionQueue 可以看做是一个专门用于存储 Runnable 的任务队列mActions 就存储了所有要执行的 Runnable 和相应的延时时间。两个post方法就用于将要执行的 Runnable 对象保存到 mActions中executeActions就负责将mActions中的所有任务提交给 Handler 执行 * font stylecolor:#000000;现在看一下/fontfont stylecolor:#F5222D;executeActions /fontfont stylecolor:#000000;被谁调用/font#viewUnsupportedAppUsage(maxTargetSdk Build.VERSION_CODES.P)void dispatchAttachedToWindow(AttachInfo info, int visibility) {mAttachInfo info;···// Transfer all pending runnables.if (mRunQueue ! null) {mRunQueue.executeActions(info.mHandler);mRunQueue null;}···}现在我们知道了这个操做也是#View.dispatchAttachedToWindow 从而使得 mActions 中的所有任务都会被插入到 mHandler 的 MessageQueue 中** 等到主线程执行完 performTraversals() 方法后就会来执行 mActions所以此时我们依然可以获取到 View 的真实宽高** * **推荐阅读**[**一文读懂 View.Post 的原理及缺陷**](https://juejin.cn/post/6939763855216082974#heading-3)
- **AsyncTask*** 内部就是一个Handler和线程池的封装。在线程池中执行后台任务并在执行过程中将执行进度传递给主线程当任务执行完毕后将最终结果传递给主线程。
- **font stylecolor:rgb(51, 51, 51);Rxjava/font**:::success 通过操作符切换线程
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
:::
Android 什么时候会用到子线程我当时回答的是可以考虑将耗时操作放在子线程的场景都可以比如网络请求数据库的读写热启动将耗时操作放在子线程进行磁盘IOApplicationContext和ActivityContext有什么区别ApplicationContext启动Activity有什么要注意的我当时回答必须要加FLAG_ACTIVITY_NEW_TASK标志位面试官反问你觉得还有什么要注意的吗 Context是上下文环境是应用程序环境信息的接口使用Context的调用方法比如启动Activity访问资源调用系统服务传入Context比如弹出Toast创建DialogActivityService和Application都是间接的继承自Context的因此一个应用程序中Context的数量ActivityServiceApplication 这个类的继承关系如下ApplicationContext生命周期与Application的生命周期一样长全局唯一与activity的生命周期无关Activity生命周期与Activity相关每一个Activity都会有一个对应的ActivityContext凡事跟UI相关的都应该使用Activity作为Context来处理其他的一些操作ServiceActivityAppliocation等实例都可以为了防止内存泄漏使用context的时候特别注意 不要让生命周期很长的引用activity context即保证引用activity的对象要与activity生命周期一样长生命长的对象引用使用ApplicationContext 为什么子线程不可以更新UI 其实子线程是可以更新UI的只要在ViewRootImpl创建之前就行了因为在更新UI操作的时候我们会调用ViewRootImpl的checkThread方法去判断mThread ! Thread.currentThread() 若为true则会抛出异常ViewRootImpl的创建时机是什么时候呢Activity.makeVisible----WindowManagerImpl.addView----WindowManagerGlobal.addView----new ViewRootImpl() 这个时候我们知道了ViewRootImpl的创建调用过程那我们更加关注的是谁调用了Activity.makeVisible从Acitivity声明周期handleResumeActivity会被优先调用到也就是说在handleResumeActivity启动后OnResumeViewRootImpl就被创建了这个时候就无法在在子线程中访问UI了。其实自线程更新UI会导致UI显示的不确定性线程共同访问更新操作同一个UI控件时容易发生不可控的错误但是若是我们加锁耗时会比较多。推荐阅读Android 子线程更新UI了解吗