金九银十就要来了,为面试者准备的Android最全面试题+答案解析

马上要到金九银十了,小编总结了一些面试题目包含百度/腾讯/小米/网易/搜狗/知乎/京东/360/瓜子,现在放上来,由于是自己整理,所以涵盖不全面的话诸位请谅解。

抽象类与接口的区别?

大体区别如下:

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在 public 抽象方法;
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
  • 接口中不能含有构造器、静态代码块以及静态方法,而抽象类可以有构造器、静态代码块和静态方法;
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口;
  • 抽象类访问速度比接口速度要快,因为接口需要时间去寻找在类中具体实现的方法;
  • 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。
  • 接口更多的为了约束类的行为,可用于解耦,而抽象类更加侧重于代码复用。

谈谈List,Set,Map的区别?

List中存储的数据是有顺序的,并且值允许重复;Map中存 储的数据是无序的,它的键是不允许重复的,但是值是允 许重复的;Set中存储的数据是无顺序的,并且不允许重 复,但元素在集合中的位置是由元素的hashcode决定,即 位置是固定的(Set集合是根据hashcode来进行数据存储 的,所以位置是固定的,但是这个位置不是用户可以控制 的,所以对于用户来说set中的元素还是无序的)

说一下线程的几种状态?

  • 第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
  • 第二是就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  • 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
  • 第五是死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。

如何实现多线程中的同步?

多线程同步和异步不是一回事。
几种情况,

  1. 就是大家说的synchronized 他可以保证原子性,保证多个线程在操作同一方法时只有一个线程可以持有锁,并且操作该方法,
  2. 就是手动调用读写锁,
  3. 手动操作线程的wait和notify
  4. volatile我记得是没有原子性的,他可以保证内存可见性,在多线程的情况下保证每个线程的数据都是最新的

谈谈线程死锁,如何有效的避免线程死锁?

一、死锁的定义

多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

二、死锁产生的原因

  1. 系统资源的竞争
    通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
  2. 进程推进顺序非法
    进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时, 两者都 会因为所需资源被占用而阻塞。
  3. 信号量使用不当也会造成死锁。
    进程间彼此相互等待对方发来的消息,结果也会使得这些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。
  4. 死锁产生的必要条件
    产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。循环
  • 等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
/**
* 一个简单的死锁类
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡
眠500毫秒
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启
动,先锁定o2,睡眠500毫秒
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被
td2锁定;
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被
td1锁定;
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执
行,从而死锁。
*/
public class DeadLock implements Runnable {
    
     
	public int flag = 1;
//静态对象是类的所有对象共享的
	private static Object o1 = new Object(), o2 = new Object();
	
	public void run() {
    
     
		System.out.println("flag=" + flag); 
		if (flag == 1) {
    
    
				synchronized (o1) {
    
    
					try {
    
    
						Thread.sleep(500);
					} catch (Exception e){
    
     
						e.printStackTrace();
					}
					synchronized (o2){
    
    
		 				System.out.println("1");
					}
		}
	}
	if (flag == 0){
    
    
		synchronized (o2) {
    
    
	try {
    
    
		Thread.sleep(500);
	} catch (Exception e){
    
     
		e.printStackTrace();
	}
	synchronized (o1){
    
     
		System.out.println("0");
	}
	}
}
}
public static void main(String[] args) {
    
    
	DeadLock td1 = new DeadLock();
	DeadLock td2 = new DeadLock();
	td1.flag = 1;
	td2.flag = 0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//td2的run()可能在td1的run()之前运行new
	Thread(td1).start();
	new Thread(td2).start();
}
}

谈一谈startService和bindService的区别,生命周期以及使用场景?

1、生命周期上的区别

执行startService时,Service会经历onCreate- >onStartCommand。当执行stopService时,直接调用onDestroy方法。调用者如果没有stopService,Service 会一直在后台运行,下次调用者再起来仍然可以stopService。

执行bindService时,Service会经历onCreate- >onBind。这个时候调用者和Service绑定在一起。调用者调用unbindService方法或者调用者Context不存在了(如Activity被finish了),Service就会调用onUnbind- >onDestroy。这里所谓的绑定在一起就是说两者共存亡了。

多次调用startService,该Service只能被创建一次,即该Service的onCreate方法只会被调用一次。但是每次调用startService,onStartCommand方法都会被调用。

