Android面试题

Posted lxn_李小牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android面试题相关的知识,希望对你有一定的参考价值。

1.ListView的优化策略和原理

参考ListView优化

2.Activity和Fragment的生命周期

Activity和Fragment的生命周期

3.View和ViewGroup的关系

View和ViewGroup的组合模式

android的UI界面都是View和ViewGroup及其子类组合而成的。View是所有UI组件的父类,其子类称为组件(Widget);ViewGroup是布局管理器,本身也是继承自View类,其子类称为布局(Layout)

4.常用布局类型,使用时如何取舍

RelativeLayout和LinearLayout

5.机型如何适配


6.Activity之间如何传递复杂的对象

 Intent intent = new Intent(this,SecondActivity.class);
        //创建Bundle对象
        Bundle bundle = new Bundle();
        //传递对象,对象需要实现Serializable接口
        bundle.putSerializable("1", new MovieInfo());
        //传递字符串,CharSequence是String的父接口
        bundle.putCharSequence("2","aaa");
        //传递CharSequence数组
        String[] str = "a","b";
        bundle.putCharSequenceArray("3",str);
        //传递CharSequence集合
        ArrayList arrayList = new ArrayList();
        bundle.putCharSequenceArrayList("4",arrayList);
        //bundle保存到intent中
        intent.putExtras(bundle);
        startActivity(intent);

7.Android有哪些Spannable

Spannable是一个接口,有两个实现类,SpannableString和SpannableStringBuilder,

SpannableStringBuilder通常配合下面的Span

  1. BackgroundColorSpan
  2. ForegroundColorSpan
  3. AbsoluteSizeSpan
  4. ImageSpan
  5. StyleSpan

8.Android有哪些Drawable

常用的Drawable有BitmapDrawable,ColorDrawable,ShapeDrawable,LayerDrawable,StateListDrawable

9.FragmentTranscation的commit和commitAllowingStateLoss的区别

Fragment事务提交的方式

10.如何处理多个旧版本的数据库升级

11.反射机制使用的场景

加载数据库驱动,动态调用函数,获取泛型

12.DeadObjectException

我们先看看它的定义

/**
 * The object you are calling has died, because its hosting process
 * no longer exists.
 */
public class DeadObjectException extends RemoteException 
    public DeadObjectException() 
        super();
    

    public DeadObjectException(String message) 
        super(message);
    
意思是说你调用的对象死了,因为它所在的进程不存在了,转载一篇博客,给出链接

linkToDeath机制了解

13.Java的值传递和引用传递

 基本类型:按值传递,方法的实参是原值的一个副本,

 引用类型:传递的是地址值,在方法内对这个对象的修改是会反映到原来的对象,但是String例外,看下面的例子

public static void main(String[] args) 
     String s = new String("A");
     change(s);
     System.out.println(s);


public static void change(String s) 
    s = "B";

因为String是final修饰的,它的值不能被修改,所以当我们修改引用所指向的内容时,会重新开辟一个控件,所以最后打印的结果 是B

14.Looper.prepare主要做了哪些工作

 (1)创建Looper对象并放入到ThreadLocal中

(2)创建消息队列

Looper.loop主要做了哪些工作

(1)从ThreadLocal中取出Looper

(2)获取Looper对应的消息队列

(3)在for循环中调用MessageQueue的next方法取出一个消息

一个线程只能由一个Looper


Handler的构造函数中做了什么工作

(1)调用Looper.myLooper获取looper对象

(2)获取Looper对应的MessageQueue

Handler的sendMessage方法有个返回值,true代表消息成功放到了消息对垒,否则是false,通常是因为Looper退出了,

注意,返回值true不代表消息一定会被处理,如果在时间处理消息的时间到达之前Looper退出了,那消息就会被丢弃。

sendMessage方法最终会调用到下面的方法

