天天看点

Android 面试题笔记(一)

每日更新每日学习面试笔记,来自https://github.com/Moosphan/Android-Daily-Interview/

  • 1、自定义 Handler 时如何有效地避免内存泄漏问题?

    问题原因:一般非静态内部类持有外部类的引用的情况下,造成外部类在使用完成后不能被系统回收内存,从而造成内存泄漏。这里 Handler 持有外部类 Activity 的引用,一旦 Activity 被销毁,而此时 Handler 依然持有 Activity 引用,就会造成内存泄漏。

解决方案:将 Handler 以静态内部类的形式声明,然后通过弱引用的方式让 Handler 持有外部类 Activity 的引用,这样就可以避免内存泄漏问题了:

1.自定义的静态handler

2.可以加一个弱引用

3.还有一个主意的就是当你activity被销毁的时候如果还有消息没有发出去 就remove掉吧

4.removecallbacksandmessages去清除Message和Runnable 加null 写在生命周的ondestroy()就行

private WeakHandler weakHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        weakHandler = new WeakHandler(this);
    }

    static class WeakHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;

        WeakHandler(MainActivity activity) {
            mActivity = new WeakReference<>(activity);  // 弱引用
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity activity = mActivity.get();
            switch (msg.what) {
                case 1:
                    if (activity != null) {
                        activity.btnDemoOne.setText("ceshi");
                    }
                    break;
                default:
                    break;
            }

        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        weakHandler.removeCallbacksAndMessages(null);
    }
           
  • 2、Activity 与 Fragment 之间常见的几种通信方式?

通常,Fragment 与 Activity 通信存在三种情形:

  • Activity 操作内嵌的 Fragment
  • Fragment 操作宿主 Activity
  • Fragment 操作同属 Activity中的其他 Fragment

在Android中我们可以通过以下几种方式优雅地实现Activity和fragment之间的通信:

  • Handler
  • 广播
  • EventBus
  • 接口回调

    见Activity 与 Fragment 之间的通信

    - 3、一般什么情况下会导致内存泄漏问题?

  • 内部类&匿名内部类实例无法释放(有延迟时间等等),而内部类又持有外部类的强引用,导致外部类无法释放,这种匿名内部类常见于监听器、Handler、Thread、TimerTask
  • 持有静态的Context(Avtivity)和View
  • 资源使用完成后没有关闭File,Cursor,Stream,Bitmap(调用recycle())等相关流的操作
  • 接收器、监听器注册没取消,BraodcastReceiver,ContentObserver
  • 集合类内存泄漏,如果一个集合类是静态的(缓存HashMap),只有添加方法,没有对应的删除方法,会导致引用无法被释放,引发内存泄漏。

顺便说一下内存泄漏和内存溢出的区别

  • 内存溢出(Out of memory):系统会给每个APP分配内存也就是Heap size值,当APP所需要的内存大于了系统分配的内存,就会造成内存溢出;通俗点就是10L桶只能装10L水,但是你却用来装11L的水,那就有1L的水就会溢出
  • 内存泄漏(Memory leak):当一个对象不在使用了,本应该被垃圾回收器(JVM)回收,但是这个对象由于被其他正在使用的对象所持有,造成无法被回收的结果,通俗点就是系统把一定的内存值A借给程序,但是系统却收不回完整的A值,那就是内存泄漏

    - 4、LaunchMode 的应用场景?

    LaunchMode 有四种,分别为 Standard,SingleTop,SingleTask 和 SingleInstance,每种模式的实现原理

  • android:launchMode="standard"可以存在多个实例,这是默认的启动模式,系统总是会在目标栈中创建新的activity实例。Standard 模式是系统默认的启动模式,一般我们 app 中大部分页面都是由该模式的页面构成的,比较常见的场景是:社交应用中,点击查看用户A信息->查看用户A粉丝->在粉丝中挑选查看用户B信息->查看用户A粉丝… 这种情况下一般我们需要保留用户操作 Activity 栈的页面所有执行顺序。
  • android:launchMode=“singleTop” 如果这个 activity 实例已经存在目标栈的栈顶,系统会调用这个 activity 中的 onNewIntent() 方法,并传递 intent,而不会创建新的 activity 实例;如果不存在这个 activity 实例或者 activity 实例不在栈顶,则 SingleTop 和Standard 作用是一样的。SingleTop 模式一般常见于社交应用中的通知栏行为功能,例如:App 用户收到几条好友请求的推送消息,需要用户点击推送通知进入到请求者个人信息页,将信息页设置为 SingleTop 模式就可以增强复用性。
  • android:launchMode=“singleTask” 不会存在多个实例,如果栈中不存在 activity 实例,系统会在新栈的根部创建一个新的 activity;如果这个 activity 实例已经存在,系统会调用这个 activity的 onNewIntent() 方法而不会创建新的 activity 实例。SingleTask 模式一般用作应用的首页,例如浏览器主页,用户可能从多个应用启动浏览器,但主界面仅仅启动一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
  • android:launchMode=“singleInstance” 这种启动模式比较特殊,因为它会启用一个新的栈结构,将 Acitvity 放置于这个新的栈结构中,并保证不再有其他 Activity 实例进入,除此之外,SingleInstance模式和 SingleTask 模式是一样的。SingleInstance 模式常应用于独立栈操作的应用,如闹钟的提醒页面,当你在A应用中看视频时,闹钟响了,你点击闹钟提醒通知后进入提醒详情页面,然后点击返回就再次回到A的视频页面,这样就不会过多干扰到用户先前的操作了。
  • 5、如何实现多线程中的同步?

    线程间的同步问题一般借助于同步锁 Synchronized 和 volatile 关键字实现:

public class Singleton{
    private volatile static Singleton mSingleton;
    private Singleton(){
    }
    public static Singleton getInstance(){
      if(mSingleton == null){
        synchronized(Singleton.class){
         if(mSingleton == null)
           mSingleton = new Singleton();
      }
    }
    return mSingleton;
  }
}
           
  • 6、Android 补间动画和属性动画的区别?
  • 补间动画

    补间动画,主要是向View对象设置动画效果,包括AlphaAnimation 、RotateAnimation 、ScaleAnimation 、TranslateAnimation 这4种效果,对应的xml标签分别是alpha、rotate、scale、translate。通过为动画设置初始和终止对应的值,根据插值器和duration计算动画过程中间相应的值实现平滑运动,即设置初始和终止状态,插值器来计算填补初始状态到终止状态间的动画

  • 属性动画

    属性动画可以对任何对象的属性做动画而不仅仅是View,甚至可以没有对象。除了作用对象进行扩展外,属性动画的效果也加强了,不仅能实现View动画的4中效果,还能实现其它多种效果,这些效果都是通过ValuAnimator或ObjectAnimator、AnimatorSet等来实现的。

  • 7、ANR出现的场景及解决方案?

    在Android中,应用的响应性被活动管理器(Activity Manager)和窗口管理器(Window Manager)这两个系统服务所监视。当用户触发了输入事件(如键盘输入,点击按钮等),如果应用5秒内没有响应用户的输入事件,那么,Android会认为该应用无响应,便弹出ANR对话框。而弹出ANR异常,也主要是为了提升用户体验。

    解决方案是对于耗时的操作,比如访问网络、访问数据库等操作,需要开辟子线程,在子线程处理耗时的操作,主线程主要实现UI的操作

  • 8、谈谈 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方法。

    问题扩展

    A. messageQueue.next 是阻塞式的取消息, 如果有 delay 会调用 nativeWake;

    那么问题来了, 线程挂起了, 是挂起的 UI线程吗? 答案是 YES, 为什么我没有察觉呢?

    还有就是 nativeWake 和 nativePollOnce 的实现原理;

B. looper.loop 既然是 while-true 为什么不会卡死?