Service的onStart方法在API 5时被废弃,替代它的是onStartCommand方法。

第一次执行bindService时,onCreate和onBind方法会被调用,但是多次执行bindService时,onCreate和onBind 方法并不会被多次调用,即并不会多次创建服务和绑定服务。

2、调用者如何获取绑定后的Service的方法

onBind回调方法将返回给客户端一个IBinder接口实例,IBinder允许客户端回调服务的方法,比如得到Service运行的状态或其他操作。我们需要IBinder对象返回具体的Service对象才能操作,所以说具体的Service对象必须首先实现Binder对象。

3、既使用startService又使用bindService的情况

如果一个Service又被启动又被绑定,则该Service会一直在后台运行。首先不管如何调用,onCreate始终只会调用一次。对应startService调用多少次,Service的onStart 方法便会调用多少次。Service的终止,需要unbindService和stopService同时调用才行。不管startService与bindService的调用顺序,如果先调用unbindService,此时服务不会自动终止,再调用stopService之后,服务才会终止;如果先调用stopService,此时服务也不会终止,而再调用
unbindService或者之前调用bindService的Context不存在了(如Activity被finish的时候)之后,服务才会自动停止。

那么,什么情况下既使用startService,又使用bindService呢?

如果你只是想要启动一个后台服务长期进行某项任务,那么使用startService便可以了。如果你还想要与正在运行的Service取得联系,那么有两种方法:一种是使用broadcast,另一种是使用bindService。前者的缺点是如果交流较为频繁,容易造成性能上的问题,而后者则没有这些问题。因此,这种情况就需要startService和bindService一起使用了。

另外,如果你的服务只是公开一个远程接口,供连接上的客户端(Android的Service是C/S架构)远程调用执行方法,这个时候你可以不让服务一开始就运行,而只是bindService,这样在第一次bindService的时候才会创建服务的实例运行它,这会节约很多系统资源,特别是如果你的服务是远程服务,那么效果会越明显(当然在Servcie创建的是偶会花去一定时间,这点需要注意)。

4、本地服务与远程服务

本地服务依附在主进程上,在一定程度上节约了资源。本地服务因为是在同一进程,因此不需要IPC,也不需要AIDL。相应bindService会方便很多。缺点是主进程被kill后,服务变会终止。远程服务是独立的进程,对应进程名格式为所在包名加上你指定的android:process字符串。由于是独立的进程,因此在Activity所在进程被kill的是偶,该服务依然在运行。缺点是该服务是独立的进程,会占用一定资源,并且使用AIDL进行IPC稍微麻烦一点。

对于startService来说,不管是本地服务还是远程服务,我们需要做的工作都一样简单。

如何更新UI,为什么子线程不能更新UI?

其实Android开发者都在说,子线程不能更新UI,难道一定就不能在子线程更新UI吗,那也未必:

下面代码是可以的,运行结果并无异常,可以正常的在子线程中更新了TextView控件

@Override
protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                tv.setText("Test");
            }
        }).start();
}

但是假如让线程休眠1000ms,就会发生错误:

Only the original thread that created a view hierarchy can touch its views.

上面报错的意思是只有创建视图层次结构的原始线程才能更新这个视图,也就是说只有主线程才有权力去更新UI,其他线程会直接抛异常的;
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7905)的异常路径可以看到抛出异常的最终在ViewRootImlcheckThread方法里,ViewRootImlView的根类,其控制着View的测量、绘制等操作,那么现在我们转到ViewRootImpl.java源码观察:

@Override
public void requestLayout() {
    
    
        if (!mHandlingLayoutInLayoutRequest) {
    
    
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
}

void checkThread() {
    
    
        if (mThread != Thread.currentThread()) {
    
    
            throw new CalledFromWrongThreadException(
          "Only the original thread that created a view hierarchy can touch its views.");
        }
}

上面的scheduleTraversals()函数里是对View进行绘制操作,而在绘制之前都会检查当前线程是否为主线程mThread,如果不是主线程,就抛出异常;这样做法就限制了开发者在子线程中更新UI的操作;
但是为什么最开始的在onCreate()里子线程对UI的操作没有报错呢,可以设想一下是因为ViewRootImp此时还没有创建,还未进行当前线程的判断;
现在,我们寻找ViewRootImp在何时创建,从Activity启动过程中寻找源码,通过分析可以查看ActivityThread.java源码,并找到handleResumeActivity方法:

final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) {
    
    
        ···
        // TODO Push resumeArgs into the activity for consideration
        ActivityClientRecord r = performResumeActivity(token, clearHide);
        if (r.window == null && !a.mFinished && willBeVisible) {
    
    
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
    
    
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }

            } else if (!willBeVisible) {
    
    
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
        ···
}