public boolean sendMessageAtTime(Message msg, long uptimeMillis) 
        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);
    
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) 
        msg.target = this;//msg的target属性是一个Handler,此时Handler就和消息对应起来了
        if (mAsynchronous) 
            msg.setAsynchronous(true);
        
        return queue.enqueueMessage(msg, uptimeMillis);
    
 boolean enqueueMessage(Message msg, long when) //把消息插入到消息队列中
        if (msg.target == null) 
            throw new IllegalArgumentException("Message must have a target.");
        
        if (msg.isInUse()) 
            throw new IllegalStateException(msg + " This message is already in use.");
        

        synchronized (this) 
            if (mQuitting) //消息队列退出了
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                msg.recycle();
                return false;
            

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;//mMessages代表消息队列的第一个元素,第一次mMessages是空的
            boolean needWake;
            if (p == null || when == 0 || when < p.when) 
                // 消息队列空闲,此时还没有消息
                msg.next = p;
                mMessages = msg;//把我们要处理的消息赋值给mMessages
                needWake = mBlocked;
             else 
                //把消息插入到消息队列中
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) 
                    prev = p;
                    p = p.next;//next代表下一个消息
                    if (p == null || when < p.when) //p==null代表消息队列中已经没有消息了,break跳出当前循环
                        break;
                    
                    if (needWake && p.isAsynchronous()) 
                        needWake = false;
                    
                
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) 
                nativeWake(mPtr);
            
        
        return true;
    


 Message next() //从消息队列读取消息
      ....
        for (;;) 
            if (nextPollTimeoutMillis != 0) 
                Binder.flushPendingCommands();
            

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) 
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;//mMessages是当前要处理的消息
                if (msg != null && msg.target == null) 
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do 
                        prevMsg = msg;
                        msg = msg.next;
                     while (msg != null && !msg.isAsynchronous());
                
                if (msg != null) 
                    if (now < msg.when) 
                        // 消息还没准备好,设置一个时间等它准备好
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                     else 
                        // 获取到一个消息
                        mBlocked = false;
                        if (prevMsg != null) 
                            prevMsg.next = msg.next;
                         else 
                            mMessages = msg.next;
                        
                        msg.next = null;
                        if (false) Log.v("MessageQueue", "Returning message: " + msg);
                        return msg;
                    
                 else 
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) 
                    dispose();
                    return null;
                
获取到消息以后,然后我们看消息是如何被处理的

public static void loop() 
        final Looper me = myLooper();
        if (me == null) 
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        
        final MessageQueue queue = me.mQueue;//获取消息队列

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) 
            Message msg = queue.next(); // 从消息队列中获取消息
            if (msg == null) 
                // 没有消息说明消息队列退出了
                return;
            
 ...//调用Handler的dispatchMessage方法处理消息
            msg.target.dispatchMessage(msg);

 public void dispatchMessage(Message msg) 
        if (msg.callback != null) 
            handleCallback(msg);
         else 
            if (mCallback != null) 
                if (mCallback.handleMessage(msg)) 
                    return;
                
            
            handleMessage(msg);//空方法,需要我们实现去处理消息
        
    
MessageQueue的存储结构是单链表

最后我们看看Looper的quit方法

public void quit() //终止loop方法的执行,不在处理消息队列中剩余的消息
        mQueue.quit(false);
    

public void quitSafely() //处理完消息队列的消息之后终止loop方法的执行
        mQueue.quit(true);
    

15.HandlerThread

   handlerThread = new HandlerThread("ht");//创建HandlerThread对象
        handlerThread.start();//启动HandlerThread,在run方法中创建了Looper对象
        mHandler = new Handler(handlerThread.getLooper()) 
            @Override
            public void handleMessage(Message msg) 
                super.handleMessage(msg);
                //这里执行在子线程,线程名称使我们在创建HandlerThread是传入的参数
            
        ;
    

    public void send(View view)
        mHandler.sendEmptyMessage(0);
    
HandlerThread自带Looper,使它可以通过消息队列,来重复使用当前线程,节省系统资源开销,这是它的优点也是缺点,

每一个任务将以队列的方式逐个被执行到,一旦任务中有某个任务执行时间太长,那么就会导致后续的任务都会被延迟处理。

16.Service生命周期