C. MessageQueue 是队列吗? 他是什么数据结构呢?

D. handler 的postDelay, 时间准吗? 答案是不准, 为什么呢?

E. handler 的 postDelay 的时间是 system.currentTime 吗? 答案是 NO, 你知道是什么吗?

F. 子线程run方法使用 handler 要先 looper.prepare(); 再 handler.post; 再 looper.loop();

那么问题来了, looper.loop(); 之后 在 handler.post 消息, 还能收到吗? 答案是 NO, 为什么?

G. handler 这么做到的 一个线程对应一个 looper, 答案是threadLocal, 你对ThreadLocal 有什么了解吗?

H. 假设先 postDelay 10ms, 再postDelay 1ms, 你简单描述一下, 怎么处理这2条消息?

I. 你知道主线程的Looper, 第一次被调用loop方法, 在什么时候吗? 哪一个类

J. 你对 IdleHandler 有多少了解?

K. 你了解 HandlerThread 吗?

L. 你对 Message.obtain() 了解吗, 或者你知道 怎么维护消息池吗;

  • 9、什么是Sticky事件?

    在Android开发中,Sticky事件只指事件消费者在事件发布之后才注册的也能接收到该事件的特殊类型。Android中就有这样的实例,也就是Sticky Broadcast,即粘性广播。正常情况下如果发送者发送了某个广播,而接收者在这个广播发送后才注册自己的Receiver,这时接收者便无法接收到刚才的广播,为此Android引入了StickyBroadcast,在广播发送结束后会保存刚刚发送的广播(Intent),这样当接收者注册完Receiver后就可以接收到刚才已经发布的广播。这就使得我们可以预先处理一些事件,让有消费者时再把这些事件投递给消费者。

  • 10、抽象类与接口的区别?

    1.抽象类是用来捕捉子类的通用特性的 。它不能被实例化,只能被用作子类的超类。抽象类是被用来创建继承层级里子类的模板。

    2.接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。这就像契约模式,如果实现了这个接口,那么就必须确保使用这些方法。接口只是一种形式,接口自身不能做任何事情。

    大体区别如下:

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

    补充详细见抽象类与接口的区别

  • 11、BroadcastReceiver 与 LocalBroadcastReceiver 有什么区别?
  • BroadcastReceiver 是跨应用广播,利用Binder机制实现,支持动态和静态两种方式注册方式。
  • LocalBroadcastReceiver是应用内广播,利用Handler实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册。
/**
 * 自定义广播
 */
public static final String LOGIN_ACTION = "com.archie.action.LOGIN_ACTION";
//广播接收器
    private LoginBroadcastReceiver mReceiver = new LoginBroadcastReceiver();
 
    //注册广播方法
    private void registerLoginBroadcast(){
        IntentFilter intentFilter = new IntentFilter(LoginActivity.LOGIN_ACTION);
        LocalBroadcastManager.getInstance(mContext).registerReceiver(mReceiver,intentFilter);
    }
 
    //取消注册
    private void unRegisterLoginBroadcast(){
        LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mReceiver);
    }
/**
     * 自定义广播接受器,用来处理登录广播
     */
    private class LoginBroadcastReceiver extends BroadcastReceiver{
 
        @Override
        public void onReceive(Context context, Intent intent) {
            //处理我们具体的逻辑,更新UI
        }
    }


/**
     * 发送我们的局部广播
     */
    private void sendBroadcast(){
        LocalBroadcastManager.getInstance(this).sendBroadcast(
                new Intent(LOGIN_ACTION)
        );
    }
 
           

小结:

1、LocalBroadcastManager在创建单例传参时,不用纠结context是取activity的还是Application的,它自己会取到tApplicationContext。

2、LocalBroadcastManager只适用于代码间的,因为它就是保存接口BroadcastReceiver的对象,然后直接调用其onReceive方法。

3、LocalBroadcastManager注册广播后,当该其Activity或者Fragment不需要监听时,记得要取消注册,注意一点:注册与取消注册在activity或者fragment的生命周期中要保持一致,例如onResume,onPause。

4、LocalBroadcastManager虽然 支持对同一个BroadcastReceiver可以注册多个IntentFilter,但还是应该将所需要的action都放进一个 IntentFilter,即只注册一个IntentFilter,这只是我个人的建议。

5、LocalBroadcastManager所发 送的广播action,只能与注册到LocalBroadcastManager中BroadcastReceiver产生互动。如果你遇到了通过 LocalBroadcastManager发送的广播,对面的BroadcastReceiver没响应,很可能就是这个原因造成的。

  • 12、请简要谈一谈单例模式?

    单例分为懒汉模式和恶汉模式,主要是双检查、静态内部类、枚举等

    懒汉模式有线程安全和非线程安全的区别

    实现线程安全的懒汉模式有多重 其中一种是加double check,一种是静态内部类

/**
 * 双重检查
 */
public class SingletonDoubleCheck {
    private SingletonDoubleCheck() { }

    private static volatile SingletonDoubleCheck instance;//代码1

    public static SingletonDoubleCheck getInc() {
        if (null == instance) {//代码2
            synchronized (SingletonDoubleCheck.class) {
                if (null == instance) {//代码3
                    instance = new SingletonDoubleCheck();//代码4
                }
            }
        }
        return instance;
    }
}


/**
 * 静态内部类实现单例
 * 
 */
public class SingleDemo4 {
    private static SingleDemo4 instance;

    private static class SingleDemo4Holder {
        private static final SingleDemo4 instance = new SingleDemo4();
    }

    private SingleDemo4() {
        if (instance != null) {
            throw new RuntimeException();
        }
    }

    /**
     * 调用这个方法的时候,JVM才加载静态内部类,才初始化静态内部类的类变量。由于由JVM初始化,保证了线程安全性,
     * 同时又实现了懒加载
     * @return
     */
    public static SingleDemo4 getInstance() {
        return SingleDemo4Holder.instance;
    }
}
           

更多详细单例见单例的五种实现方式,及其性能分析

在代码 在多线程中 两个线程可能同时进入代码2, synchronize保证只有一个线程能进入下面的代码,

此时一个线程A进入一个线程B在外等待, 当线程A完成代码3 和代码4之后,

线程B进入synchronized下面的方法, 线程B在代码3的时候判断不过,从而保证了多线程下 单例模式的线程安全,

另外要慎用单例模式,因为单例模式一旦初始化后 只有进程退出才有可能被回收,如果一个对象不经常被使用,尽量不要使用单例,否则为了几次使用,一直让单例存在占用内存。

接着上一篇Android 面试题笔记(一)

13、Window和DecorView是什么?DecorView又是如何和Window建立联系的?

DecorView的作用

DecorView是顶级View,本质就是一个FrameLayout

包含了两个部分,标题栏和内容栏

内容栏id是content,也就是activity中setContentView所设置的部分,最终将布局添加到id为content的FrameLayout中

获取content:ViewGroup content = findViewById(R.android.id.content)

获取设置的View:content.getChidlAt(0)

Window是什么?

表示一个窗口的概念,是所有View的直接管理者,任何视图都通过Window呈现(单击事件由Window->DecorView->View; Activity的setContentView底层通过Window完成)

Window是一个抽象类,具体实现是PhoneWindow

创建Window需要通过WindowManager创建

WindowManager是外界访问Window的入口

Window具体实现位于WindowManagerService中

WindowManager和WindowManagerService的交互是通过IPC完成

DecorView又是如何和Window建立联系的?

在Activity的启动流程中,处理onResume()的相关方法中,将DecorView作为Window的成员变量保存到Window内部

DecorView与Window建立联系又有什么用呢?例如Activity的onSaveInstanceState()进行数据保存时,就通过window内部的DecorView触发整个View树进行状态保存

//ActivityThread.java