可以看到内部执行了performResumeActivity方法:

public final ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide) {
    
    
	if (r != null && !r.activity.mFinished) {
    
     
        r.activity.performResume(); 
    省略...
} 

会发现在内部调用了ActivityperformResume方法,可以肯定应该是要回调生命周期的onResume方法了:

	final void performResume() {
    
    
        ···
        mCalled = false;
        // mResumed is set by the instrumentation
        mInstrumentation.callActivityOnResume(this);
        if (!mCalled) {
    
    
            throw new SuperNotCalledException(
                "Activity " + mComponent.toShortString() +
                " did not call through to super.onResume()");
        }
        ···
    }

接着然后又调用了InstrumentationcallActivityOnResume方法:

	public void callActivityOnResume(Activity activity) {
    
    
        activity.mResumed = true;
        activity.onResume();
        
        if (mActivityMonitors != null) {
    
    
            synchronized (mSync) {
    
    
                final int N = mActivityMonitors.size();
                for (int i=0; i<N; i++) {
    
    
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    am.match(activity, activity, activity.getIntent());
                }
            }
        }
    }

然后就可以看到执行了 activity.onResume()方法,也就是回调了Activity生命周期的onResume方法;现在让我们回头看看handleResumeActivity方法,会执行这段代码:

···
r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
    
    
                    r.activity.makeVisible();
                }

发现在内部调用了ActivitymakeVisible方法:

void makeVisible() {
    
    
        if (!mWindowAdded) {
    
    
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

源码内部调用了WindowManageraddView方法,而WindowManager方法的实现类是WindowManagerImp类,直接找WindowManagerImpaddView方法:

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    
    
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

最终然后又执行了WindowManagerGlobaladdView方法,在该方法中,终于看到了ViewRootImpl的创建;

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
    
              
        ···
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
        ···
    }

真相大白:

刚刚源码分析可得知,ViewRootImpl对象是在onResume方法回调之后才创建,那么就说明了为什么在生命周期的onCreate方法里,甚至是onResume方法里都可以实现子线程更新UI,因为此时还没有创建ViewRootImpl对象,并不会进行是否为主线程的判断;

个人理解

必须要在主线程更新UI,实际是为了提高界面的效率和安全性,带来更好的流畅性;你反推一下,假如允许多线程更新UI,但是访问UI是没有加锁的,一旦多线程抢占了资源,那么界面将会乱套更新了,体验效果就不言而喻了;所以在Android中规定必须在主线程更新UI,很合理吧

总结源码分析

第一点:子线程可以在ViewRootImpl还没有创建之前更新UI的

第二点:访问UI是没有加对象锁的,在子线程环境下更新UI,会造成各种未知风险的

第三点:Android开发者一定要在主线程更新UI操作,这个是职业习惯哦

谈谈 Handler 机制和原理?

首先在UI线程我们创建了一个Handler实例对象,无论是匿名内部类还是自定义类生成的Handler实例对象,我们都需 要对handleMessage方法进行重写,在handleMessage方法中我们可以通过参数msg来写接受消息过后UIi线程的逻辑处理,接着我们创建子线程,在子线程中需要更新UI的时候,新建一个Message对象,并且将消息的数据记录在这个消息对象Message的内部,比如arg1,arg2,obj等,然后通过前面的Handler实例对象调用sendMessge方法把这个Message实例对象发送出去,之后这个消息会被存放于MessageQueue中等待被处理,此时MessageQueue的管 家Looper正在不停的把MessageQueue存在的消息取出 来,通过回调dispatchMessage方法将消息传递给Handler的handleMessage方法,最终前面提到的消息会被Looper 从MessageQueue中取出来传递给handleMessage方法。

由于文章篇幅有限,不能将我整理的所有面试题全部展示出来,不过也没关系,我已经将所有的面试题整理成PDF文档了,

在这里插入图片描述

有需要完整面试题和答案解析的朋友可以扫描下方二维码免费领取!童叟无欺!!

猜你喜欢

转载自blog.csdn.net/YoungOne2333/article/details/132109294