Android 面试题收集:Handler+Binder+Activity+时间分发机制+View绘制流程+……等

news2024/12/23 5:50:34

一、Handler相关知识

一个线程只有一个Looper,一个Messagequeue,可以创建多个handler。

1、Handler与Looper的关联是怎样的?

实例化 Handler 的时候 Handler 会去检查当前线程的 Looper 是否存在,如果不存在则会报异常,也就是说在创建 Handler 之前一定需要先创建 Looper 。代码如下:

public Handler(Callback callback, boolean async) {
        //检查当前的线程是否有 Looper
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //Looper 持有一个 MessageQueue
        mQueue = mLooper.mQueue;
}

主线程创建好了Looper,但是一旦在子线程创建Handler就会报上述错误,解决办法也是描述中的要Looper.prepare(),那Looper.prepare()做了什么?

//Looper
private static void prepare(boolean quitAllowed) {
  if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
  }
  sThreadLocal.set(new Looper(quitAllowed));
}

Looper.prepare()的作用就是在子线程创建Looper对象,并且会借助 ThreadLocal 来实现与当前线程的绑定,Looper.loop()会开始不断尝试从 MessageQueue 中获取 Message , 并分发给对应的 Handler,也就是说 Handler 跟线程的关联是靠 Looper 来实现的。

2、Handler是如何线程切换的?

如果我们想将子线程的消息回传到主线程,我们一般的做法是:(示例代码)

//创建主线程的Hnadler,重点就在Looper.getMainLooper()这行代码
Handler mHandler = new Handler(Looper.getMainLooper()){
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        if(msg.what==HANDLER_TAG){
            LogUtil.d("主线程收到消息:"+msg.arg1);
        }
    }
};

//子线程通过主线程的Hnadler发送消息
Message message = mHandler.obtainMessage();
message.what = HANDLER_TAG;  //消息标识
message.arg1=10;   //消息内容
mHandler.sendMessage(message);

因为用了主线程的Looper,所以消息队列自然用的主线程的消息队列,往主线程的消息队列发消息自然会回调到主线程。具体看下源码:

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    //将mQueue赋值给MessageQueue
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    //具体发消息的方法
    return enqueueMessage(queue, msg, uptimeMillis);
}

可以注意到mQueue赋值给了queue对象,那mQueue是什么?

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    //就是将Looper的消息队列赋值给了mQueue
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

就是将Looper的消息队列赋值给了mQueue。

3、Handler为什么会造成内存泄漏?解决办法是什么?

Java 的特性,内部类会持有外部类的引用,使得 Activity类引用 会被 Handler 持有,当Handler发送延时消息Activity退出时,导致Activity引用在内存中无法清除就造成了内存泄漏。解决办法就是将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并及时移除所有消息

4、为什么Looper.loop()不会卡死?

handler机制是使用pipe来实现的,主线程没有消息处理时会阻塞在管道的读端。

binder线程会往主线程消息队列里添加消息,然后往管道写端写一个字节,这样就能唤醒主线程从管道读端返回,也就是说queue.next()会调用返回。

主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

5、既然是死循环又如何去处理其他事务呢?

答案是通过创建新线程的方式

我们看到main方法里调用了thread.attach(false),这里便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。

ActivityThread对应的Handler是一个内部类H,里边包含了启动Activity、处理Activity生命周期等方法。

二、Activity部分相关知识

1、启动模式及应用场景

四种启动模式:

  1. standard:标准模式:如果在mainfest中不设置就默认standard;standard就是新建一个Activity就在栈中新建一个activity实例;
  2. singleTop:栈顶复用模式:与standard相比栈顶复用可以有效减少activity重复创建对资源的消耗,但是这要根据具体情况而定,不能一概而论;
  3. singleTask:栈内单例模式,栈内只有一个activity实例,栈内已存activity实例,在其他activity中start这个activity,Android直接把这个实例上面其他activity实例踢出栈GC掉;
  4. singleInstance :堆内单例:整个手机操作系统里面只有一个实例存在就是内存单例;