final void handleResumeActivity(IBinder token, …) {

//1. 创建DecorView,设置为不可见INVISIBLE

View decor = r.window.getDecorView();

decor.setVisibility(View.INVISIBLE);

//2. 获取到WindowManager, addView方法将DecorView添加到Window中

ViewManager wm = a.getWindowManager();

wm.addView(decor, l);

//3. 将DecorView设置为visible

r.activity.makeVisible();

}

  • 14、对于 Context,你了解多少?

    Context 宏观来说是一个描述应用程序全局信息的场景,当然,本质上来说,这个“场景”其实是一个抽象类详细见Android Context 上下文 你必须知道的一切

  • 15、SharedPreferences 是线程安全的吗?它的 commit 和 apply 方法有什么区别?

    SharedPreferences 是线程安全的 进程不安全的, commit 是同步写入有返回值,apply是异步写入。

    apply没有返回值而commit返回boolean表明修改是否提交成功

    apply是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘, 而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内容,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。

    由于在一个进程中,sharedPreference是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,建议使用apply,当然需要确保提交成功且有后续操作的话,还是需要用commit的。

    - 16、HashMap 的实现原理?

  • 数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
  • 链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
  • Hashmap: 综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。

    更多可参考以下文章:

    HashMap 原理以及源码解析

    HashMap 碰撞问题

    HashMap 中的负载因子

  • 17、简述一下 Android 中 UI 的刷新机制?

    应用层的:

  • 界面刷新的本质流程

    通过ViewRootImpl的scheduleTraversals()进行界面的三大流程。

    调用到scheduleTraversals()时不会立即执行,而是将该操作保存到待执行队列中。并给底层的刷新信号注册监听。

    当VSYNC信号到来时,会从待执行队列中取出对应的scheduleTraversals()操作,并将其加入到主线程的消息队列中。

    主线程从消息队列中取出并执行三大流程: onMeasure()-onLayout()-onDraw()

  • 同步屏障的作用

    同步屏障用于阻塞住所有的同步消息(底层VSYNC的回调onVsync方法提交的消息是异步消息)

    用于保证界面刷新功能的performTraversals()的优先执行。

  • 同步屏障的原理?

    主线程的Looper会一直循环调用MessageQueue的next方法并且取出队列头部的Message执行,遇到同步屏障(一种特殊消息)后会去寻找异步消息执行。如果没有找到异步消息就会一直阻塞下去,除非将同步屏障取出,否则永远不会执行同步消息。

    界面刷新操作是异步消息,具有最高优先级

    我们发送的消息是同步消息,再多耗时操作也不会影响UI的刷新操作

系统层的:

首先屏幕是 大约16.6ms刷新一次(固定的),当界面需要改变时, CPU开始计算,将计算结果 赋予 GPU 的buffer缓存起来,等待刷新时间的到来,然后根据buffer的数据刷新界面。如果当前界面没有变化,CPU不用计算,也不会给GPU的buffer赋值啥的,这个buffer也就没变化,等到刷新时间的到来,会依旧根据buffer刷新屏幕

结论是:界面改不改变都会刷新界面,只是在于CPU是否计算这点区别

UI刷新卡顿,基本都在于卡在CPU计算这一环节,对于根据GPU 的buffer刷新这一环节,在系统里有很高的优先级

  • 18、Serializable和Parcelable的区别?

    Serializable是属于Java自带的,本质是使用了反射。序列化的过程比较慢,这种机制在序列化的时候会创建很多临时的对象,比引起频繁的GC。Parcelable 是属于 Android 专用。不过不同于Serializable,Parcelable实现的原理是将一个完整的对象进行分解。而分解后的每一部分都是Intent所支持的数据类型。 如果在内存中使用建议Parcelable。持久化操作建议Serializable;目前AS安装android parcelable code generator插件可直接生成Parcelable

  • 19、Android进程间的通信方式

    1、Bundle的使用

    可以看到Bundle实现了Parcelable 接口。

    优点:简单易用

    缺点:只能传递Bundle支持的数据类型

    使用场景:四大组件间的进程通讯

2.文件共享

优点:简单易用

缺点:不适合高并发的场景,不能做到即时通讯。

使用场景:无并发访问的情景,简单的交换数据,实时性要求不高。

3.AIDI

优点:功能强大,支持一对多并发通信,支持实时通信。

缺点:一定要处理好线程同步的问题

使用场景:一对多进行通讯,有RPC(远程过程调用协议)的需求

4.Messenger(信使)

优点:功能一般,支持一对多串行通信,支持实时通信。

缺点:不能很好的处理高并发场景,不支持RPC,数据通过Message进行传输,因此只能支持Bundle支持的数据类型。

使用场景:低并发的一对多的实时通讯,没有RPC的需求或者说没有返回结果的RPC(不调用服务端的相关方法)

5.ContentProvider

优点:主要用于数据访问,支持一对多的并发数据共享。

缺点:受约束,主要针对数据源的增删改查。

使用场景:一对多的数据共享。

6.Socket(套接字)

优点:功能强大,通过读写网络传输字节流,支持一对多的并发的实时通讯。

缺点:不支持直接的RPC(这里我也不是很明白,间接的怎么实现?)

使用场景:网络的数据交换

20、请简述一下String、StringBuffer和StringBuilder的区别?

  • String 为字符串常量,一旦创建不可以被修改,是线程安全的;String 类使用 final

    修饰符,不可以被继承;String 的长度是不变的。适用于少量操作的字符串。

  • StringBuffer 为字符串变量,长度是可变的,线程安全。适用于多线程下在字符缓冲区进行大量字符串操作
  • StringBuilder 为字符串变量,长度是可变的,线程不安全。适用于单线程下在字符缓冲区进行大量字符串操作。
  • 字符串操作在执行速度:StringBuilder > StringBuffer > String

    21、请简述从点击图标开始app的启动流程?

    ①点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;

    ②system_server进程接收到请求后,向zygote进程发送创建进程的请求;

    ③Zygote进程fork出新的子进程,即App进程;

    ④App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;

    ⑤system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;

    ⑥App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;

    ⑦主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。

    ⑧到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

  • 22、IntentService 的应用场景和使用姿势?

    IntentService是Service的子类,比普通的Service增加了额外的功能。先看Service本身存在两个问题:Service不会专门启动一条单独的进程,Service与他所在应用位于同一个进程中。

    Service也不是专门一条新进程,因此不应该在Service中直接处理耗时的任务。

    特点:

    IntentService会创建独立的worker线程来处理所有的Intent请求;

    会创建独立的worker线程来处理onHandleIntent()方法实现的代码,无需处理多线程的问题;

    所有请求处理完成后,IntentService会自动停止,无需调用stopSelf()方法停止Service;

    为Service的onBind()提供默认实现,返回null;

    为Service的onStartCommand提供默认实现,将请求Intent添加到队列中;

    接着上一篇面试题Android 面试题笔记(二)

    23、IntentFilter是什么?有哪些使用场景?

(1)IntentFilter是和intent相匹配的,其中action,category,组成了匹配规则。同时intentFilter还可以设置优先级,其中默认是0,范围是【-1000,1000】,值越大优先级越高。并且IntentFilter多被通过AndroidManifest.xml的形式使用。

(2) 使用场景

activity的隐式启动和广播的匹配

(3)IntentFilter的匹配规则

IntentFilter的过滤信息有action,category,data.一个组件可以包含多个intent-filter,一个intent只要能完全匹配一组intent-filter即可成功的启动对应的组件。

24、回答一下什么是强、软、弱、虚引用以及它们之间的区别?

  • 强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

  • 软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。

软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列 ReferenceQueue 联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 ReferenceQueue 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

- 25、AsyncTask的优点和缺点?

优点:使用方便,既可以执行串行任务,也可以执行并行任务