通过startService: onCreate--- onStartCommand(onStart过时)---onDestory

通过bindService:  onCreate--onBind--onUnBind--onDestory

onStartCommand我们可以指定一个返回值,START_STICKY,意思如下

如果Service在启动之后(onStartCommand返回值执行了),被系统杀死了,然后会保留在开启状态,但是不保留intent,

然后系统会重建Service,但是注意此时的intent是null.

17.通过bindService启动intentService会有什么结果

intentService源码中bindService返回的是null,而且通过上面的生命周期我们可以看出,通过bindService启动的Service不会走onStartCommand方法,而onStartCommand放啊中利用Handler发送了消息到handleMessage方法中执行,所以bindService启动intentService不会触发onHandleIntent方法。

18.为什么多次启动IntentService会顺序执行事件,停止服务后,后续的时间得不到执行。

因为IntentService是使用Handler,Looper,MessageQueue机制把消息发送到线程中去执行的,所以多次启动IntentService不会创建新的服务和新的线程,只是把消息放入到消息队列中等待执行,如果服务停止

@Override
    public void onDestroy() 
        mServiceLooper.quit();//调用了Looper的quit方法,通过之前的分析,我们知道,此时消息队列中的任务是得不到执行的
    

19.Context的结构



Context可以理解为上下文,环境,通过Context我们可以访问资源,和其他组件进行交互,接下来我们看看Context是何时创建的

Application:

在ApplicationThread的scheduleLaunchActivity方法中,有下面的代码

public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo,
                IVoiceInteractor voiceInteractor, int procState, Bundle state,
                PersistableBundle persistentState, List<ResultInfo> pendingResults,
                List<Intent> pendingNewIntents, boolean notResumed, boolean isForward,
                ProfilerInfo profilerInfo) 
   .....
sendMessage(H.LAUNCH_ACTIVITY, r);
然后到了ActivityThread中的handleMessage方法

 public void handleMessage(Message msg) 
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) 
                case LAUNCH_ACTIVITY: 
                  ...
                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null);
                
 private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) 
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        if (r.profilerInfo != null) 
            mProfiler.setProfiler(r.profilerInfo);
            mProfiler.startProfiling();
        

        // Make sure we are running with the most recent config.
        handleConfigurationChanged(null, null);

        if (localLOGV) Slog.v(
            TAG, "Handling launch of " + r);
  //创建一个Activity
        Activity a = performLaunchActivity(r, customIntent);


 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) 
        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) 
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        

        ComponentName component = r.intent.getComponent();
        if (component == null) 
            component = r.intent.resolveActivity(
                mInitialApplication.getPackageManager());
            r.intent.setComponent(component);
        

        if (r.activityInfo.targetActivity != null) 
            component = new ComponentName(r.activityInfo.packageName,
                    r.activityInfo.targetActivity);
        

        Activity activity = null;
        try 
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(//创建Activity
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) 
                r.state.setClassLoader(cl);
     ...
        try //创建Application
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
            if (activity != null) //创建Activity的Context
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                        + r.activityInfo.name + " with config " + config);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.voiceInteractor);

                if (customIntent != null) 
                    activity.mIntent = customIntent;
                
                r.lastNonConfigurationInstances = null;
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) 
                    activity.setTheme(theme);
                

                activity.mCalled = false;
                if (r.isPersistable()) 
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                 else 
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                
                if (!activity.mCalled) 
                    throw new SuperNotCalledException(
                        "Activity " + r.intent.getComponent().toShortString() +
                        " did not call through to super.onCreate()");
                
                r.activity = activity;
                r.stopped = true;
                if (!r.activity.mFinished) 
                    activity.performStart();
                    r.stopped = false;
                
                if (!r.activity.mFinished) 
                    if (r.isPersistable()) 
                        if (r.state != null || r.persistentState != null) 
                            mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
                                    r.persistentState);
                        
                     else if (r.state != null) 
                        mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
                    
                
                if (!r.activity.mFinished) 
                    activity.mCalled = false;
                    if (r.isPersistable()) 
                        mInstrumentation.callActivityOnPostCreate(activity, r.state,
                                r.persistentState);
                     else 
                        mInstrumentation.callActivityOnPostCreate(activity, r.state);
                    
                    if (!activity.mCalled) 
                        throw new SuperNotCalledException(
                            "Activity " + r.intent.getComponent().toShortString() +
                            " did not call through to super.onPostCreate()");
                    
                
            
            r.paused = true;

            mActivities.put(r.token, r);
  ...
        return activity;