在singleTop、singleTask、singleInstance 中如果在应用内存在Activity实例,并且再次发生startActivity(Intent intent)回到Activity后,由于并不是重新创建Activity而是复用栈中的实例,因此Activity再获取焦点后并没调用onCreate、onStart,而是直接调用了onNewIntent(Intent intent)函数;

Activity四种启动模式常见使用场景:

LauchModeInstance
standard邮件、mainfest中没有配置就默认标准模式
singleTopWXPayEntryActivity、WXEntryActivity 、推送通知栏
singleTask程序模块逻辑入口:主页面(Fragment的containerActivity)、WebView页面、扫一扫页面、电商中:购物界面,确认订单界面,付款界面
singleInstance系统Launcher、锁屏键、来电显示等系统应用

2、Activity启动流程

Activity 启动主要涉及到3个进程。

    1. 系统进程 SystemServer (负责管理整个framework,是Zygote孵化的第一个进程)
    1. App进程(App进程是用户点击桌面icon时,通过Launcher进程请求SystemServer,再调用Zygote孵化的)
    1. Zygote进程(所有进程孵化都由Zygote完成,而Zygote是init进程的子进程,也由init进程孵化)
    1. 如果点击桌面icon启动还会涉及到 Launcher进程(Zygote孵化的第一个应用进程)

Activity启动流程主要包含几步?

我们以点击Launcher的一个icon为开始,整体扯一下Activity的启动过程,桌面其实就是LauncherApp的一个Activity

    1. 当点击Launcher的icon开始,Launcher进程会像AMS发送点击icon的启动信息(这些信息就是在AndroidMainifest.xml中标签定义的启动信息,数据由PackageManagerService解析出来)
    1. AMS收到信息后会先后经过ActivityTaskManagerService->ActivityStartController->ActivityStarter内部类Request,然后把信息存到Request中,并通知Launcher进程让Activity休眠(补充个小知识点,这个过程会检测Activity在AndroidMainifest.xml的注册,如果没有注册就报错了)
    1. Launcher进程的ApplicationThread对象收到消息后调用handlePauseActivity()进行暂停,并通知AMS已经暂停。
      实现细节:ActivityThread.sendMessage()通过ActivityThread的H类发送Handler消息,然后触发 mTransactionExecutor.execute(transaction),
      执行过程中依赖ActivityClientRecord.mLifecycleState数值并通过ClientTransactionHandler抽象类的实现(ActivityThread)进行分发。
      注 :ActivityClientRecord.mLifecycleState(-1 ~ 7分别代表 UNDEFINED, PRE_ON_CREATE, ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY, ON_RESTART)
    1. AMS收到Launcher的已暂停消息后,会检查要启动的Activity所在的进程是否已经启动了,如果已经启动了就打开,如果未启动则通过Process.start(android.app.ActivityThread)来启动一个新的进程。
    1. 进程创建好以后,会调用ActivityThread.main(),初始化MainLooper,并创建Application对象。然后Instrumentation.newApplication()反射创建Application,创建ContextImpl通过Application的attach方法与Application进行绑定,最终会调用Instrumentation.callApplicationOnCreate执行Application的onCreate函数进行一些初始化的工作。完成后会通知AMS进程已经启动好了。
      通知过程:通过IActivityManager.attachApplication(IApplicationThread thread, long startSeq),将Application对象传入AMS
    1. AMS收到app进程启动成功的消息后,从ActivityTaskManagerService中取出对应的Activity启动信息, 并通过ApplicationThreadProxy对象,调用其scheduleTransaction(ClientTransaction transaction)方法,具体要启动的Activity都在ClientTransaction对象中。
    1. app进程的ApplicationThread收到消息后会调用ActiivtyThread.sendMessage(),通过H发送Handler消息,在handleMessage方法的内部又会调用 mTransactionExecutor.execute(transaction);具体参考第3步
      最终调用performLaunchActivity方法创建activity和context并将其做关联,然后通过mInstrumentation.callActivityOnCreate()->Activity.performCreate()->Activity.onCreate()回调到了Activity的生命周期。