缺点:默认使用串行任务执行效率低,不能充分利用多线程加快执行速度;如果使用并行任务执行,在任务特别多的时候会阻塞UI线程获得CPU时间片,后续做线程收敛需要自定义AsynTask,将其设置为全局统一的线程池,改动量比较大

  • 26、对于面向对象的六大基本原则了解多少?
  • 单一职责(Single Responsibility Principle):一个类只做一件事,可读性提高
  • 里式替换原则( Liskov Substitution Principle):依赖继承和多态,就是能用父类的地方就可以用子类替换,用子类的但不能用父类。
  • 依赖倒置原则(Dependence Inversion Principle):依赖抽象,就是模块之间的依赖通过抽象发生。
  • 开闭原则(Open-Close Principle):不管是实体类,模块还是函数都应该遵循对扩展开放对修改关闭。还是要依赖封装和继承
  • 接口隔离原则(Interface Segregation Principle):一个类对另一个类的依赖应该建立在最小的接口上,如果接口太大,我们需要把它分割成一些更细小的接口,也是为了降低耦合性
  • 迪米特原则(Law of Demeter ):也称最少知识原则,也就是说一个类应该对自己需要耦合或者调用的类知道的最少,只需知道该方法即可,实现细节不必知道。
  • 27、LinearLayout, FrameLayout, RelativeLayout 哪个效率高, 为什么?

    对于比较三者的效率那肯定是要在相同布局条件下比较绘制的流畅度及绘制过程,在这里流畅度不好表达,并且受其他外部因素干扰比较多,比如CPU、GPU等等,我说下在绘制过程中的比较,1、Fragment是从上到下的一个堆叠的方式布局的,那当然是绘制速度最快,只需要将本身绘制出来即可,但是由于它的绘制方式导致在复杂场景中直接是不能使用的,所以工作效率来说Fragment仅使用于单一场景,2、LinearLayout 在两个方向上绘制的布局,在工作中使用页比较多,绘制的时候只需要按照指定的方向绘制,绘制效率比Fragment要慢,但使用场景比较多,3、RelativeLayout 它的没个子控件都是需要相对的其他控件来计算,按照View树的绘制流程、在不同的分支上要进行计算相对应的位置,绘制效率最低,但是一般工作中的布局使用较多,所以说这三者之间效率分开来讲个有优势、不足,那一起来讲也是有优势、不足,所以不能绝对的区分三者的效率

  • 28、请简述一下 Android 7.0 的新特性?

    1.低电耗功能改进

    2.引入画中画功能

    3.引入“长按快捷方式”,即App Shortcuts

    4.引入混合模式,同时存在解释执行/AOT/JIT,安装应用时默认不全量编译,使得安装应用时间大大缩短

    5.引入了对私有平台库限制,然而用一个叫做Nougat_dlfunctions的库就行

    6.不推荐使用file:// URI传递数据,转而推荐使用FileProvider

    7.快速回复通知

  • 29、 Android 8.0 的新特性

1、通知渠道 — Notification Channels

2、画中画模式 — PIP

3、自适应图标 — Adaptive Icons

4、定时作业调度

5、后台限制

6、广播限制

7、后台位置限制

8、WebView API

9、多显示器支持

10、 统一的布局外边距和内边距

11、指针捕获

12、输入和导航

13、新的 StrictMode 检测程序

14、指纹手势

15、更新的 ICU4J Android Framework API

  • 30、Android9.0新特性?

    1、室内WIFI定位

    2、“刘海”屏幕支持

    3、通知

    4、增强体验

    5、通道设置、广播以及免打扰

    6、多相机支持和相机更新

    7、新的图片解码

    8、动画

    9、HDR VP9视频,HEIF图像压缩和媒体API

    10、JobScheduler中的数据成本敏感度

    11、神经网络API 1.1

    12、改进表单自动填充

    13、安全增强

    14、Android 备份加密

  • 31、请谈谈你对 MVC 和 MVP 的理解?

1.MVC

用户首先通过View发起交互,View调用Controller执行业务逻辑,Controller修改Model,然后View通过观察者模式检测到Model的变化(具体表现形式可以是Pub/Sub或者是触发Events),刷新界面显示。

从这里可以看出,主要业务逻辑都在Controller中,Controller会变得很重。MVC比较明显的缺点:

View依赖特定的Model,无法组件化

View和Controller紧耦合,如果脱离Controller,View难以独立应用(功能太少)

2.MVP

为了克服MVC的上述缺点,MVP应运而生。在MVP中,View和Model是没有直接联系的,所有操作都必须通过Presenter进行中转。View向Presenter发起调用请求,Presenter修改Model,Model修改完成后通知Presenter,Presenter再调用View的相关接口刷新界面。这样,View就不需要监听具体Model的变化了,只需要提供接口给Presenter调用就可以了。MVP具有以下优点:

View可以组件化,不需要了解业务逻辑,只需提供接口给Presenter

便于测试:只需要给Presenter mock一个View,实现View的接口即可

- 32、谈谈Android的事件分发机制?

  • 会经过Activity->ViewGroup->view,一次往下传递事件,如果一直不拦截再回调回来。
  • 主要经过三个方法,dispatchTouchEvent(分发事件),oninterceptTouchEvent(是否拦截View中不存在),onTouchEvent(处理)。
  • 三个方法的用法是,先用dispatchTouchEvent来分发事件,然后用oninterceptTouchEvent来判断是否拦截该任务(此方法在dispatchTouchEvent内部),如果不拦截直接dispatch向下回调,如果拦截就调用自己的onTouchEvent来处理事件。
  • 如果由setOnClickListener方法会先执行onClick.

    更多事件分发机制见讲讲 Android 的事件分发机制

  • 33、谈谈ArrayList和LinkedList的区别?

    ArrayList和LinkedList的大致区别:

    1.ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。

    2.对于随机访问的get和set方法,ArrayList要优于LinkedList,因为LinkedList要移动指针。

    3.对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

    性能上的缺点:

    1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。

    2.在ArrayList集合中添加或者删除一个元素时,当前的列表所所有的元素都会被移动。而LinkedList集合中添加或者删除一个元素的开销是固定的。

    3.LinkedList集合不支持 高效的随机随机访问(RandomAccess),因为可能产生二次项的行为。

    4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

  • 34、handlerThread使用场景分析及原理?

当我们需要向子线程发送消息处理耗时操作时可使用handlerThread,详细使用介绍见

handlerThread使用场景分析及源码解析

  • 35、针对RecyclerView你做了哪些优化?

    1,减少view type的种类,如果样式差别不大,可以公用一个布局。因为inflate调用比公用布局的绘制占用更多的性能。

    2,可以使用DiffUtil去刷新数据,notifyDataSetChanged性能太低而且不会出发增删动画。(子线程计算新旧数据,主线程刷新recylerView)

    3,分页加载

    4,有大量图片时,滚动停止加载图片,停止才通知adapter去加载

    5,设置合理的RecycledViewPool

    6,item的高度固定时setHasFixedSize(true)

    7,在ViewHolder中设置点击事件而不是在onBindViewHolder

  • 36,请说一下HashMap与HashTable的区别?

    HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。HashMap的工作原理、ArrayList与Vector的比较以及这个问题是有关Java 集合框架的最经典的问题。Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。Hashtable和HashMap在Java面试中相当容易被问到,甚至成为了集合框架面试题中最常被考的问题,所以在参加任何Java面试之前,都不要忘了准备这一题。

  • 1父类不同

第一个不同主要是历史原因。Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现。

public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable {…}

public class Hashtable<K, V> extends Dictionary<K, V> implements Map<K, V>, Cloneable, Serializable {…}

而HashMap继承的抽象类AbstractMap实现了Map接口:

public abstract class AbstractMap<K, V> implements Map<K, V> {…}

  • 2 线程安全不一样

Hashtable 中的方法是同步的,而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。

  • 3允不允许null值