我们看看makeApplication方法,这里的packageInfo是一个loadedApk对象,

 public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) 
        if (mApplication != null) 
            return mApplication;
        

        Application app = null;

        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) 
            appClass = "android.app.Application";
        

        try 
            java.lang.ClassLoader cl = getClassLoader();
            if (!mPackageName.equals("android")) 
                initializeJavaContextClassLoader();
            
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(//创建Application
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
         catch (Exception e) 
            if (!mActivityThread.mInstrumentation.onException(app, e)) 
                throw new RuntimeException(
                    "Unable to instantiate application " + appClass
                    + ": " + e.toString(), e);
            
        
        mActivityThread.mAllApplications.add(app);
        mApplication = app;

        if (instrumentation != null) 
            try 
                instrumentation.callApplicationOnCreate(app);
             catch (Exception e) 
                if (!instrumentation.onException(app, e)) 
                    throw new RuntimeException(
                        "Unable to create application " + app.getClass().getName()
                        + ": " + e.toString(), e);
                
            
        

        // Rewrite the R 'constants' for all library apks.
        SparseArray<String> packageIdentifiers = getAssets(mActivityThread)
                .getAssignedPackageIdentifiers();
        final int N = packageIdentifiers.size();
        for (int i = 0; i < N; i++) 
            final int id = packageIdentifiers.keyAt(i);
            if (id == 0x01 || id == 0x7f) 
                continue;
            

            rewriteRValues(getClassLoader(), packageIdentifiers.valueAt(i), id);
        

        return app;
    


20.ListView优化

(1)复用ConvertView,避免每次填充布局,保证了只填充屏幕内看到的条目

(2)使用ViewHolder,保存控件的引用,避免每次查找控件

(3)在getView中不要做复杂逻辑

(4)滑动的时候不加载图片

(5)异步加载图片

(6)ItemView的布局层次减少

(7)数据分批和分页加载

(8)局部刷新

(9)使用内存和本地缓存

(10)设置下面的属性

     //是否允许条目的绘制缓存
        listView.setScrollingCacheEnabled(false);
        //5.0以后废弃了,通过setLayerType可以控制子View的缓存行为
        listView.setAnimationCacheEnabled(false);

21.Android语言切换

Androi7.0语言设置

Android7.0语言设置2

Android语言设置

22.Sqlite数据库面试题

产生的journal文件的作用:用于事务的回滚,如果没有操作异常或者不需要回滚,那么这个文件大小为0.如果程序crash了,该文件会被保留,下次打开数据库文件时,系统会检查有没有journal文件存在,有则用它来恢复数据。

如何插入大量的数据,使用事务+SqliteStatement,代码如下

   MyDatabase myDatabase = new MyDatabase(this);
        SQLiteDatabase db = myDatabase.getReadableDatabase();
        try
            db.beginTransaction();//开启事务
            //预编译Sql语句
            SQLiteStatement sqLiteStatement = db.compileStatement("");
            for (int i = 0; i < 10000; i++) 
                //绑定数据
                sqLiteStatement.bindLong(1,22);
                sqLiteStatement.bindString(2,"test");
                sqLiteStatement.executeInsert();//执行插入
            
            db.setTransactionSuccessful();
        catch(Exception e)
            e.printStackTrace();
        finally 
            db.endTransaction();
            db.close();
        

其他优化

1.为表创建索引,语句如下

create index index_name on table_name

注意:索引不应该使用在比较小的表上,索引会增加查询的性能,降低插入和更新的性能,所以如果查询多,可以用索引。

2.语句拼接使用Stringbuilder代替String

3.查询时返回更少的结果

4.少用cursor.getColumnIndex,可以在建表的时候用Static变量记住列的index

Sqlite多线程访问

SQLiteDatabase 多线程访问需要注意的问题

23.线程池

  public static void main(String[] args) 
        //线程同时执行
        ExecutorService service = Executors.newCachedThreadPool();
        //线程依次执行
        ExecutorService service2 = Executors.newSingleThreadExecutor();
        //每次最多同时执行固定数量的线程
        ExecutorService service3 = Executors.newFixedThreadPool(2);
        //
        ScheduledExecutorService service4 = Executors.newScheduledThreadPool(2);
       //执行下面的会发生RejectedExecutionException
//        service.shutdown();
//        service.submit(new Task());
        //以固定间隔执行任务
//        service4.scheduleAtFixedRate(new Task(),0,2, TimeUnit.SECONDS);
//        //固定延迟执行任务
//        service4.scheduleWithFixedDelay(new Task(),0,2,TimeUnit.SECONDS);
//        service3.execute(new Task());
//        service.submit(new Task());
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,30,
                TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(1));
        for (int i = 0; i < 4; i++) 
            executor.execute(new Task());
        
    

    static class Task implements Runnable

        @Override
        public void run() 
            try 
                Thread.sleep(2000);
                System.out.println("===========run");
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    