三、Android 事件分发机制

三个非常重要的与事件相关的方法。

  • dispatchTouchEvent()
  • onTouchEvent()
  • onInterceptTouchEvent()

1、Android 事件分发总是遵循 Activity => ViewGroup => View 的传递顺序;
2、onTouch() 执行总优先于 onClick()

看下面 图示 较清晰:

主要注意事件的传递、回传和消费对象。

四、Android View 绘制流程

每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()。

1、onMeasure

measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。

MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:

  1. EXACTLY

表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

  1. AT_MOST

表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

  1. UNSPECIFIED

表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

调用getDefaultSize()方法来获取视图的大小,如下所示:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

这里传入的measureSpec是一直从measure()方法中传递过来的。然后调用MeasureSpec.getMode()方法可以解析出specMode,调用MeasureSpec.getSize()方法可以解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。

当然,一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小,如下所示:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

这里首先会去遍历当前布局下的所有子视图,然后逐个调用measureChild()方法来测量相应子视图的大小,如下所示:

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。

2、onLayout

measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程,如下所示:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了layout()方法中。那么我们来看下layout()方法中的代码是什么样的吧,如下所示:

public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
        if (mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
}

在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会调用onLayout()方法,但onLayout()是一个空的抽象方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。

在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。说到这里,我相信很多朋友长久以来都会有一个疑问,getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。

首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

3、onDraw

ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:

public void draw(Canvas canvas) {
	if (ViewDebug.TRACE_HIERARCHY) {
	    ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
	}
	final int privateFlags = mPrivateFlags;
	final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
	        (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
	mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
	// Step 1, draw the background, if needed
	int saveCount;
	if (!dirtyOpaque) {
	    final Drawable background = mBGDrawable;
	    if (background != null) {
	        final int scrollX = mScrollX;
	        final int scrollY = mScrollY;
	        if (mBackgroundSizeChanged) {
	            background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
	            mBackgroundSizeChanged = false;
	        }
	        if ((scrollX | scrollY) == 0) {
	            background.draw(canvas);
	        } else {
	            canvas.translate(scrollX, scrollY);
	            background.draw(canvas);
	            canvas.translate(-scrollX, -scrollY);
	        }
	    }
	}
	final int viewFlags = mViewFlags;
	boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
	boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
	if (!verticalEdges && !horizontalEdges) {
	    // Step 3, draw the content
	    if (!dirtyOpaque) onDraw(canvas);
	    // Step 4, draw the children
	    dispatchDraw(canvas);
	    // Step 6, draw decorations (scrollbars)
	    onDrawScrollBars(canvas);
	    // we're done...
	    return;
	}
}

可以看到,第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。

接下来的第三步是在第34行执行的,这一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。

第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。

以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。

五、Android Window、Activity、DecorView以及ViewRoot

Activity

Activity并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互。

Window

Window是视图的承载器,内部持有一个 DecorView,而这个DecorView才是 view 的根布局。Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main。Window 通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。

DecorView

DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。

ViewRoot

ViewRoot可能比较陌生,但是其作用非常重大。所有View的绘制以及事件分发等交互都是通过它来执行或传递的。

ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。

ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。

六、Android 的核心 Binder

Binder是一种进程间通信机制,它是一种类似于COM和CORBA分布式组件架构,通俗一点,其实是提供远程过程调用(RPC)功能。从英文字面上意思看,Binder具有粘结剂的意思,那么它把什么东西粘结在一起呢?在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。Binder就是一种把这四个组件粘合在一起的粘结剂了,其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Service Manager和Binder驱动已经在Android平台中实现好,开发者只要按照规范实现自己的Client和Server组件就可以了。

关系图示:

  1. Client、Server和Service Manager实现在用户空间中,Binder驱动程序实现在内核空间中。
  2. Binder驱动程序和Service Manager在Android平台中已经实现,开发者只需要在用户空间实现自己的Client和Server。
  3. Binder驱动程序提供设备文件/dev/binder与用户空间交互,Client、Server和Service Manager通过open和ioctl文件操作函数与Binder驱动程序进行通信。
  4. Client和Server之间的进程间通信通过Binder驱动程序间接实现。
  5. Service Manager是一个守护进程,用来管理Server,并向Client提供查询Server接口的能力。

七、协程和线程的区别

协程是一种比线程更加轻量级的一种函数。正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。协程不是被操作系统内核所管理的,而是完全由程序所控制的,即在用户态执行。 这样带来的好处是:性能有大幅度的提升,因为不会像线程切换那样消耗资源。

提示: 协程不是进程也不是线程,而是一个特殊的函数。这个函数可以在某个地方被“挂起”,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。

一个进程可以包含多个线程,一个线程也可以包含多个协程。简单来说,在一个线程内可以有多个这样的特殊函数在运行,但是有一点必须明确的是:一个线程中的多个协程的运行是串行的。 如果是多核CPU,那多个进程或一个进程内的多个线程是可以并行运行的。但是在一个线程内协程 却绝对是串行的,无论CPU有多少个核毕竟协程虽然是一个特殊的函数,但仍然是一个函数。 一个线程内可以运行多个函数,但这些函数都是串行运行的。当一个协程运行时,其他协程必须被挂起

协程的优势:

  • 节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。
  • 节约内存:在 64 位的Linux中,一个线程需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。
  • 稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。
  • 开发效率:使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时 IO 请求等。

协程本质上就是用户态下的线程,所以也有人说协程是 “轻线程”,但我们一定要区分用户态和内核态的区别,很关键。

更多Android 面试题锦https://0a.fit/YXwVq

第一章 Android 高频面试之必考Java基础
1.面向对象和面向过程的区别
2.面向对象的特征有哪些
3.解释下Java的编译与解释并存的现象
4.简单介绍下JVM的内存模型
5.简单介绍下Java的类加载器
6.谈一下Java的垃圾回收,以及常用的垃圾回收算法
7.成员变量和局部变量的区别
8.Java 中的方法重写(Overriding)和方法重载(Overload)的含义
9.简单介绍下传递和引用传递
10.为什么重写 equals 时必须重写 hashCode 方法
11.接口和抽象类的区别和相同点是什么
12.……


第二章 Android 面试之必问Android基础
1.Activity
2.Fragment
3.Service
4.BroadcastReceiver
5.ContentProvider
6.Android View知识点
7.Android进程
8.序列化
9.Windows
10.消息机制
11.RecyclerView优化

第三章 Android 面试之必问高级知识点
1.编译模式
2.类加载器
3.Android Hook
4.代码混淆
5.NDK
6.动态加载

第四章 Android 面试之必问性能优化
1.启动优化
2.UI渲染优化
3.内存优化
4.网络优化
5.耗电优化
6.安装包优化

Android 面试题锦https://0a.fit/YXwVq

第五章 Android 面试之开源库分析
1.HTTP与缓存理论
2.OKHttp
3.Retrofit
4.Glide
5.EventBus
6.……

第六章 算法面试题汇总
1.排序
2.二叉树
3.链表
4.栈 / 队列
5.二分搜索
6.哈希表
7.堆 / 优先队列
8.二叉搜索树
9.数组 / 双指针
10.贪心
11.字符串处理
12.动态规划
13.矩阵
14.二进制 / 位运算
15.……

第七章 Android面试之Flutter相关面试题全解析
1.Dart部分
2.Flutter 部分

第八章 Android面试之必问设计模式
1.请列举出在 JDK中几个常用的设计模式?
2.什么是设计模式?你是否在你的代码里面使用过任何设计模式?
3.Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
4.在 Java 中,什么叫观察者设计模式(observer design pattern)?
5.使用工厂模式最主要的好处是什么?在哪里使用?
6.举一个用 Java 实现的装饰模式(decorator design pattern)?它是作用于对象层次还是类层次?
7.在 Java 中,为什么不允许从静态方法中访问非静态变量?
8.设计一个 ATM 机,请说出你的设计思路?比如设计金融系统来说,必须知道它们应该在任何情况下都能够正常工作。
9.在 Java 中,什么时候用重载,什么时候用重写?
10.举例说明什么情况下会更倾向于使用抽象类而不是接口?
11.设计模式六大原则
12.设计模式的分类

Android 面试题锦https://0a.fit/YXwVq

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/46675.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于TreeView的简单使用(Qt6.4.1)

前言 TreeView是在Qt6.3中加入的&#xff0c;弥补了Qt中无官方树图。笔者上手尝试了下&#xff0c;虽然有点麻烦&#xff0c;但官方也做了不少简化。 本次教程&#xff0c;笔者创建一个简单的示例&#xff0c;以帮助读者使用TreeView。 一、创建模型类 当前模型需要使用C定义…

人工智能概况笔记

文章目录一、人的智能与人工智能二、人工智能的发展历程三、人工智能的主要应用四、人工智能的伦理思考五、神经网络与深度学习六、国内外人工智能动向一、人的智能与人工智能 智能&#xff1a;基于推理的学习、理解和做出判断或意见的能力 人的智能&#xff08;Human Intell…

【数据结构-树】哈夫曼树及其应用

文章目录1 哈夫曼树的构造2 哈夫曼树的应用——哈夫曼编码3 相关例子1 哈夫曼树的构造 将 n 个结点作为 n 棵仅含一个节点的二叉树&#xff0c;构成森林 F在 F 中选取两棵权值最小的二叉树&#xff0c;作为新结点的左右子树&#xff0c;并将新结点的权值置为左、右子树的根结点…

Nodejs -- 前后端身份认证概念及在Express中使用认证(Session,Cookie,JWT)

文章目录前后端的身份认证1 Web开发模式1.1 服务器端渲染的Web开发模式1.2 前后端分离的Web开发模式1.3 如何选择Web开发模式2 身份认证2.1 什么是身份认证2.2 为什么需要身份认证2.3 不同开发模式下的身份认证3 Session认证机制3.1 HTTP协议的无状态性3.2 如何突破HTTP无状态的…

【STL】set集合

构造函数 std::set<std::string> myset;这条指令将会创建一个空的set容器 该容器会默认采用std::less< T >的排序规则对成员进行排序。 这种排序方法适用于内置的数据类型&#xff0c;比如整型、浮点型、字符串等等。 std::set<std::string> myset{"j…

Linux 之 Ubuntu 上 Vim 的安装、配置、常用命令的简单整理

Linux 之 Ubuntu 上 vim 的安装、配置、常用命令的简单整理 目录 Linux 之 Ubuntu 上 vim 的安装、配置、常用命令的简单整理 一、简单介绍 二、Vim 的安装 三、vim 的 32 种工作模式 四、vim 的一些基础配置&#xff0c;及其说明 五、普通模式下的常用操作命令 六、插入…

域自适应——Bidirectional Learning for Domain Adaptation of Semantic Segmentation

论文题目&#xff1a;Bidirectional Learning for Domain Adaptation of Semantic Segmentation 本文的域位移是针对虚拟数据和真实数据之间的。 本文的贡献是&#xff1a; 提出了一种语义分割的双向学习系统 &#xff0c;其是一个学习分割适应模型和图像翻译模型的闭环学习系…

如何安装使用Jupyter Notebook?

大家好鸭&#xff01;我是小熊猫~ 今天来给大家介绍一下关于Jupyter Notebook的用法 关于它的组成部分就先不在这里详细解说啦~ 毕竟我可太懂你们啦~ 文章太长就会吃灰的~ 源码、资料电子书免费获取可点击这里 一、什么是Jupyter Notebook&#xff1f; 1. 简介 Jupyter Not…

vue3 弹窗开发之三,完善版

首先我们看一看整体的效果 点击图一的缩略图&#xff0c;查看大图&#xff08;如图二&#xff09;。同时弹窗内容底下有当前所有图片的缩略图展示&#xff0c;同时通过一个橙色的边框定位当前缩略图的位置。切换左右箭头滚动下一站或者上一张&#xff0c;同时底下缩略图同步有个…

mybatis的xml中<trim>标签的用法

文章目录1. 前言2. 先说结论3. 验证1. 情况一2. 情况二3. 情况三4. 情况四5. 验证prefixOverrides去掉的是trim内原sql内容1. 前言 在工作中离不开跟数据库打交道&#xff0c;目前流行的固然是mybatis&#xff0c;在xml中写sql的时候&#xff0c;可能会出现下面情况&#xff1a…

线性表的顺序实现【C语言版的真代码】

顺序表线性表顺序表顺序表的概念及其结构顺序表基本操作顺序表的初始化顺序表的插入顺序表的删除顺序表的查找线性表 线性表&#xff1a;一个线性表是含n个数据元素的有限序列。 它的逻辑结构要求是线性的&#xff0c;但其存储结构并没有做要求&#xff0c;即逻辑结构类似于如…

非DBA人员从零到一,MySQL InnoDB数据库调优之路(四)-数据备份与迁移

上一篇【非DBA人员从零到一&#xff0c;MySQL InnoDB数据库调优之路(三)-分区表与分库分表】 我认为分表在数据库的调优中是一种加法&#xff0c;通过拆分单表数据到多个表中&#xff0c;减少单表的压力&#xff0c;从而达到调优的效果&#xff0c;那么这一篇博文会通过对数据…

Spring Security权限管理原理

1.简介 授权是更具系统提前设置好的规则&#xff0c;给用户分配可以访问某一资源的权限&#xff0c;用户根据自己所具有的权限&#xff0c;去执行相应的操作&#xff0c;spring security提供的权限管理功能主要有两种&#xff1a; 基于过滤器的权限管理功能&#xff08;Filte…

Syntax Error: Error: Missing binary. See message above.

安装完nvm&#xff0c;选择后node版本&#xff0c;在idea中引入vue项目&#xff0c;npm install后&#xff0c;运行npm run serve后 控制台出现下面错误&#xff1a; Try running this to fix the issue: D:\Program Files\nodejs\node.exe E:\vue-project\node_modules\fibe…

篮球竞赛预约平台设计与实现的源码+文档

摘 要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;篮球竞赛预约平台也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时代的变化&#x…

platform总线

1、什么是platform总线&#xff1f; platform是Linux内核抽象出来的软件代码&#xff0c;用于设备与驱动的连接&#xff0c;设备与驱动通过总线进行匹配&#xff1b;匹配成功后会执行驱动中的probe函数&#xff0c;在probe函数中可以获取到设备的信息&#xff1b; 设备与驱动…

gitlab的使用方法,详解gitlab操作

1.导读 本教程主要讲解了GitLab在项目的环境搭建和基本的使用&#xff0c;可以帮助大家在企业中能够自主搭建GitLab服务&#xff0c;并且可以GitLab中的组、权限、项目自主操作。 - GitLab简介 - GitLab环境搭建 - GitLab基本使用(组、权限、用户、项目) 2.GitLab简介 Gi…

debug - JLX12864C(ST7920-12864)液晶屏不能使用串行通讯的原因

文章目录debug - JLX12864C(ST7920-12864)液晶屏不能使用串行通讯的原因概述调试备注ENDdebug - JLX12864C(ST7920-12864)液晶屏不能使用串行通讯的原因 概述 正在给板子写出厂测试程序, 买的12864型号是JLX12864C. STC官方给的例程是并行通讯, 好使. 但是想在测试程序中改为…

Linux线程基础

目录 一&#xff0c;线程函数 1、创建一个线程 2、获取自身线程ID 3、线程终止 4、取消正在执行线程 5、线程等待 6、线程分离 二&#xff0c;线程的使用 1&#xff0c;线程等待和线程分离 (1)、线程等待 (2)、线程分离 (3)、线程等待线程分离同时进行 三&#xff…

中英翻译《森林火灾的预防措施》

The Preventive Measures for Forest Fire 森林火灾的预防措施 The preventive measures for forest fires include some preemptive methods that can help reduce the risks of fires and contril their severity and spread, and thus, maintain ecological balance …