Hashtable中,key和value都不允许出现null值,否则会抛出NullPointerException异常。

而在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

  • 4遍历方式的内部实现上不同

Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

  • 5哈希值的使用不同

HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

  • 6 内部实现方式的数组的初始大小和扩容的方式不一样

HashTable中的hash数组初始大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

  • 37,简述一下自定义View的流程?

    自定义属性;

    选择和设置构造方法;

    重写onMeasure()方法;

    重写onDraw()方法;

    重写onLayout()方法;

    重写其他事件的方法(滑动监听等);

    更多见自定义view的三种实现方式Android自定义View的三种实现方式

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

    死锁产生的条件

    一般来说,出现死锁问题需要满足以下条件

  • 互斥条件:一个资源每次只能被一个线程使用
  • 请求与保证条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源,在未使用完成之前,不能强行剥夺
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

在JAVA编程中,有3中典型的死锁类型:

  • 静态的锁顺序死锁
  • 动态的锁顺序死锁
  • 协作对象之间发生的死锁
  • 典型死锁例子
  • 注意以下代码都是错误代码

1.静态的锁顺序死锁

class Test{
    final Object objA = new Object();
    final Object objB = new Object();
    
    public void a(){
        //注意这里   先A后B
        synchronized(objA){
            synchronized(objB){
                //sth....
            }
        }
    }
    
    public void b(){
        //注意这里    先B后A
        synchronized(objB){
            synchronized(objA){
                //sth....
            }
        }
    }
}
           

2.动态的锁顺序死锁

动态的锁顺序死锁是指两个线程调用同一个方法时,传入的参数颠倒造成的死锁。如下情景,一个线程调用了transferMoney(转账)方法并传入参数accountA,accountB;另一个线程调用了transferMoney方法并传入参数accountB,accountA。此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个线程获得了accountA锁并等待accountB锁,第二个线程获得了accountB锁并等待accountA锁。

3.协作对象之间发生的死锁

有时,死锁并不会那么明显,比如两个相互协作的类之间的死锁,比如:一个线程调用了A对象的a方法,另一个线程调用了B对象的b方法。此时可能会发生,第一个线程持有A对象锁并等待B对象锁,另一个线程持有B对象锁并等待A对象锁。

  • 39、“equals”与“==”、“hashCode”的区别和使用场景?

    我们一般这么理解

    equal比较的是内容

    == 比较的存储地址或基本数据类型的数值比较(数学意义)

    hashCode 对内存分配的位置确定

使用场景

equal一般比较内容相等 比如字符串相等

==一般比较数值 或者null判断

hashcode我们一般用来判断来两个对象是否相等,但这里需要注意的是 两个对象的hashcode相等,两个对象不一定相等,两个相等的对象hashcode一定相等。

我们为什么要这样判断呢?

因为判断两个对象相等重写equal的重载方法比较多,需要判断 传递性、非空性、自反性、一致性、对称性

  • 40、谈一谈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来说,不管是本地服务还是远程服务,我们需要做的工作都一样简单。

  • 41、synchronized和volatile关键字的区别?

    1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

    2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

    volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

    3.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

    4.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

synchronized 可以保证原子性。他可以保证 在同一时刻,只有一个线程可以访问被 synchronized 修饰的方法,或者代码块。

volatile 不能保证原子性。当时在使用这个关键字后。当被Volatitle 修饰字段的值发生改变后,其他线程会立刻知道这个值已经发生变化了。volatitle 可以保证可见性和有序性。

  • 42、什么是冒泡排序?如何优化?

    冒泡排序算法原理:(从小到大排序)

    1.比较相邻的元素。如果第一个比第二个大,就交换他们两个

    2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,交换一趟后,最后的元素会是最大的数

    3.针对所有的元素重复以上的步骤,除了最后一个

    4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较

优化方案1(定义一个变量l来保存一趟交换中两两交换的次数,如果l==0,则说明排序已经完成,退出for循环)

优化方案2(假如有一个长度为50的数组,在一趟交换后,最后发生交换的位置是10,那么这个位置之后的40个数必定已经有序了,记录下这位置,下一趟交换只要从数组头部到这个位置就可以了)

定义一个变量n来保存一趟交换中最后一次发生交换的位置,并把它传递给下一趟交换

/**
 * 排序思想:
 * 对一组数字进行从小到大或者从大到小的进行排序。
 * 它是通过让相邻的两个元素进行比较,大的元素向下沉,小的元素向上冒
 * arr[0]与arr[1]进行比较,如果前者大于后者,则交换位置
 * 然后arr[1]与arr[2]进行比较,以此类推。当进行到n-1轮后,排序完成。
 */
import java.util.Arrays;
public class Sort {

    public static void main(String[] args){

        int arr[]= {100,90,101,23,13,75};
        int temp=0;
        for(int i=0;i<arr.length-1;i++) {
            for(int j=0;j<arr.length-1-i;j++) {
                if(arr[j]>arr[j+1]) {
                    temp=arr[j+1];
                    arr[j+1]=arr[j];
                    arr[j]=temp;
                }
            }
            System.out.println("第["+(i+1)+"]轮,排序结果:"+ Arrays.toString(arr));
        }
        System.out.print("================================");
        int arr2[]= {100,90,101,23,13,75};
        sort2(arr2);
    }

    /**
     * 优化思路:
     * 假如在第1轮比较当中,发现所有的元素都没有进行交换,则说明此原数据就是有序的,不需要再进行排序
     * @param arr
     */
    public static void sort2(int arr[]){

        int temp=0;
        int flag=0;
        for(int i=0;i<arr.length-1;i++) {
            flag=0;
            for(int j=0;j<arr.length-1-i;j++) {
                if(arr[j]>arr[j+1]) {
                    temp=arr[j+1];
                    arr[j+1]=arr[j];
                    arr[j]=temp;
                    //如果有交换的行为,则flag=1
                    flag=1;
                }
            }
            //说明上面 内for循环中,没有交换任何元素。
            if(flag==0) {
                break;
            }
            System.out.println("第["+(i+1)+"]轮,排序结果:"+Arrays.toString(arr));
        }

    }
}
           
  • 43、分别讲讲 final,static,synchronized 关键字可以修饰什么,以及修饰后的作用?

    static

    static 方法

    static 方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有 this 的,因为它不依附于任何对象,既然都没有对象,就谈不上 this 了。

    public class StaticTest {

    public static void a(){

    }

    public static void main(String[]args){

    StaticTest.a();

    }

    }

    static 变量

    static 变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

    static 代码块

    static 关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static 块可以置于类中的任何地方,类中可以有多个 static 块。在类初次被加载的时候,会按照 static 块的顺序来执行每个 static 块,并且只会执行一次。

    public class StaticTest {

    private static int a ;

    private static int b;

    static {

    a = 1;

    b = 2;

    }

    final

    final 变量

    凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为 final 的都叫作 final 变量。final 变量经常和 static 关键字一起使用,作为常量。

    private final int aa = 1;

    static {

    a = 1;

    b = 2;

    }

    private void init(){

    aa = 2;//报错编译器会提示 不能赋值。。

    }

    final 方法

    final 也可以声明方法。方法前面加上 final 关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为 final。final 方法比非 final 方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。

    public static void main(String[]args){

    StaticTest.a();

    }

    class StaticTest2 extends StaticTest{

    public final void a(){ //这边就会编译器提示不能重写

    }

    }

    **final 类 **

    其实更上面同个道理,使用 final 来修饰的类叫作 final 类。final 类通常功能是完整的,它们不能被继承。Java 中有许多类是 final 的,譬如 String,Interger 以及其他包装类。

    synchronized

    synchronized 是 Java 中解决并发问题的一种最常用的方法,也是最简单的一种方法。synchronized 的作用主要有三个:

确保线程互斥的访问同步代码

保证共享变量的修改能够及时可见

有效解决重排序问题。

synchronized 方法

有效避免了类成员变量的访问冲突:

private synchronized void init(){

aa = 2;

}

synchronized 代码块

这时锁就是对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的 instance 变量(它得是一个对象)来充当锁。

public final void a(){

synchronized (lock){

//代码

}

}

@Override

public void run() {

}

  • 44、什么是 RemoteViews?使用场景有哪些?

    RemoteViews

    RemoteViews翻译过来就是远程视图.顾名思义,RemoteViews不是当前进程的View,是属于SystemServer进程.应用程序与RemoteViews之间依赖Binder实现了进程间通信.

    用法

    通常是在通知栏

//1.创建RemoteViews实例
        RemoteViews mRemoteViews=new RemoteViews("com.example.remoteviewdemo", R.layout.remoteview_layout);

        //2.构建一个打开Activity的PendingIntent
        Intent intent=new Intent(MainActivity.this,MainActivity.class);
        PendingIntent mPendingIntent=PendingIntent.getActivity(MainActivity.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        //3.创建一个Notification
        mNotification = new Notification.Builder(this)
        .setSmallIcon(R.drawable.ic_launcher)
        .setContentIntent(mPendingIntent)
        .setContent(mRemoteViews)
        .build();

        //4.获取NotificationManager
        manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        Button button1 = (Button) findViewById(R.id.button1);
        button1.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                //弹出通知
                manager.notify(1, mNotification);
            }
        });

           
  • 45、什么是反射机制?反射机制的应用场景有哪些?

    Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

    应用场景:

逆向代码,例如反编译

与注解相结合的框架,如 Retrofit

单纯的反射机制应用框架,例如 EventBus(事件总线)

动态生成类框架 例如Gson

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;


/**
 * 对于任何一个类,我们都能够知道这个类有哪些方法和属性。对于任何一个对象,
 * 我们都能够对它的方法和属性进行调用。
 * 我们把这种动态获取对象信息和调用对象方法的功能称之为 反射机制
 */
/**
 * 所谓反射其实是获取类的字节码文件,
 * 也就是.class文件,那么我们就可以通过Class这个对象进行获取
 */
public class HookTest {

    public static void main(String[] args) {
        //第一种方式
        LoopTest loopTest = new LoopTest();
        Class aClass = loopTest.getClass();
        System.out.println(aClass.getName());
        //第二种方式
        Class aclass2 = LoopTest.class;
        System.out.println(aclass2.getName());
        //第三种方式
        try {
            Class aclass3 = Class.forName("LoopTest");
            System.out.println(aclass3.getName());
        }catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }

        /**
         * 那么这3中方式我们一般选用哪种方式呢?第一种已经创建了对象,那么这个时候就不需要去进行反射了,
         * 显得有点多此一举。第二种需要导入类的包,依赖性太强。所以我们一般选中第三种方式。
         */

        /**
         * 三、通过反射获取类的构造方法、方法以及属性
         */

        /**
         * 1、获取构造方法
         */
        Constructor[]constructors = aclass2.getConstructors();
        System.out.println("获取构造方法:");
        for (Constructor constructor1 : constructors){
            System.out.println(constructor1.getName());
        }
        System.out.println("获取类的属性:");
        Field[] fields = aclass2.getFields();
        //88888
        System.out.println("获取类的方法:");
        Method[]methods = aclass2.getMethods();
        for (Method method : methods){
            System.out.println(method.getName());
        }

        /**
         * 反射执行方法
         */

        try {   Class aclass4 = Class.forName("LoopTest");
            Method method   = aclass4.getDeclaredMethod("method",String.class);
            Constructor ct = aclass4.getConstructor(null);
            Object obj = ct.newInstance(null);
            method.invoke(obj,"反射调用");
        } catch (Exception e) {
            e.printStackTrace();
        }


        /**
         * Android中使用场景:其实很多用过的EventBus 、Retrofit 都有涉猎 可以去看看源码
         */
    }

}

           

java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

静态编译:在编译时确定类型,绑定对象。

动态编译:在运行时确定类型,绑定对象。

反射机制的优缺点:

优点:运行期类型的判断,动态加载类,提高代码灵活度。