接下来看看每种线程池内部的实现

newCachedThreadPool

public static ExecutorService newCachedThreadPool() 
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    

核心线程数为0,最大线程数为整数最大值,任务队列为SynchronousQueue,这个队列在接收到任务的时候,会直接提交给线程池处理。

newSingleThreadExecutor

   public static ExecutorService newSingleThreadExecutor() 
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    

这个线程池很有意思,没有直接返回ThreadPoolExecutor,而是包装了一个FinalizableDelegatedExecutorService,它的源码如下

private static class FinalizableDelegatedExecutorService
            extends DelegatedExecutorService 
        FinalizableDelegatedExecutorService(ExecutorService executor) 
            super(executor);
        
        protected void finalize() 
            super.shutdown();
        
    

我们知道,在当前对象要被垃圾回收器回收之前,会调用它的finalize方法

newFixedThreadPool

 public static ExecutorService newFixedThreadPool(int nThreads) 
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    

核心线程数等于最大线程数

ScheduledThreadPoolExecutor

public ScheduledThreadPoolExecutor(int corePoolSize) 
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    

核心线程树由我们指定,最大线程数是整数最大值,

24.Java中程序何时终止

当程序中的所有非守护线程终止的时候,程序就终止了。

  public static void main(String[] args) 
        Thread t1 = new Thread()
            @Override
            public void run() 
                try 
                    Thread.sleep(2000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                System.out.println("run");
            
        ;
        t1.start();
        System.out.println("main");
    

运行上面的程序,会立即打印出main,然后隔2秒打印出run,程序结束。加入我们把t1设置为守护线程呢。

 public static void main(String[] args) 
        Thread t1 = new Thread()
            @Override
            public void run() 
                try 
                    Thread.sleep(2000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                System.out.println("run");
            
        ;
        //设置为守护线程,当程序中只剩下守护线程时,JVM就退出了
        t1.setDaemon(true);
        t1.start();
        System.out.println("main");
    
再次运行上面的程序,结果是打印出main之后程序就退出了,因为main线程执行完以后,就剩下一个守护线程了,JVM就退出了。






以上是关于Android面试题的主要内容,如果未能解决你的问题,请参考以下文章

Netty相关面试题

年后上来面试了13家企业Android岗位,面试题整理

备战2022一二线互联网公司Android面试题汇总,48份(2010-2021)大厂面试题整理分享

备战2022一二线互联网公司Android面试题汇总,48份(2010-2021)大厂面试题整理分享

闭关啃完1932页《Android开发工程师面试题》,成功定级自如T4,月薪20k

备战金三银四2022最新Android中高级大厂面试题汇总,高薪必备(文末巨量资料免费分享)