缺点:性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。

  • 46、Java 中使用多线程的方式有哪些?

    1、继承Thread类创建线程

    Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。

    2、实现Runnable接口创建线程

    如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口

    3、实现Callable接口通过FutureTask包装器来创建Thread线程

    Callable接口(也只有一个方法

    4、4、使用ExecutorService、Callable、Future实现有返回结果的线程

ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。

可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口

  • 47、请简述一下什么是 Kotlin?它有哪些特性?

    设计理念

    1、创建一种兼容Java的语言

    2、让它比Java更安全,能够静态检测常见的陷阱。如:引用空指针

    3、让它比Java更简洁,通过支持variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation等实现。

    4、让它比最成熟的竞争对手Scala语言更加简单。

    Kotlin优势

    1、简洁: 大大减少样板代码的数量。

    2、安全: 避免空指针异常等整个类的错误。

    3、互操作性: 充分利用 JVM、Android 和浏览器的现有库。

    4、工具友好: 可用任何 Java IDE 或者使用命令行构建。

kotlin和java都是运行在java虚拟机的语言。编译后都会生成.class文件。而虚拟机运行的正是.class文件。所以两者都可以用来写Android。再说说个人的一些看法。java作为一门相对时间长一点的语言。相对来说更万能一些。基本上能完成所有的开发场景。而且,因为时间够久,相对来说问题也很少,虽然大家都吐槽分号,类型转换,空指针这些傻瓜操作,但是我并没有觉得不写这些就能对我的开发有质的的提升,唯一让我想学kt的动力就是google的Android实例将来要用kt写。而kotlin作为一门新语言,有他自己的优点,也有一些缺点。具体什么缺点大家看下面的文章吧。

`public class LruCachePhoto {

/**

* 图片 缓存技术的核心类,用于缓存下载好的所有图片,

* 在程序内存达到设定值后会将最少最近使用的图片移除掉

*/

private LruCache<String, Bitmap> mMenoryCache;

public LruCachePhoto() {

//获取应用最大可用内存

int maxMemory = (int) Runtime.getRuntime().maxMemory();

//设置 缓存文件大小为 程序最大可用内存的 1/8

int cacheSize = maxMemory / 8;

mMenoryCache = new LruCache<String, Bitmap>(cacheSize) {

@Override

protected int sizeOf(String key, Bitmap value) {

return value.getByteCount();

}

};

}

/**

* 从 LruCache 中获取一张图片,如果不存在 就返回 null

*

* @param key LurCache 的键,这里是 图片的地址

* @return 返回对应的 Bitmap对象,找不到则为 null

*/

public Bitmap getBitmapFromMemoryCache(String key) {

return mMenoryCache.get(key);

}

/**

* 添加一张图片

* * @param key key

* @param bitmap bitmap

*/

public void addBitmapToCache(String key, Bitmap bitmap) {

if (getBitmapFromMemoryCache(key) == null) {

mMenoryCache.put(key, bitmap);

}

}

}`

  • 57、谈谈怎么给apk瘦身?

    (1)res目录优化:将png格式转webp或svg格式,

    保真压缩图片:可以使用一些图片压缩网站或者工具压缩你的资源文件吧,例如TinyPng、ImageOptim、Zopfli、智图等。

    (2)使用lint删除无用资源:在多人开发过程中,通常都会有漏删无用资源的问题,图片资源也不例外,例如需要删除一个模块的代码时,很容易就会漏删资源文件,所以可以定期使用lint检测出无用的资源文件,原理这里不作介绍,使用方法非常简单,可以直接在AS里面使用,如下图所示。注意:lint检查出来的资源都是无直接引用的,所以如果我们通过getIdentifier()方法引用文件时,lint也会标记为无引用,所以删除时注意不要删除通过getIdentifier()引用的资源。

    (3)方法:Analyze -> Run Inspection by Name -> 输入:Unused resources -> 跳出弹框选择范围即可

    (4)去掉无用资源:打开shrinkResources

    shrinkResources是在编译过程中用来检测并删除无用资源文件,也就是没有引用的资源,minifyEnabled 这个是用来开启删除无用代码,比如没有引用到的代码,所以如果需要知道资源是否被引用就要配合minifyEnabled使用,只有两者都为true时才会起到真正的删除无效代码和无引用资源的目的。打开方式也是非常简单,在build.gralde文件里面打开即可:

    android {

    buildTypes{

    minifyEnabled true

    shrinkResources true

    }

    }

    (5)Proguard代码混淆:

    Proguard是一款免费的Java类文件压缩器、优化器和混淆器,Android Studio已经集成了这个工具,只要经过简单的配置,即可完成,如下代码所示,在build.gradle里面设置minifyEnabled为ture,同时在proguardFiles指向proguard的规则文件即可。

    android {

    buildTypes{

    minifyEnabled true

    proguardFiles ‘proguard.cfg’

    }

    }

  • 58、JVM、Dalvik、ART三者的原理和区别?

    JVM:是Java Virtual Machine的缩写,其并不是指某个特定的虚拟机实现,而指任何能够运行Java字节码(class文件)的虚拟机实现,比如oracle的Hotspot VM

Dalvik:是Google写的一个用于Android的虚拟机,但其严格来说并不算JVM(没有遵守Java虚拟机规范,比如其字节码格式是dex而非class)

该虚拟机在5.0时被ART替代

ART:是Android Runtime的缩写,严格来说并不是一个虚拟机,在4.4~6.0时采用安装时全部编译为机器码的方式实现,7.0时默认不全部编译,采用解释执行+JIT+空闲时AOT以改善安装耗时

ART在安卓4.4时加入,5.0取代dalvik作为唯一实现直到现在。

  • 59、谈谈你是如何优化App启动过程的?

    (1)尽量不要在Application里做耗时操作,能放子线程的放子线程,能延后初始化的延后

    (2)启动页可以做成一个view在主页面加载,同时主页面的一些操作可以在这个过程中开始初始化

    (3)启动页的view层级尽量简单

  • 60、谈一谈单例模式,建造者模式,工厂模式的使用场景?如何合理选择?

    (1)单例模式,一般是指将消耗内存、属性和对象支持全局公用的对象,应该设置为单例模式,如持久化处理(网络、文件等)

    (2)建造者模式,一般见于开发的框架或者属性时可以直接链式设置属性,比如我们看到的AlertDialog,一般用在某些辅助类(如BRVAH的BaseViewHolder)或者开发的框架的时候方便连续设置多个属性和调用多个方法。

    (3)工厂模式,一般用于业务的实体创建,在创建的过程中考虑到后期的扩展。在Android源码中比较常见的有BitmapFactory

    LayoutInflater.Factory,在实体编码的过程中,比如BRVAH的多布局,如果数据类型比较多或者后期需要扩展,则可以通过工厂布局的方式,将实现MultiItemEntity

    接口的实体通过工厂模式创建:
  • 61、谈谈布局优化的技巧?

    1、降低Overdraw(过度绘制),减少不必要的背景绘制

    2、减少嵌套层次及控件个数

    3、使用Canvas的clipRect和clipPath方法限制View的绘制区域

    4、通过imageDrawable方法进行设置避免ImageView的background和imageDrawable重叠

    5、借助ViewStub按需延迟加载

    6、选择合适的布局类型

    7、熟悉API尽量借助系统现有的属性来实现一些UI效果

    8、尽量减少控件个数,对 TextView 左边或者右边有图片可是试用 drawableLeft,drawableRight

  • 61、说一下线程的几种状态?

    1.初始(NEW) ,创建线程对象

    2.运行(RUNNABLE),此时就绪且正在运行一起称为运行

    3.阻塞(BLOCKED),线程阻塞

    4.等待(WAITING),等待中断等操作

    5.超时等待(TIMED_WAITING),可以指定时间返回,不一定需要操作

    6.终止(TERMINATED),线程执行完毕

  • 62、简单介绍下ContentProvider是如何实现数据共享的?

    使用 ContentProvider 可以将数据共享给其他应用,让除本应用之外的应用也可以访问本应用的数据。它的底层是用 SQLite 数据库实现的,所以其对数据做的各种操作都是以 Sql 实现,只是在上层提供的是 Uri,用户只需要关心操作数据的 uri 就可以了,ContentProvider 可以实现不同 app 之间共享。详细使用见ContentProvider跨程序共享数据(一)

  • 63、谈谈App的电量优化?

    (1)GPS

——使用要谨慎,如精确度不高可用WiFi定位或者基站定位,可用;非要用的话,注意定位数据的复用和定位频率的阈值

(2)Process和Service

——按需启动,用完就退出

(3)网络数据交互

——减少网络网络请求次数和数据量;WiFi比手机网络省电

(4)CPU

——减少I/O操作(包括数据库操作),减少大量的计算

(5)减少手机硬件交互

——使用频率优化和选择低功耗模式

(6)避免轮循。可以利用推送。如果非要轮循,合理的设置频率。

应用处于后台时,避免某些数据的传输,比如感应器,定位,视频缓存。

页面销毁时,取消掉网络请求。

限制访问频率,失败后不要无限的重连。

合理的选择定位精度和频率。

使用缓存。如果数据变化周期比较长,可以出一个配置接口,用于记录那些接口有变化。没变化的直接用缓存。

减少广播的使用频率。可以用观察者,startActivityForResult等代替。

  • 64、Java 线程中notify 和 notifyAll有什么区别?

    当线程状态为等待、超时等待会调用notify 和 notifyAll方法通知线程更改状态,此时

    当线程数量为1时,notify 和 notifyAll的效果一样,会唤醒一个线程,并获取锁

    当线程数量大于1时,notify会唤醒一个线程,并获取锁,notifyAll会唤醒所有线程并根据算法选取其中一个线程获取锁,区别在于此时使用notify可能会出现死锁的情况

  • 65、谈一谈你对binder的机制的理解?

    Binder机制:

    1.为了保证进程空间不被其他进程破坏或干扰,Linux中的进程是相互独立或相互隔离的。

    2.进程空间分为用户空间和内核空间。用户空间不可以进行数据交互;内核空间可以进行数据交互,所有进程共用一个内核空间。

    3.Binder机制相对于Linux内传统的进程间通信方式:(1)性能更好;Binder机制只需要拷贝数据一次,管道、消息队列、Socket等都需要拷贝数据两次;而共享内存虽然不需要拷贝,但实现复杂度高。(2)安全性更高;Binder机制通过UID/PID在内核空间添加了身份标识,安全性更高。

    4.Binder跨进程通信机制:基于C/S架构,由Client、Server、Server Manager和Binder驱动组成。

    5.Binder驱动实现的原理:通过内存映射,即系统调用了mmap()函数。

    6.Server Manager的作用:管理Service的注册和查询。

    7.Binder驱动的作用:(1)传递进程间的数据,通过系统调用mmap()函数;(2)实现线程的控制,通过Binder驱动的线程池,并由Binder驱动自身进行管理。

    8.Server进程会创建很多线程处理Binder请求,这些线程采用Binder驱动的线程池,由Binder驱动自身进行管理。一个进程的Binder线程池默认最大是16个,超过的请求会阻塞等待空闲的线程。

    9.Android中进行进程间通信主要通过Binder类(已经实现了IBinder接口),即具备了跨进程通信的能力。

  • 66、什么是线程池?如何创建一个线程池?

    线程池:

    1.线程池:创建多个线程,并管理线程,为线程分配任务并执行。

    2.使用线程池的好处:多个线程的创建会占用过多的系统资源,造成死锁或OOM

    3.线程池的作用:(1)可以复用创建好的线程,减少线程的创建或销毁的开销;(2)提高响应速度,当任务到达时,不需要等待就可以立即执行;(3)可有效控制最大并发的线程数,提高系统资源的利用率。防止死锁或OOM;(4)可以提供定时和定期的执行方式。

    4.线程池参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、workQueue(阻塞队列)、keepAliveTime(保活时间)、threadFactory(线程工厂,用于生成线程)。

    5.线程池提交任务:有两个方法可以向线程池提交任务,分别是execute()和submit()。

    execute():用于提交不需要返回值的任务,无法判断任务是否被线程执行成功。

    submit():用于提交需要返回值的任务,会返回一个future类型的对象,来判断任务是否执行成功,还可以通过future的get()方法获取返回值,get()方法会阻塞当前线程直到任务完成。

    5.线程池的工作流程:

    (1)有新任务时,判断当前线程数是否超过corePoolSize,如果小于corePoolSize,即使有空闲线程可以执行任务,也会创建一个新的线程用来执行该任务;

    (2)如果超过corePoolSize,就把任务放在workQueue(阻塞队列)中,等待被执行,前提是workQueue是有界队列;

    (3)如果workQueue满了,判断当前线程数是否小于maximumPoolSize,如果小于maximumPoolSize就创建一个线程用来执行任务。

    (4)如果当前线程数大于maximumPoolSize,就会执行线程池的饱和策略。

    6.线程池的饱和策略:(1)默认策略:直接抛出异常;(2)用调用者所在的线程(提交任务的那个线程)执行任务;(3)丢弃阻塞队列中最靠前的任务,执行当前任务;(4)直接丢弃任务。

    7.线程池的状态:

    (1)RUNNING:接收提交的任务。

    (2)SHUTDOWN:不再接收新提交的任务,继续处理阻塞队列中的任务。

    (3)STOP:不再接收新的任务,也不会处理阻塞队列中的任务,并会终止正在执行的任务。

    (4)TIDYING:所有的任务已终止,ctl记录的任务数量为0,会执行钩子函数terminated()。

    (5)TERMINATED:线程池彻底终止。

    8.关闭线程池的方法:ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow()。

    原理:都是循环遍历线程池的工作线程,然后依次调用线程的intercept()方法来中断线程。

    shutdown():将线程池状态设置为SHUTDOWN。

    shutdownNow():将线程池状态设置为STOP。

  • 67、给View设置的透明度的三种方法

    1,java代码实现

text = (TextView) findViewById(R.id.text);
text.getBackground().setAlpha(12);
           

setAlpha()的括号中可以填0–255之间的数字。数字越大,越不透明。

注意点:在5.0以上系统时,有些机型会出现莫名其妙的颜色值不起作用,变成透明了,也就是用此方法会导致其他共用一个资源的布局(例如:@color/white)透明度也跟着改变。

比如text用上述方法设置成透明后,项目中,其他用到text颜色值的控件,都变成透明了。

原因:在布局中多个控件同时使用一个资源的时候,这些控件会共用一个状态,例如ColorState,如果你改变了一个控件的状态,其他的控件都会接收到相同的通知。这时我们可以使用mutate()方法使该控件状态不定,这样不定状态的控件就不会共享自己的状态了。

text.getBackground().mutate().setAlpha(12);
           

2,在xml布局中进行设置

<TextView
        android:id="@ id/text"
        android:text="Hello World!"
        android:background="#FFFFFF"
        android:layout_width="match_parent"
        android:alpha="0.6"
        android:layout_height="100dp" />
           

android:alpha的值为0~1之间的数。数字越大,越不透明。1表示完全不透明,0表示完全透明。

3,在xml布局中通过android:background设置

<TextView
        android:id="@ id/text"
        android:text="Hello World!"
        android:background="#52FFFFFF"
        android:layout_width="match_parent"
        android:layout_height="100dp" />
           

颜色和不透明度 (alpha) 值以十六进制表示法表示。任何一种颜色的值范围都是 0 到 255(00 到 ff)。对于 alpha,00 表示完全透明,ff 表示完全不透明。android:background的值的格式为”#AARRGGBB”。AA即透明度,R、G、B是红绿蓝三色。每一位均为0–F的十六位数。其中透明度的数值越大,越不透明

java代码

//java代码生成的对应表
for (int i = 100; i>=0; i--) {
   double j = (i / 100.0d);
   int alpha = (int) Math.round(255-j * 255);
   String hex = Integer.toHexString(alpha).toUpperCase();
   if (hex.length() == 1) hex = "0" + hex;
   int percent = (int) (j*100);
   System.out.println(String.format("%d%% — %s", percent, hex));
}
           

透明度对照表

透明度 16进制表示

100% 00(全透明)

99% 03

98% 05

97% 07

96% 0A

95% 0D

94% 0F

93% 12

92% 14

91% 17

90% 1A

89% 1C

88% 1E

87% 21

86% 24

85% 26

84% 29

83% 2B

82% 2E

81% 30

80% 33

79% 36

78% 38

77% 3B

76% 3D

75% 40

74% 42

73% 45

72% 47

71% 4A

70% 4D

69% 4F

68% 52

67% 54

66% 57

65% 59

64% 5C

63% 5E

62% 61

61% 63

60% 66

59% 69

58% 6B

57% 6E

56% 70

55% 73

54% 75

53% 78

52% 7A

51% 7D

50% 80

49% 82

48% 85

47% 87

46% 8A

45% 8C

44% 8F

43% 91

42% 94

41% 96

40% 99

39% 9C

38% 9E

37% A1

36% A3

35% A6

34% A8

33% AB

32% AD

31% B0

30% B3

29% B5

28% B8

27% BA

26% BD

25% BF

24% C2

23% C4

22% C7

21% C9

20% CC

19% CF

18% D1

17% D4

16% D6

15% D9

14% DB

13% DE

12% E0

11% E3

10% E6

9% E8

8% EB

7% ED

6% F0

5% F2

4% F5

3% F7

2% FA

1% FC

0% FF(完全不透明)

不透明度对照表

不透明度—十六进制值

100% — FF(完全不透明)

99% — FC

98% — FA

97% — F7

96% — F5

95% — F2

94% — F0

93% — ED

92% — EB

91% — E8

90% — E6

89% — E3

88% — E0

87% — DE

86% — DB

85% — D9

84% — D6

83% — D4

82% — D1

81% — CF

80% — CC

79% — C9

78% — C7

77% — C4

76% — C2

75% — BF

74% — BD

73% — BA

72% — B8

71% — B5

70% — B3

69% — B0

68% — AD

67% — AB

66% — A8

65% — A6

64% — A3

63% — A1

62% — 9E

61% — 9C

60% — 99

59% — 96

58% — 94

57% — 91

56% — 8F

55% — 8C

54% — 8A

53% — 87

52% — 85

51% — 82

50% — 80

49% — 7D

48% — 7A

47% — 78

46% — 75

45% — 73

44% — 70

43% — 6E

42% — 6B

41% — 69

40% — 66

39% — 63

38% — 61

37% — 5E

36% — 5C

35% — 59

34% — 57

33% — 54

32% — 52

31% — 4F

30% — 4D

29% — 4A

28% — 47

27% — 45

26% — 42

25% — 40

24% — 3D

23% — 3B

22% — 38

21% — 36

20% — 33

19% — 30

18% — 2E

17% — 2B

16% — 29

15% — 26

14% — 24

13% — 21

12% — 1F

11% — 1C

10% — 1A

9% — 17

8% — 14

7% — 12

6% — 0F

5% — 0D

4% — 0A

3% — 08

2% — 05

1% — 03

0% — 00(全透明)

继续阅读