IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android复习系列③之《Android筑基》 -> 正文阅读

[移动开发]Android复习系列③之《Android筑基》

1.Android系统架构

在这里插入图片描述

  1. 应用层
  2. 应用框架层(Framwork)
  3. 系统运行库层
  4. Linux内核层

2.四大组件

1. Activity

1.1 生命周期

在这里插入图片描述

1.2. activity四种启动模式

standard:标准模式,每次都会在活动栈中生成一个新的Activity实例。

singleTop:栈顶复用,如果Activity实例已经存在栈顶,就不会在栈中创建新的实例,只会调用该Activity的onNewIntent()方法。
场景:通知消息打开的Activity。因为你肯定不想前台Activity已经是该Activity的情况下,点击通知,又给你再创建一个同样的Activity。

singleTask:栈内复用,如果Activity在当前栈中已经存在,就会将当前Activity上面的其他Activity都移出栈。
场景:常见于主界面。比如主页面的设计一般使用SingleTask模式来设计,因为用户点击多次页面的相互跳转后,在点击回到主页,再次点击退出,这时他的实际需求就是要退出程序,而不是一次一次关闭刚才跳转过的页面,最后才退出。这就需要用到SingleTask模式。

singleInstance:单实例模式,创建一个新的任务栈,这个活动实例独自处在这个活动栈中。
场景:呼叫来电界面。

面试题1:onSaveInstanceState(Bundle outState),onRestoreInstanceState(Bundle savedInstanceState) 的调用时机?

onSaveInstanceState()调用时机:

  1. 按下HOME键时。
  2. 屏幕方向切换时(无论竖屏切横屏还是横屏切竖屏都会调用)。
  3. 关闭屏幕显示。
  4. 从当前activity启动一个新的activity时。
  5. 长按Home键或任务键切换到其它应用时

总结:即当系统“未经你许可”时销毁了你的activity,则onSaveInstanceState会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据

onRestoreInstanceState() 的调用时机:

onRestoreInstanceState(Bundle savedInstanceState)只有在activity确实是被系统回收,重新创建activity的情况下才会被调用

会被调用的例子:
比如横竖屏切换时:
onPause -> onSaveInstanceState -> onStop -> onDestroy -> onCreate -> onStart -> onRestoreInstanceState -> onResume
在这里onRestoreInstanceState被调用,是因为屏幕切换时原来的activity被系统回收了,又重新创建了一个新的activity。

不会被调用的例子:
按HOME键返回桌面,又马上点击应用图标回到原来页面时,activity生命周期如下:
onPause -> onSaveInstanceState -> onStop -> onRestart -> onStart -> onResume
因为activity没有被系统回收,因此onRestoreInstanceState没有被调用

面试题2:onCreate()里也有Bundle参数,可以用来恢复数据,它和onRestoreInstanceState有什么区别?

因为onSaveInstanceState不一定会被调用,所以onCreate()里的Bundle参数可能为空,如果使用onCreate()来恢复数据,一定要做非空判断。而onRestoreInstanceState的Bundle参数一定不为空,因为它只有在上次activity被系统回收(回收时会调用onSaveInstanceState)后重新创建时才会调用。

面试题3:屏幕方向切换时,activity生命周期?
onPause -> onSaveInstanceState -> onStop -> onDestroy -> onCreate -> onStart -> onRestoreInstanceState -> onResume

注意:以下网上常见的说法,是错误

  1. 不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
  2. 设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次
  3. 设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法

从 Android 3.2 (API级别 13)开始

  1. 不设置Activity的android:configChanges,或设置Activity的android:configChanges=“orientation”,或设置Activity的android:configChanges=“orientation|keyboardHidden”,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行一次。
  2. 配置 android:configChanges=“orientation|keyboardHidden|screenSize”,才不会销毁activity,且只调用 onConfigurationChanged方法。

面试题4:Activity A跳转Activity B,再按返回键,生命周期执行的顺序 ?

在A跳转B会执行:A onPause -> B onCreate -> B onStart -> B onResume->A onStop
在B按下返回键会执行:B onPause -> A onRestart -> A onStart -> A onResume-> B onStop -> B onDestroy

可以看到 下一个Activity执行了onResume后,前一个Activity才回执行onStop

面试题5:Activity的onNewIntent()方法什么时候执行?

当ActivityA的LaunchMode为SingleTop时,如果ActivityA在栈顶,再启动ActivityA,这时会调用onNewIntent()方法;
当ActivityA的LaunchMode为SingleInstance,SingleTask时,如果已经ActivityA已经在堆栈中,那么此时再次启动会调用onNewIntent()方法;。

刁钻面试题
①弹出dialog对生命周期的影响?

生命周期回调都是AMS通过Binder通知应用进程调用的;而弹出Dialog、Toast、PopupWindow 本质上都直接是通过 WindowManager.addView 显示的(没有经过 AMS),所以不会对生命周期有任何影响。

②Activity A跳转到主题是透明的Activity B或者主题为DialogActivity的Activity B的生命周期?

A.onPause -> B.onCrete -> B.onStart -> B.onResume
注意前一个 Activity A不会回调 onStop,因为只有在 Activity 切到后台不可见才会回调 onStop;只是失去了焦点所以仅有 onPause 回调。

③Activity之间传递数据的方式Intent是否有大小限制?如果传递的数据量偏大,有哪些方案?

Intent中携带的数据要从APP进程传输到AMS进程,再由AMS进程传输到目标Activity所在进程,通过Binder来实现进程间通信 。

通信过程:

  1. Binder 驱动在内核空间创建一个数据接收缓存区。
  2. 在内核空间开辟一块内核缓存区,建立内核缓存区和内核空间的数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系。
  3. 发送方进程通过系统调用copyfromuser() 将数据 copy 到内核空间的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

当使用Intent来传递数据时,用到了Binder机制,数据就存放在了Binder的事务缓冲区里面,而事务缓冲区是有大小限制的,普通的由Zygote孵化而来的用户进程,映射的Binder内存大小是不到1M的Binder本身就是为了进程间频繁、灵活的通信所设计的, 并不是为了拷贝大量数据

解决方案: (IPC:Inter-Process Communication,进程间通信)

如果非IPC
单例,eventBus,Application,sqlite,sharedpreference,?le 都可以。

如果是IPC

  1. 共享内存性能还不错, 通过MemoryFile开辟内存空间,获得FileDescriptor; 将 FileDescriptor 传递给其他进程; 往共享内存写入数据;从共享内存读取数据。(参考https://www.jianshu.com/p/4a4bc36000fc)
  2. Socket或者管道性能不太好,涉及到至少两次拷贝。

1.3.Activity的显示启动和隐示启动

显示启动:

  1. 类名启动Activity

    Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
    
  2. 构造方法传入Component

    intent.setComponent(new ComponentName(getPackageName(), getPackageName() + ".SecondActivity"));
    
  3. setComponent(componentName):包名加类名

     intent.setClassName(getPackageName(), getPackageName() + ".SecondActivity");
    

隐式启动:

隐式Intent是通过在AndroidManifest文件中设置action、data、category,让系统来筛选出合适的Activity

 <intent-filter>
                <action android:name="android.intent.action.SECONDACTIVITY_START" />
                <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

data ,即数据,主要包含两部分,mimeType和URL(Schheme,Host,Port,Path,PathPattern,pathPrefix)。
intent-?lter中指定了data,intent中就要指定其中的一个data

 <data android:scheme="com.android.demo" android:host="abc" android:port="8080" android:mimeType="image/*"/>
 
 intent1.setDataAndType(Uri.parse("com.android.demo://abc:8080"),"image/png");

1.4.scheme使用场景,协议格式,如何使用?

什么是URL Scheme?
android中的scheme是一种页面内跳转协议,是一种非常好的实现机制,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面;通过scheme协议,服务器可以定制化告诉App跳转那个页面,可以通过通知栏消息定制化跳转页面,可以通过H5页面跳转页面等。

URL Scheme应用场景
客户端应用可以向操作系统注册一个URL scheme,该 scheme 用于从浏览器或其他应用中启动本应用。通过指定的URL字段,可以让应用在被调起后直接打开某些特定页面,比如商品详情页、活动详情页等等。也可以执行某些指定动作,如完成支付等。也可以在应用内通过 html 页来直接调用显示 app 内的某个页面。综上URL Scheme使用场景大致分以下几种:

  1. 客户端根据服务器下发跳转路径跳转相应的页面
  2. H5页面点击锚点,根据锚点的跳转路径,APP端跳转具体的页面
  3. APP端收到服务器端下发的push通知栏消息,根据消息的点击跳转路径跳转相关页面
  4. APP根据URL跳转到另外一个APP指定页面

URL Scheme协议格式
先来个完整的URL Scheme协议格式:

xl://goods:8888/goodsDetail?goodsId=10011002

通过上面的路径 Scheme、Host、port、path、query全部包含,基本上平时使用路径就是这样子的。

  • xl代表该Scheme 协议名称
  • goods代表Scheme作用于哪个地址域
  • 8888代表该路径的端口号
  • goodsDetail代表Scheme指定的页面
  • goodsId代表传递的参数

URL Scheme如何使用

<activity
            android:name=".GoodsDetailActivity"
            android:theme="@style/AppTheme">
            <!--要想在别的App上能成功调起App,必须添加intent过滤器-->
            <intent-filter>
                <!--协议部分,随便设置-->
                <data android:scheme="xl" android:host="goods" android:path="/goodsDetail" android:port="8888"/>
                <!--下面这几行也必须得设置-->
                <category android:name="android.intent.category.DEFAULT"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>
        </activity>

调用方式:
网页上:

<a href="xl://goods:8888/goodsDetail?goodsId=10011002">打开商品详情</a>

原生调用

Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse("xl://goods:8888/goodsDetail?goodsId=10011002"));
startActivity(intent);

获取Scheme跳转的参数获取:

Uri uri = getIntent().getData();
if (uri != null) {
    String url = uri.toString(); // 完整的url信息
    String scheme = uri.getScheme(); // scheme部分
    String host = uri.getHost();// host部分
    String path = uri.getPath();// 访问路劲
    String goodsId = uri.getQueryParameter("goodsId");  //获取指定参数值
}

1.5.activty间传递数据的方式

  1. 通过intent传递数据(Intent.putExtra 的内部也是维护的一个 Bundle)
  2. 通过Application
  3. 使用单例
  4. 静态成员变量。(可以考虑 WeakReferences )
  5. 持久化(sqlite、SharedPreference、file等)

1.6.跨进程启动Activity的方式,注意事项

  1. 隐示跳转,自定义一个action, 系统会去找到这个action所对应的Activity

    Intent intent = new Intent();
    intent.setAction("com.iflytek.note");//这里是采用的自定义action
    startActivity(intent);
    

    待启动APP 的activity在AndroidManifest.xml中的配置
    <!- 需要配置对应的自定义action->

    <activity
       	...
        <intent-filter>
              <action android:name="com.iflytek.note"/>//重点是这里
              <category android:name="android.intent.category.DEFAULT"/>
         </intent-filter>
    </activity>
    

    注意:如果有两个action属性值相同的Activity,那么在启动时手机系统会让你选择启动哪一个Activity,常见于我们打开网页选择浏览器,如果手机装了多种浏览器,系统会弹窗让我们自己选择。

    要解决这个问题,需要给被启动的Activity的intent-filter中再加上一个属性data。

    	<activity
    	   	...
    	    <intent-filter>
    	          <action android:name="com.iflytek.note"/>//重点是这里
    	          <category android:name="android.intent.category.DEFAULT"/>
    	          < data android:scheme="app">
    	     </intent-filter>
    	</activity>
    

    然后再启动该Activity的Intent中加上一个URI,其中“app”必须与data属性的scheme的值一样

    //因为data中仅配置了一个scheme,这里hello可以改为任意字符串
    intent=new Intent("com.iflytek.note", Uri.parse("app://hello"));	
    
  2. 使用ComponentName 指定包名和类名

  3. 共享uid的App
    android中uid用于标识一个应用程序,uid在应用安装时被分配,并且在应用存在于手机上期间,都不会改变。一个应用程序只能有一个uid,多个应用可以使用sharedUserId 方式共享同一个uid,前提是这些应用的签名要相同。 在AndroidManifest中:manifest标签中添加android:sharedUserId=“xxxx”
    启动时:startActivity(new Intent().setComponent(new ComponentName(“XXX”,“XXX”)));

补充知识点:

  1. 如果显式设置exported属性,那么exported的值就是显式设置的值
  2. 如果没有设置exported属性,那么exported属性取决于这个activity是否有intent-filter
    如有intent-filter,那么exported的值就是true
    如没有intent-filter,那么exported的值就是false

也就是说,如果APP要被其它应用调用,你没有intent-filter,那就必须显式地把exported设为true。

1.7.Activity任务栈是什么

  1. android任务栈又称为Task,它是一个栈结构,具有后进先出的特性,用于存放我们的Activity组件。
  2. 我们每次打开一个新的Activity或者退出当前Activity都会在一个称为任务栈的结构中添加或者减少一个Activity组 件,一个任务栈包含了一个activity的集合, 只有在任务栈栈顶的activity才可以跟用户进行交互。
  3. 在我们退出应用程序时,必须把所有的任务栈中所有的activity清除出栈时,任务栈才会被销毁。当然任务栈也可以移动到后台, 并且保留了每一个activity的状态. 可以有序的给用户列出它们的任务, 同时也不会丢失Activity的状态信 息。

2. Service

2.1. Service的生命周期

在这里插入图片描述

2.2. 启动Service的2种方式的区别

  1. startService(): onCreate() -> onStartCommand() -> onDestroy()

    启动:service会一直运行,只有外部调用了stopService()或stopSelf()方法时,该Service才会停止运行并销毁。多次startService不会重复执行onCreate回调,但每次都会执行onStartCommand回调

    销毁:当执行stopService时,直接调用onDestroy方法

    注意:使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。

  2. bindService(): onCreate() -> onbind() -> onUnbind()-> onDestroy()

    启动:如果服务已经开启,多次执行bindService时,不会重复调用onCreate和onBind方法

    销毁:调用unBindService方法(或者调用者activity finish掉了),Service就会调用onUnbind->onDestroy

    注意:使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止。

2.3. 前台Service

Service几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是Service的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的Service。如果你希望Service可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台Service。

```
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
```

```java
Notification notification = new Notification(icon, text, System.currentTimeMillis());
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(this, title, mmessage, pendingIntent);
startForeground(ONGOING_NOTIFICATION_ID, notification);
```

2.4.Service与Activity怎么实现通信

很多场景下,Activity是需要和Service进行交互的,比如音乐播放界面,用户可以根据播放进度条掌握播放的进度,用户也可以自己根据歌词的进度选择调整整首歌的进度。

1. 通过Binder对象

  • Service中定义一个继承Binder的内部类,并添加自定义的逻辑方法。
  • Service中重写Service的onBind方法,返回我们自定义的内部类实例
  • Activity中定义一个ServiceConnection对象,绑定Service时传入此对象,在该对象的onServiceConnected中返回的IBinder,就是Service中我们定义的的Binder,可以直接调用Service里面的方法了。

代码示例:
①Service代码段

public class MyService extends Service {

    private DownloadBinder mBinder = new DownloadBinder();

    class DownloadBinder extends Binder { //定义一个继承Binder的内部类
        public void startDownload() { //在服务中定义一个业务方法,activity中调用此方法
       		//业务逻辑
        }
 
        public int getProgress() { //在服务中定义一个业务方法,activity中调用此方法
       		//业务逻辑
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;//与普通Service不同之处,onBind()不再打酱油,而是返回我们定义的内部类实例
    }

②Activity代码段

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
 
    private MyService.DownloadBinder downloadBinder;
    
 	//可交互的后台服务与普通服务的不同之处,就在于这个connection建立起了两者的联系
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
 
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
        	//在这里实现对服务的方法的调用
            downloadBinder = (MyService.DownloadBinder) service;
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }
    };
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bind_service:
                Intent bindIntent = new Intent(this, MyService.class);
                // 绑定服务和活动,之后活动就可以去调服务的方法了
                bindService(bindIntent, connection, BIND_AUTO_CREATE); 
                break;
            case R.id.unbind_service:
                unbindService(connection); // 解绑服务,服务要记得解绑,不要造成内存泄漏
                break;
        }
    }
}

2.Service通过BroadCast广播与Activity通信

2.5.IntentService是什么,IntentService原理,应用场景及其与Service的区别

首先,如果直接把耗时操作放在 Service 的 onStartCommand() 中,很容易引起 ANR .如果有耗时操作就必须开启一个单独的线程来处理。

概念:

IntentService 是Service 的子类,默认开启了一个工作线程HandlerThread来处理耗时操作。启动 IntentService 的方式和启动传统Service 一样,同时,当任务执行完后,IntentService 会自动停止,而不需要我们去手动控制。另外,可以启动 IntentService 多次,而每一个耗时操作会以工作队列的方式在IntentService 的 onHandleIntent 回调方法中执行,并且,每次只会执行一个工作线程,执行完第一个再执行第二个,以此类推。

IntentService的优点:
既然IntentService是在Service里开启线程去做任务处理,那我直接在Service里启动线程去做不就好了吗?当然可以,但是IntentService已经帮您封装好了,为什么还要自己再去实现IntentService的一套逻辑呢?IntentService会在任务执行完成后自行结束自己,而不需要外部去调用stopService了。简单说:

  1. 省去了在 Service 中手动开线程的麻烦
  2. 操作完成时,我们不用手动停止 Service

2.6 bindService和startService混合使用的生命周期以及怎么关闭

如果你只是想要启动一个后台服务长期进行某项任务,那么使用startService便可以了。如果你还想要与正在运行的 Service取得联系,那么有两种方法:一种是使用broadcast,另一种是使用bindService。

启动时生命周期:

  1. 如果先startService,再bindService
    onCreate() -> onbind() -> onStartCommand()
  2. 如果先bindService,再startService
    onCreate() -> onStartCommand() -> onbind()

关闭:

  1. 如果只stopService:
    Service的OnDestroy()方法不会立即执行,在Activity退出的时候,会执行OnDestroy。

  2. 如果只unbindService
    只有onUnbind方法会执行,onDestory不会执行

  3. 如果要完全退出Service,那么就得执行unbindService()以及stopService。

2.7 Service其它常见面试题

1 Service 的 onStartCommand 方法有几种返回值?各代表什么意思?

有四种返回值:

START_STICKY:如果 service 进程被 kill 掉,保留 service 的状态为开始状态,但不保留递送的 intent 对象。随后系统会尝试重新创建 service,由于服务状态为开始状态,所以创建服务后一定会调用 onStartCommand()方法。如果在此期间没有任何启动命令被传递到 service,那么参数 Intent 将为 null。

START_NOT_STICKY:“非粘性的”。如果在执行完 onStartCommand 后,服务被异常 kill 掉,系统不会自动重启该服务。

START_REDELIVER_INTENT:重传 Intent。使用这个返回值时,如果在执行完 onStartCommand 后,服务被异 常 kill 掉,系统会自动重启该服务,并将 Intent 的值传入。

START_STICKY_COMPATIBILITY: START_STICKY 的兼容版本,但不保证服务被 kill 后一定能重启。

2.如何提高service的优先级? (能说几个就够了)

  1. 在AndroidManifest.xml文件中对于intent-filter可以通过android:priority =“1000”这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时适用于广播。
  2. 在onStartCommand里面调用startForeground()方法把Service提升为前台进程级别(前台服务会在状态栏显示一个通知,最典型的应用就是音乐播放器)
  3. onStartCommand方法,手动返回START_STICKY
  4. Application加上Persistent属性。

3.Service 的 onRebind(Intent)方法在什么情况下会执行?

如果在 onUnbind()方法返回 true 的情况下会执行,否则不执行。

4.Service里面可以弹Toast么?

可以的。弹吐司有个条件就是得有一个Context上下文,而Service本身就是Context的子类,因此在Service里面弹Toast是完全可以的。比如我们在Service中完成下载任务后可以弹一个Toast通知用户。

5.Service和Thread的区别

  1. Service是安卓中系统的组件,它运行在独立进程的主线程中,不可以执行耗时操作。Thread是程序执行的最小单元,分配CPU的基本单位,可以开启子线程执行耗时操作。

  2. Service在不同Activity中可以获取自身实例,可以方便的对Service进行操作。Thread在不同的Activity中难以获取自身实例,如果Activity被销毁,Thread实例就很难再获取得到。

3. BroadcastReceiver

3.1 广播的分类

  1. 按照发送的方式分类

    标准广播
    广播发出后,所有的广播接收者几乎同时收到,这种广播是没法被截断的。

    有序广播
    广播发后,同一时刻只有一个广播接收器可以收到消息, 按照先后顺序进行接收。发送方式变为: sendOrderedBroadcast(intent),拦截广播abortBroadcast();

    注意:广播接受者接收广播的顺序规则:(同时面向静态和动态注册的广播接受者):按照 Priority 属性值从大到小排序, Priority属性相同者,动态注册的广播优先。

  2. 按照注册的方式分类

    静态注册广播
    在AndroidManifest中注册,如果我们需要在程序还没启动时就可以接收到广播,就需要静态注册。

     <receiver
              <intent-filter>
                  <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
              </intent-filter>
      </receiver>
    

    动态注册广播
    动态注册必须在运行时才能进行,有一定的局限性。

    //继承一个BroadcastReceiver,实现onReceive方法
    public class MyBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
            abortBroadcast();
        }
    }
    
    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
        	//动态注册广播
            IntentFilter intentFilter= new IntentFilter();
            intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");//添加action
            MyBroadcastReceiver myBroadcastReceiver= new MyBroadcastReceiver();
            registerReceiver(myBroadcastReceiver, intentFilter);
            
            //发送广播
            button.setOnClickListener{
                  Intent intent = new Intent("android.net.conn.CONNECTIVITY_CHANGE");//指定action
                  sendBroadcast(intent); 
            };
        }
    }
    
  3. 按照定义的方式分类

    系统广播
    Android系统中内置了多个系统广播,每个系统广播都具有特定的intent-filter,其中主要包括具体的action,系统广播发出后,将被相应的BroadcastReceiver接收。

    自定义广播
    由开发者自己定义的广播

3.2 静态注册广播与动态注册广播的区别

静态注册:常驻系统,即使App退出,仍能接收到广播,耗电,占内存。
动态注册:非常驻,跟随组件的生命变化,组件结束,广播结束。在组件结束前,需要先移除广播,否则容易造成内存泄漏。

注意:Android 3.1开始系统在Intent与广播相关的flag增加了参数:
FLAG_INCLUDE_STOPPED_PACKAGES:包含已经停止的包(停止指的是即包所在的进程已经退出)
FLAG_EXCLUDE_STOPPED_PACKAGES:不包含已经停止的包
自Android3.1开始,系统默认直接增加了值为FLAG_EXCLUDE_STOPPED_PACKAGES的flag,导致即使是静态注册的广播接收器,对于其所在进程已经退出的App,同样无法接收到广播。

但是对于自定义的广播,可以通过覆写此flag为FLAG_INCLUDE_STOPPED_PACKAGES,使得静态注册的BroadcastReceiver,即使所在App进程已经退出,也能接收到广播,并会启动应用进程。
代码实现为:

Intent intent = new Intent();
intent.setAction(BROADCAST_ACTION);
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
sendBroadcast(intent);

3.3 广播的安全性问题

Android中的广播可以跨进程甚至跨App直接通信,且exported属性在有intent-filter的情况下默认值是true,由此将可能出现的安全隐患如下:

  • 其他App可针对性的发出与当前App intent-filter相匹配的广播,由此导致当前App不断接收到广播并处理
  • 其他App可以注册与当前App一致的intent-filter用于接收广播,获取广播具体信息。

常见的一些增加安全性的方案包括:

  1. 同一App内部发送和接收广播,将exported设置为false,使非本App内部发出的此广播不被接收;
  2. 在广播发送和接收时,都增加上相应的permission,用于权限验证;
  3. 发送广播时,指定特定广播接收器所在的包名,具体是通过intent.setPackage(packageName)指定,这样此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。
  4. 采用LocalBroadcastManager的方式

3.4 LocalBroadcastManager

为了解决安全性问题,Android在android.support.v4.content包中引入了LocalBroadcastManager

使用该机制发出的广播只能够在应用程序内部进行传递并且广播接收器也只能接收来自本地应用程序发出的广播,这样所有的安全性问题都不存在了。

public class MainActivity extends AppCompatActivity {
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        localBroadcastManager = LocalBroadcastManager.getInstance(this); //1.创建LocalBroadcastManager 实例
        
        IntentFilter intentFilter= new IntentFilter();
        intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
        localReceiver = new LocalReceiver();
        localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 2.通过LocalBroadcastManager注册本地广播监听器
        button.setOnClickListener {
                Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
                localBroadcastManager.sendBroadcast(intent); // 3.通过LocalBroadcastManager 发送本地广播
        };
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        localBroadcastManager.unregisterReceiver(localReceiver);//4.通过LocalBroadcastManager注销广播
    }

    class LocalReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
        }
    }
}

3.5 BroadcastReceiver 与 LocalBroadcastReceiver 有什么区别?

  • BroadcastReceiver 是跨应用广播利用Binder机制实现支持动态和静态两种方式注册方式
  • LocalBroadcastReceiver 是应用内广播,利用Handler实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册

4. ContentProvider

4.1 ContentProvider的作用和原理

作用:为不同的应用之间数据共享,提供统一的接口。
原理:把provider启动起来并记录和发布给AMS,AMS作为一个中间管理员的身份,所有的provider会向它注册 。向AMS请求到provider之后,就可以在client和server之间自行binder通信,不需要再经过systemserver。

4.2.ContentProvider,ContentResolver,ContentObserver之间的关系

  • ContentProvider:管理数据,提供数据的增删改查操作,数据源可以是数据库、文件、XML、网络等。
  • ContentResolver:外部进程可以通过 ContentResolver 与 ContentProvider 进行交互。其他应用中的ContentResolver 可以不同 URI 操作不同的 ContentProvider 中的数据。
  • ContentObserver:观察 ContentProvider 中的数据变化,并将变化通知给外界。

4.3 ContentProvider的优点

  1. 封装
    其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单且高效。如一开始数据存储方式采用 SQLite 数据库,后来把数据库换成Room,也不会对上层数据ContentProvider使用代码产生影响

  2. 提供一种跨进程数据通信的方式
    应用程序间的数据共享还有另外的一个重要话题,就是数据更新通知机制了。因为数据是在多个应用程序中共享的, 当其中一个应用程序改变了这些共享数据的时候,它有责任通知其它应用程序,让它们知道共享数据被修改了,这样它们就可以作相应的处理。

4.4ContentProvider 和 sql 在实现上有什么区别?

  1. ContentProvider 屏蔽了数据存储的细节,内部实现透明化,用户只需关心 uri 即可(是否匹配)
  2. ContentProvider 能实现不同 app 的数据共享,sql 只能是自己程序才能访问
  3. Contentprovider 还能增删本地的文件,xml等信息

4.5 Uri是什么

其它应用可以通过ContentResolver来访问ContentProvider提供的数据,而ContentResolver通过Uri来定位自己要访问的数据

定义:Uniform Resource Identi?er,即统一资源标识符
作用:每一个 ContentProvider都拥有一个公共的URI ,这个URI 用于表示这个ContentProvider所提供的数据。

Uri一共分为4个部分:
在这里插入图片描述

  • 主题(schema):标准前缀,;“content://”;
  • 授权信息(authority):URI 的标识,用于唯一标识这个 ContentProvider ,外部调用者可以根据这个标识来找到它。
  • 表名(path):路径,通俗的讲就是你要操作的数据库中表的名字
  • 记录(ID):如果URI中包含表示需要获取的记录的 ID;则就返回该id对应的数据,如果没有 ID,就表示返回全部

3.屏幕适配

1.相关重要概念

屏幕尺寸
含义:手机对角线的物理尺寸
单位:英寸(inch),1英寸=2.54cm
Android手机常见的尺寸有5寸、5.5寸、5.8寸、6寸、6.2寸等等

屏幕分辨率
含义:屏幕像素点数总和
例子:1080x1920,即宽度方向上有1080个像素点,在高度方向上有1920个像素点
单位:px(pixel),1px=1像素点(UI设计师的设计图会以px作为统一的计量单位)

Android手机常见的分辨率:320x480、480x800、720x1280、1080x1920

屏幕像素密度
含义:每英寸的像素点数
单位:dpi(dots per ich)

假设设备内每英寸有160个像素,那么该设备的屏幕像素密度=160dpi

安卓手机对于每类手机屏幕大小都有一个相应的屏幕像素密度:
在这里插入图片描述
屏幕尺寸、屏幕分辨率、屏幕像素密度三者关系
在这里插入图片描述
密度无关像素
含义:density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关
单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果

Android开发时用dp而不是px单位设置图片大小,是Android特有的单位
场景:假如同样都是画一条长度是屏幕一半的线,如果使用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。

dp与px的转换
因为ui设计师给你的设计图是以px为单位的,Android开发则是使用dp作为单位的,那么我们需要进行转换:
在这里插入图片描述
在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

独立比例像素
含义:scale-independent pixel,叫sp或sip
单位:sp

Android开发时用此单位设置文字大小,可根据字体大小首选项进行缩放
推荐使用12sp、14sp、18sp、22sp作为字体设置的大小,不推荐使用奇数和小数,容易造成精度的丢失问题;小于12sp的字体会太小导致用户看不清

2.屏幕适配:目前最好的适配方案

1.android最原始的适配方案

dp + (wrap_content、match_parent、layout_weight),所有的适配方案都不是用来取代match_parent、wrap_content、layout_weight的,而是用来完善他们的。

缺点(随便过一眼即可):

  1. 只能保证适配大部分手机,部分手机仍然需要单独适配;为什么dp只解决了90%的适配问题,因为并不是所有的1080P的手机dpi都是480(因为分辨率相同,屏幕尺寸可能相差很多),比如Google的Pixel2(19201080)的dpi是420,也就是说,在Pixel2中,1dp=2.625px,这样会导致相同分辨率的手机中,这样 一个100dp100dp的控件,在一般的1080P手机上,可能都是300px,而 Pixel 2 中,就只有262.5px,这样控件的实际大小会有所不同。

  2. 这种方式无法快速高效的把设计师的设计稿实现到布局代码中,通过dp直接适配,我们只能让UI基本适配不同的手机,但是在设计图和UI代码之间的鸿沟,dp是无法解决的,因为dp不是真实像素。而且,设计稿的宽高往往和Android的手机真实宽高差别极大,以我们的设计稿为例,设计稿的宽高是375px750px,而真实手机可能普遍是10801920;那 么在日常开发中我们是怎么跨过这个鸿沟的呢?基本都是通过百分比啊, 或者通过估算,或者设定一个规范值等等。总之,当我们拿到设计稿的时候,设计稿的ImageView是128px128px,当我们在编写layout文件的时 候,却不能直接写成128dp128dp。在把设计稿向UI代码转换的过程中,我们需要耗费相当的精力去转换尺寸,这会极大的降低我们的生产 力,拉低开发效率。

2.SmallestWidth适配(sw限定符适配)

实现原理:Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。
?
sw限定符适配宽高限定符适配类似,区别在于,前者有很好的容错机制,如果没有value-sw360dp文件夹,系统会向下寻找,比如离360dp最近的只有value-sw350dp,那么Android就会选择value-sw350dp文件夹下面的资源文件。这个特性就完美的解决了宽高限定符的容错问题。
??
优点:1.非常稳定,极低概率出现意外
?? 2.不会有任何性能的损耗
?? 3.适配范围可自由控制,不会影响其他三方库
??
缺点:就是多个dimens文件可能导致apk变大,几百k(影响很小)。

附件:[生成sw文件的工具](https://github.com/ladingwu/dimens_sw)

3.今日头条适配方案

实现原理修改系统的density值(核心)

今日头条适配是以设计图的宽或高进行适配的,适配最终是改变系统density实现的。

在这里插入图片描述
优点:使用成本低,侵入性低,修改一次项目所有地方都会适配,无性能损耗
缺点:
1.只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的。
2.这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图完全一样。

4. AutoSize

AndroidAutoSize 是基于今日头条适配方案,该开源库已经很大程度上解决了今日头条适配方案的两个缺点,可以对activity,fragment进行取消适配。也是目前我的项目中所使用的适配方案。

使用也非常简单只需两步:

(1)引入:

```java
 implementation 'me.jessyan:autosize:1.1.2'
```

(2)在 AndroidManifest 中填写全局设计图尺寸 (单位 dp),如果使用副单位,则可以直接填写像素尺寸,不需要再将像素转化为 dp,详情请查看 demo-subunits

```java
<manifest>
    <application>            
        <meta-data
            android:name="design_width_in_dp"
            android:value="360"/>
        <meta-data
            android:name="design_height_in_dp"
            android:value="640"/>           
     </application>           
</manifest>
```

4.handler(面试八股文之一)

1.子线程通知更新UI的方式

1、调用activity的runOnUIThread():到主线程更新

// 因为runOnUiThread是Activity中的方法,Context是它的父类,所以要转换成Activity对象才能使用
((Activity) context).runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // 更新ui
    }
});

2、Handle.post(Runnable runnanle):
思考:这个为啥是在主线程?

new Handler(mContext.getMainLooper()).post(new Runnable() {
    @Override
        public void run() {
          // 更新ui
        }
});

3、Handler发送Message:handler在主线程创建的,handleMessage也在主线程

private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        case XXX:
       		 XXX XXX= msg.obj;
             break;
        }
    }
};
	Message msg = Message.obtain();
	msg.what = XXX;
	msg.obj = XXX;
	mHandler.sendMessage(msg);

Handler推送Message和Runnable的区别

其实,Message里面有一个叫callback的Runnable属性,无论你send一个Message,还是post一个Runnnable,最终都是send一个Message,Runnable最终也是转化为一个Message。

2.handler原理(非常重要)

消息机制指Handler、Looper、MessageQueue、Message之间如何工作的。

handler负责处理和发送消息。在handler的构造函数中,会绑定其中的looper和MessageQueue。handler依赖于looper,looper依赖于MessageQueue,所以在子线程中使用handler抛出异常是因为子线程中没有初始化looper对象(子线程中使用handler需要手动调用Looper.prepare()),而主线程中looper是在ActivityThread中已经初始化过了,所以能直接在主线程中能拿到Handler。

Looper是用来轮询消息,通过Looper.loop()方法实现死循环,有消息的时候,通过MessageQueue.next方法取出message,没有消息的时候,线程处于阻塞的状态。在有消息的时候获取到消息,将消息交给了handler,handler会根据消息中有没有callback,如果有callback会直接callback,否则通过handleMessage处理。

MessageQueue是一个单链表结构来存储Message,每次通过next方法取出Message消息后,取完之后将message.next给当前的message,再将message.next=null,实际上就是移除当前的message(但是在looper里面每次在next取出message后,放到了message的sPool里面,缓存起来方便使用。)拿到message就调用handler的dispatchMessage,dispatchMessage被成功调 用,接着调用handlerMessage()。

Message就没什么好说的,主要存储平常经常用的obj和what信息,以及我们不用关心的target和callback等。

3.handler之各种面试题

3.1 子线程能不能直接new一个handler?为什么主线程可以?主线程的Looper第一次调用loop方法是在什么时候,在哪个类?

不能。因为Handler 的构造方法中,会通过Looper.myLooper()获取looper对象,如果为空,则抛出异常。
主线程则因为已在入口处ActivityThread的main方法中通过 Looper.prepareMainLooper()获取到这个对象, 并通过 Looper.loop()开启循环,在子线程中若要使用handler,可先通过Loop.prepare获取到looper对象,并使用 Looper.loop()开启循环。

3.2. 一个线程会有几个Looper,几个Handler,几个MessageQueue对象?以及Looper会存在线程哪里?

一个线程可以有多个Handler,只有一个Looper对象,只有一个MessageQueue对象。
Looper.prepare()函数中知道,在Looper的prepare方法中创建了Looper对象,并放入到ThreadLocal中,并通过ThreadLocal来获取looper 的对象,ThreadLocal的内部维护了一个ThreadLocalMap,ThreadLocalMap是以当前thread做为key的,因此可以得 知,一个线程多只能有一个Looper对象, 在Looper的构造方法中创建了MessageQueue对象,并赋值给mQueue 字段。因为Looper对象只有一个,那么Messagequeue对象肯定只有一个。

Looper会存在线程的ThreadLocal对象里,该对象是线程的缓存区。

3.3. Handler导致的内存泄露原因及其解决方案

原因:

  1. Java中非静态内部类和匿名内部类都会隐式持有当前类的外部引用
  2. 我们在Activity中使用非静态内部类初始化了一个Handler,此Handler就会持有当前Activity的引用。
  3. 我们想要一个对象被回收,那么前提它不被任何其它对象持有引用,所以当我们Activity页面关闭之后,存在引用关系:“未被处理 / 正处理的消息 -> Handler实例 -> 外部类”。如果在Handler消息队列 还有未处理的消息 / 正在处理消 息时 导致Activity不会被回收,从而造成内存泄漏。

解决方案:

  1. 将Handler的子类设置成 静态内部类,使用 WeakReference弱引用持有Activity实例
  2. 当外部类结束生命周期时,清空Handler内消息队列

3.4. 创建 Message 的几种方式及区别?

有三种:

  1. Message msg = new Message();
  2. Message msg = Message.obtain(); // Message 的静态方法
  3. Message msg = handler.obtainMessage(); //Handler 的公有方法

obtainMessage()其内部也是调用的obtain()方法

3.5. handler 有哪些发送消息的方法

  1. sendMessage(Message msg)
  2. sendMessageDelayed(Message msg, longuptimeMillis)
  3. post(Runnable r)
  4. postDelayed(Runnable r, long uptimeMillis)
  5. sendMessageAtTime(Message msg,long when)

3.6. handler的post与sendMessage的区别和应用场景

sendMesage用法:

private final Handler mHandler = new Handler();
...
mHandler.sendEmptyMessage(0);

sendMessage源码:

public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
}

  // sendMessage 最终调用到 sendMessageAtTime
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
 }

post用法:

private final Handler mHandler = new Handler();
...
mHandler.post(new Runnable() {
            @Override
            public void run() {
            }
});

post源码:

public final boolean post(@NonNull Runnable r) {
		// 在post方法中message是通过getPostMessage(Runnable r)这个方法获取的
       return  sendMessageDelayed(getPostMessage(r), 0); 
 }

private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;	// 注释1: 给message的callback赋值为runnable对象
        return m;
    }
 // 可以看到 post 最终也是调用到 sendMessageAtTime
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

可以看到,post和sendMessage本质上是没有区别的,都是发送一个消息到消息队列中,最终都是执行到sendMessageAtTime,只不过post使用方式更简单。

然后看消息出队列的源码:

/**
 * Handle system messages here.
 */
public void dispatchMessage(@NonNull Message msg) {
 		//从注释1可以知道,如果是post,则callback不为空,直接进入handleCallback
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
         // 如果是sendMessage,且创建handler时没有传入callback,则callback为空,
         // 进入到我们自己复写的处理消息的handleMessage()方法
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
  }

看下handleCallback()方法:

 // 直接run不会启动新线程,所以这就是post的runnable里面可以直接更新UI的原因
 private static void handleCallback(Message message) {
        message.callback.run(); // 即普通方法run, 非start()
 }

总结区别为:

调用post方法的消息是在post传递的Runnable对象的run方法中处理。

调用sendMessage方法需要重写handleMessage方法处理,或者给handler设置callback,在callback的 handleMessage中处理并返回true

给handler设置callBack代码如下:

private final Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            return true; // 在这里返回true处理message
        }
  });

应用场景

  • post一般用于单个场景,比如单一的倒计时弹框功能
  • sendMessage的回调需要去实现handleMessage,Message则做为参数,用于多判断条件的场景。

3.7. handler postDealy后消息队列有什么变化,假设先 postDelay 10s, 再 postDelay 1s, 怎么处理这2条消息?

postDelayed传入的时间,会和当前的时间SystemClock.uptimeMillis()做加和,而不是单纯的只是用延时时间。延时消息会和当前消息队列里的消息头的执行时间做对比,如果比头的时间靠前,则会做为新的消息头,不然则会从消息头开始向后遍历,找到合适的位置插入延时消息。 postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞; 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前 面),然后调用nativeWake()方法唤醒线程; MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一 下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞; 直到阻塞时间到或者下一次有Message进队。

3.8.MessageQueue是什么数据结构

内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表 (链表在插入和删除方面的性能好)
这点和传统的队列有点不一样,主要区别在于Android的这个队列中的消息是按照时间先后顺序来存储的,时间较早的消息,越靠近队头。 当然,我们也可以理解成,它是先进先出的,只是这里的先依据的不是谁先入队,而是消息待发送的时间。

3.9.子线程能不能更新UI?

极端情况是可以的。

  1. 更新UI,都会调用到ViewRootImpl。
  2. Android每次刷新UI的时候,都会调用根布局**ViewRootImpl.checkThread()**来检查线程是否是主线程。如果不是主线程,那就会抛出异常。
  3. ViewRootImpl的创建在onResume方法回调之后,这个时候无法在在子线程中访问UI了。

也就是说如果我们在onResume之前就在子线程更新UI,明面上是不会报错的。

可以参考详细测试:Android中子线程真的不能更新UI吗?

3.10.为什么Android系统不建议子线程访问UI ?

在android中子线程可以有好多个,但是如果每个线程都可以对UI进行访问,我们的界面可能就会变得混乱不堪,这样多个线程操作同一资源就会造成线程安全问题,当然,需要解决线程安全问题的时候,我们第一想到的可能就是加锁,但是加锁会降低运行效率,所以android出于性能的考虑,并没有使用加锁来进行UI操作的控制.

3.11.为什么主线程不会因为Looper.loop()里的死循环卡死?

App进程中是需要死循环的,如果循环结束的话,App进程就结束了。

首先,Looper.loop()的阻塞和UI线程上执行耗时操作导致卡死是完全的两回事。

  • Looper上的阻塞,前提是没有输入事件,MessageQueue 为空(有可能有延时的消息),Looper空闲状态,线程进入阻塞,释放CPU执行权,等待唤醒。
  • UI耗时导致卡死,前提是要有输入事件,MessageQueue 不为空,Looper正常轮询,线程并没有阻塞,但是该事件执行时间过长,然后就ANR异常了。

详细可以阅读:《Android中为什么主线程不会因为Looper.loop()里的死循环卡死?》

3.12. 一个looper是如何区分多个Handler的?当 Activity有多个handler的时候,怎么样区分当前消息由哪个Handler处理?

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
		//会把this赋值给msg.target,此时target就指向当前Handler
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
		//之后调用MessageQueue的enqueueMessage分发消息进行处理
        return queue.enqueueMessage(msg, uptimeMillis);
    }

message有多个属性,常用的有what,arg1,arg2,data等,其实还有一个属性叫做target,这个target属性就是标识handler的。每个Handler会被添加到Message的target字段上面,Looper通过调用 Message.target.handleMessage() 来让Handler 处理消息。

3.13.Looper.quit/quitSafely的区别

当我们调用Looper的quit方法时,实际上执行了MessageQueue中的removeAllMessagesLocked方法,该方法的作用是把MessageQueue消息池中所有的消息全部清空,无论是延迟消息(延迟消息是指通过sendMessageDelayed 或通过postDelayed等方法发送的需要延迟执行的消息)还是非延迟消息。 当我们调用Looper的quitSafely方法时,实际上执行了MessageQueue中的removeAllFutureMessagesLocked方 法,通过名字就可以看出,该方法只会清空MessageQueue消息池中所有的延迟消息,并将消息池中所有的非延迟 消息派发出去让Handler去处理,quitSafely相比于quit方法安全之处在于清空消息之前会派发所有的非延迟消息。

3.14.Handler是怎么完成线程切换的?(重要)

首先我想说:问题本身是有问题的。。网上各种长篇大论分析源码的都没说到要点,一会儿扯什么ThreadLocal,一会儿又是 IPC 。在主线程创建了Handler,然后在子线程直接就可以拿主线程的handler实例调用sendMessage方法了。这特么难道还没有跨线程吗?服务端开发里面的消息队列本身也是这个原理,队列对所有线程都是可见的,大家都可以往里面 enqueue 消息。

要明白的重点:
线程间的数据是共享的:同一进程中线程和线程之间资源是共享的,也就是对于任何变量在任何线程都是可以访问和修改的(这就是为什么你在主线程创建的Handler,可以在子线程直接使用)。handler机制中所谓的线程切换:其实就是借助共享变量来实现的.

非要强行回答这个问题,可以从调用栈的角度入手。

先思考一个问题:既然线程间数据共享,那我们能不能通过接口的形式,在子线程通知主线程数据改变了?测试下看看:

一个简单的接口:

public interface MyInterface {
    void doSomeThing(String msg);
}

Activity中实现这个接口并复写方法。创建接口实例myInterface ,在子线程中调用myInterface 的doSomeThing方法。看看doSomeThing方法执行在哪个线程。

public class MainActivity extends AppCompatActivity implements MyInterface {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e("doSomeThing", "当前线程" + Thread.currentThread().getName());
        Log.e("doSomeThing", "------------");
        MyInterface myInterface = new MainActivity();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.e("doSomeThing", "当前线程:" +Thread.currentThread().getName());
                Log.e("doSomeThing", "在子线程获取数据");
                Log.e("doSomeThing", "数据获取中,请稍后...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.e("doSomeThing", "数据获取完毕");
                Log.e("doSomeThing", "------------");
                myInterface.doSomeThing("子线程获取到的数据");
            }
        }).start();
    }


    @Override
    public void doSomeThing(String msg) {
        Log.e("doSomeThing", "当前线程:" +Thread.currentThread().getName());
        Log.e("doSomeThing", msg);
    }
}
2021-02-23 11:14:55.070 19027-19027/com.example.zjhdemo E/doSomeThing: 当前线程main
2021-02-23 11:14:55.070 19027-19027/com.example.zjhdemo E/doSomeThing: ------------
2021-02-23 11:14:55.070 19027-19090/com.example.zjhdemo E/doSomeThing: 当前线程:Thread-2
2021-02-23 11:14:55.070 19027-19090/com.example.zjhdemo E/doSomeThing: 在子线程获取数据
2021-02-23 11:14:55.070 19027-19090/com.example.zjhdemo E/doSomeThing: 数据获取中,请稍后...
2021-02-23 11:14:56.071 19027-19090/com.example.zjhdemo E/doSomeThing: 数据获取完毕
2021-02-23 11:14:56.071 19027-19090/com.example.zjhdemo E/doSomeThing: ------------
2021-02-23 11:14:56.071 19027-19090/com.example.zjhdemo E/doSomeThing: 当前线程:Thread-2
2021-02-23 11:14:56.071 19027-19090/com.example.zjhdemo E/doSomeThing: 子线程获取到的数据

可以看到子线程中调用接口方法,回调方法依旧执行在子线程。可以理解为我们开启子线程后,run()方法中的内容包括接口回调都会运行在子线程

既然说线程间的数据是共享的,那我们试试另一种方式:

public class MainActivity extends AppCompatActivity {
	//共享变量
    private boolean a = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //假设a = true 是在子线程获取的数据
                Log.e("doSomeThing", "创建的thread的run方法所在线程:" + Thread.currentThread().getName() + "------" + "a=" + a);
                a = true;
            }
        });
        thread.start();
        try {
            //等待子线程执行完  再执行主线程
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //假设这里获取到数据后更新UI
        Log.e("doSomeThing", "更新UI所在线程:" + Thread.currentThread().getName() + "------" + "a=" + a);
    }
}

结果:

E/doSomeThing: 创建的thread的run方法所在线程:Thread-2------a=false
E/doSomeThing: 更新UI所在线程:main------a=true

说明确实是这行的。但是我们这种方式显然太过暴力,而且没有考虑多线程并发的问题,同时还会阻塞主线程。

接下来看看Handler的调用栈

Thread.foo(){
	Looper.loop()
	 -> MessageQueue.next()
 	  -> Message.target.dispatchMessage()
 	   -> Handler.handleMessage()
}

参考上面第一个例子,我们知道Handler.handleMessage() 所在的线程最终由调用 Looper.loop() 的线程所决定。所以在主线程创建的handler,其handleMessage方法最终也执行在主线程。

理解:

当在A线程中创建handler的时候,同时创建了MessageQueue与Looper,Looper在A线程中调用loop进入一个无限的for循环从MessageQueue中取消息,当B线程调用handler发送一个message的时候,会通过 msg.target.dispatchMessage(msg);将message插入到handler对应的MessageQueue中,Looper发现有message 插入到MessageQueue中,便取出message执行相应的逻辑,因为Looper.loop()是在A线程中启动的,所以则回到 了A线程,达到了从B线程切换到A线程的目的

总结:可以理解为handler实现了跨线程或者线程切换,但是实际上线程本身资源是共享的。Handler帮助我们实现了线程通信,同时解决了多线程并发等等问题。你可以说是handler实现的线程切换,没毛病,但是本质是上,handler也是利用了线程资源共享的原理。

3.15.Handler 如何与 Looper 关联的

通过构造方法 mLooper = Looper.myLooper()—>sThreadLocal.get()

实际Looper和Handler就是通过ThreadLoacl把两者联系在了一起,总结下:

Looper.prepare()生成了一个Looper对象(并且在生成Looper对象的同时生成了一个与之对应的MessageQueue对象,赋值给了Looper的mQueue成员变量)然后把这个Looper对象存入ThreadLocal中去。 在使用Handler的时候,在其构造函数中,先通过Looper的myLooper方法获取到当前线程对象的值。然后赋值给Handler的mLooper的这个成员变量,然后把Looper的mQueue成员变量的值赋值给Handler的mQueue成员变量。

3.16.Looper 如何与 Thread 关联的

Looper与Thread之间是通过 ThreadLocal 关联的,这个可以看 Looper.prepare() 方法,Looper 中有一个 ThreadLocal 类型的 sThreadLocal 静态字段,Looper通过它的 get 和 set 方法来赋值和取值。 由于 ThreadLocal是与线程绑定的,所以我们只要把 Looper 与 ThreadLocal 绑定了,那 Looper 和 Thread 也就关联上了

3.17.MessageQueue的enqueueMessage()方法如何进行线程同步的

就是单链表的插入操作。如果消息队列被阻塞回调用nativeWake去唤醒。 用synchronized代码块去进行同步

3.18.子线程中是否可以用MainLooper去创建Handler,Looper和Handler 是否一定处于一个线程

可以的。 在子线程中Handler handler = new Handler(Looper.getMainLooper());此时两者就不在一个线程中

3.19.ANR和Handler的联系

Handler是线程间通讯的机制,Android中,网络访问、文件处理等耗时操作必须放到子线程中去执行,否则将会造成ANR异常。 ANR异常:Application Not Response 应用程序无响应。产生ANR异常的原因:在主线程执行了耗时操作,对Activity来说,主线程阻塞5秒将造成ANR异常,对BroadcastReceiver来说,主线程阻塞10秒将会造成ANR异常。 解决ANR异常的方法:耗时操作都在子线程中去执行,但是,Android不允许在子线程去修改UI,可我们又有在 子线程去修改UI的需求,因此需要借助Handler。

4.HandlerThread

HandlerThread 就是一种可以使用Handler 的Thread。HandlerThread本质上是一个线程类,它继承了Thread。handlerThread 在 Android 中的一个具体的使用场景是 IntentService。

整体看:HandlerThread有自己的内部Looper对象,可以进行loopr循环。通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage()方法中执行异步任务。

源码分析:
创建HandlerThread后必须先调用HandlerThread.start()方法,Thread会先调用run方法,在 run() 方法中通过 Looper.prepare() 创建消息队列,并通过 Looper.loop() 方法来开启消息循环。由于 Loop.looper() 是一个死循环,导致 run() 也是无线循环,当有耗时任务进入队列时,则不需要开启新线程,在原有的线程中执行耗时任务即可,否则线程阻塞。因此当我们不需要使用 HandlerThread 的时候,要调用它的 quit() 方法或者 quiteSafely() 方法。

代码示例:

public class HandlerThreadActivity extends BaseActivity {
    private static final String TAG = "HandlerThreadActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        mHandlerThread = new HandlerThread("THREAD_NAME"); //创建handlerThread
        mHandlerThread.start();	// 必须先调用handlerThread的start方法
        mHandler = new Handler(mHandlerThread.getLooper());	//创建handler

        mStartBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Log.d(TAG,  String.valueOf((Looper.myLooper() == Looper.getMainLooper())) );
                        SystemClock.sleep(3000);
                    }
                });
            }
        });
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandlerThread.quit(); //destroy的时候终止线程
    }
}

快速点击三次按钮,会发现三次不同的任务按顺序执行,而且都是运行在子线程中。

优势:

  1. 将loop运行在子线程中处理,减轻了主线程的压力,使主线程更流畅,有自己的消息队列,不会干扰UI线程
  2. 串行执行,开启一个线程起到多个线程的作用

劣势:

  1. 由于每一个任务队列逐步执行,一旦队列耗时过长,消息会延时
  2. 对于IO等操作,线程等待,不能并发

我们可以使用HandlerThread处理本地IO读写操作(数据库,文件),因为本地IO操作大多数的耗时属于毫秒级别, 对于单线程 + 异步队列的形式不会产生较大的阻塞。

5.IdleHandler

idle:即闲置的、空闲的
可以理解为:在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制.

IdleHandler 是 MessageQueue 内定义的一个接口,一般可用于做性能优化。当消息队列内没有需要立即执行的 message 时,会主动触发 IdleHandler 的 queueIdle 方法。返回值为 false,即只会执行一次;返回值为 true,即每次当消息队列内没有需要立即执行的消息时,都会触发该方法

public final class MessageQueue {
    public static interface IdleHandler {
        boolean queueIdle();
    }
}

使用方式:通过获取 looper 对应的 MessageQueue 队列注册监听。

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        // doSomething()
        return false;
    }
});

使用场景:

  1. Activity启动优化:onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间
  2. 想要在一个View绘制完成之后添加其他依赖于这个View的View,当然这个用View.post()也能实现,区别就是用IdleHandle可以在消息队列空闲时执行
  3. 系统源码中 ActivityThread 的GcIdler,在某些场景等待消息队列暂时空闲时会尝试执行GC 操作;
  4. 系统源码中 ActivityThread 的 Idler,在 handleResumeActivity() 方法内会注册 Idler(),等待 handleResumeActivity 后视图绘制完成,消息队列暂时空闲时再调用 AMS 的 activityIdle 方法,检查页面的生命周期状态,触发 activity 的 stop 生命周期等。
    这也是为什么我们 BActivity 跳转 CActivity 时,BActivity 生命周期的 onStop() 会在 CActivity 的 onResume() 后。
  5. 一些第三方框架 Glide 和 LeakCanary 等也使用到 IdleHandler

6.同步屏障机制

我们知道无论是应用启动还是屏幕刷新都需要完整绘制整个页面内容,目前大多数手机的屏幕刷新率为60Hz,也就是耳熟能详的16ms刷新一次屏幕。那么问题来了,如果主线程的消息队列待执行的消息非常多,怎么能保证绘制页面的消息优先得到执行,来尽力保证不卡顿呢?

同步屏障的概念
同步屏障可以通过MessageQueue.postSyncBarrier函数来设置。该方法发送了一个 target = null 的Message到Queue中,在next方法中获取消息时,如果发现 target = null的Message,则在一定的时间内跳过同步消息,优先执行异步消息。再换句话说,同步屏障为Handler消息机制增加了一种「异步消息优先执行」的机制。在创建Handler时有一个async参数,传true表示此handler发送的时异步消息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保证UI绘制优先执行。

根据前文分析知:普通消息在入队前一定会设置target属性,那什么时候不会设置target属性,也就是target为null呢?

源码分析:以应用启动入手,页面启动过程不详述了,大体调用链路是ViewRootImpl.setView -> ViewRootImpl.requestLayout -> ViewRootImpl.scheduleTraversals

先看scheduleTraversals:

 void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 1.调用了消息队列的postSyncBarrier方法,进去看看
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

再看postSyncBarrier

   /**
        * @hide  
        */
        public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            // 2.仔细看这里 :创建了Message对象,但没有设置target属性,也就是 tagget = null
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
   			...
        }
    }

看到这里就知道了:什么消息的target是null?那就是postSyncBarrier发送的同步屏障消息

源码继续往下看会发现在postCallbackDelayedInternal方法中,创建了真正绘制页面的消息对象,并且调用setAsynchronous()将消息设置为了异步

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        ......  

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                // *
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true); //3. 将消息设置为了异步
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

通过以上分析可知,同步屏障应用:
在View更新时,draw、requestLayout、invalidate等很多地方都调用了ViewRootImpl.scheduleTraversals( Android为了更快的响应UI刷新事件,在ViewRootImpl.scheduleTraversals中使用了同步屏障

5.View的绘制(面试八股文之一)

5.1 Activity界面显示流程

首先大致梳理下,触发addView的整个流程:
在这里插入图片描述

Activity界面显示流程:Activity启动后,不会立马去显示界面上的view,而是等到onResume的时候才会真正显示view的时机,首先会触发windowManager.addView方法,在该方法中触发代理对象WindowManagerGlobal的addView方法,代理对象的addView方法中创建了viewRootImpl,将setContentView中创建的decorView通过viewRootImpl的setView方法放到了viewRootImpl中,最终经过viewRootImpl一系列的方法最终调用performTraversals方法。这之后才开始了View的真正绘制流程。

谈View的绘制前,先了解几个概念:
在这里插入图片描述
1. Window
Window即窗口,在Framework中的实现为android.view.Window这个抽象类,该类是对Android系统中的窗口的抽象。实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WMS分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。

android.view.Window这个抽象类可以看做Android中对窗口这一宏观概念所做的约定,而PhoneWindow这个类是Framework为我们提供的Window的唯一实现。

2.PhoneWindow
setContentView(),实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观。

3.DecorView
DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout,我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。

5.2.View绘制的三个阶段

从DecorView自上而下遍历整个View树:

  1. onMeasure(测量):
    View: onMeasure 方法会计算自己的尺寸并通过 setMeasureDimension 保存。

    ViewGroup : 重写onMeasure,遍历测量所有子的view,根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec,合并所有子View计算出ViewGroup的尺寸 ,然后通过setMeasuredDimension存储测量后的尺寸。

    (为什么要重写OnMeasure?因为ViewGroup是一个抽象类,没有定义测量的具体过程,其测量过程的 onMeasure方法需要各个子类去实现。如:LinearLayout、FrameLayout等等,这些控件的特性都是不一样的,测量规则自然也都不一样)

  2. onLayout(布局):
    View :因为是没有子View 的,所以View的onLayout里面什么都不做。

    ViewGroup: ViewGroup 中的 onLayout 方法会调用所有子 View 的 onLayout 方法,把尺寸和位置传给他们,让他们完成自我的内部布局(子元素如果是 ViewGroup 的子类,又开始执行 onLayout,如此循环往复,)。

  3. onDraw(绘制):
    ViewRoot创建一个Canvas对象,然后调用OnDraw():

    1. 绘制背景:对应 drawBackground(Canvas)(不能重写)
    2. 如果需要的话,保存canvas的图层(layer),来准备fading(不是必要的步骤)
    3. 绘制View内容:对应 onDraw(Canvas)
    4. 绘制子View: 对应 dispatchDraw(Canvas)
    5. 如果需要的话,绘制fading edges,然后还原图层(layer)(不是必要的步骤)
    6. 绘制滑动相关和前景: 对应 onDrawForeground(Canvas)

5.3.MeasureSpec(即测量规格)是什么

MeasureSpec指的是测量规格,是一个32位的整形值,它的高2位表示SpecMode(测量模式),低30位表示SpecSize(指在某种SpecMode下的规格大小)。MeasureSpec是View类的一个静态内部类,用来说明应该如何测量这个View。它由三种测量模式, 如下:

  • EXACTLY:精确测量模式,视图宽高指定为match_parent或具体数值时生效,这种模式下SpecSize的值就是View的测量值。
  • AT_MOST:大值测量模式,视图宽高指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的大尺寸的任何尺寸。
  • UNSPECIFIED:不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,由系统内部调用,例如ScrollView,我们不用管

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法,打包方法为makeMeasureSpec,解包方法为getMode和getSize。

5.4.子View创建MeasureSpec创建规则是什么

根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec:

在这里插入图片描述

5.5.自定义Viewwrap_content不起作用,而且是起到与match_parent(填充父容器)相同作用,为什么?

看源码:

public static int getDefaultSize(int size, int measureSpec) {  
	...
    switch (specMode) {  
		...
        // 模式为 AT_MOST / EXACTLY时,使用View测量后的宽/高值
        // 即measureSpec中的specSize
        case MeasureSpec.AT_MOST:  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
    }  
    return result;  //返回View的宽/高值
}

1.因为onMeasure()->getDefaultSize(),当View的测量模式是AT_MOST或EXACTLY时,View的大小都会被设置成子View MeasureSpec的specSize。而AT_MOST对应wrap_content;EXACTLY对应match_parent,所以默认情况下,wrap_content和match_parent是具有相同的效果的。

那为什么效果是填充父容器的效果呢?

因为子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来,计算逻辑封装在getChildMeasureSpec()中。在其中,子View MeasureSpec在属性被设置为wrap_content或match_parent情况下,子View MeasureSpec的specSize被设置成 parenSize = 父容器当前剩余空间大小

解决方案:
当给一个View/ViewGroup设置宽高为具体数值或者match_parent,它都能正确的显示,但是如果你设置的是 wrap_content->AT_MOST,则默认显示出来是其父容器的大小。
如果你想要它正常的显示为wrap_content,需要自己重写onMeasure()来自己计算它的宽高度并设置。此时,可以在wrap_content的情况下(对应 MeasureSpec.AT_MOST)指定内部宽/高(mWidth和mHeight)。

可参考:Android 自定义View:为什么你设置的wrap_content不起作用?

5.6.Android 在Activity中获取某个View的宽高的方式

  1. onWindowFocusChanged。当Activity的窗口得到焦点和失去焦点时均会被调用一次,如果频繁的进行onResume和onPause,那么onWindowFocusChanged也会被频繁调用

  2. view.post(runnable)。 通过post将runnable放入ViewRootImpl的RunQueue中,RunQueue中runnable最后的执行时机,是在下一个performTraversals到来的时候,也就是view完成layout之后的第一时间获取宽高。

  3. ViewTreeObserve.addOnGlobalLayoutListener()。当view树的状态发生改变或者view树内部的view可见发生变化时,onGlobalLayout方法将被回调

5.7.为什么onCreate获取不到View的宽高

Activity在执行完oncreate,onResume之后才创建ViewRootImpl,ViewRootImpl进行View的绘制工作是在performTraversals() 才开始的;而这个方法的调用显然是在 onResume() 方法之后,所以在 onCreate() 和 onResume() 方法中拿不到 View 的宽高信息也就很容易理解了。

5.8.View.post与Handler.post的区别

看View.post源码:

public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        getRunQueue().post(action);
        return true;
    }
  • View已经attach到window,等同于的handler.post()
  • View还未attach到window,将runnable放入ViewRootImpl的RunQueue中,而不是通过MessageQueue。RunQueue的作用类似于MessageQueue,只不过这里面的所有runnable最后的执行时机,是在下一个performTraversals到来的时候,也就是view完成layout之后的第一时间获取宽高,MessageQueue里的消息处理的则是下一次loop到来的时候。

5.9.什么是surface?什么是SurfaceView?

1.简单的说Surface对应了一块屏幕缓冲区,每个window对应一个Surface,任何View都要画在Surface的Canvas上。传统的view共享一块屏幕缓冲区,所有的绘制必须在UI线程中进行。引伸地,可以认为Android中的Surface就是一个用来画图形(graphics)或图像(image)的地方。

2.SurfaceView是View的子类,实现了Parcelable接口,其中内嵌了一个专门用于绘制的 Surface,SurfaceView可以控制这个Surface的格式和尺寸,以及Surface的绘制位置。可以理解为Surface就是管理数据的地方,SurfaceView就是展示数据的地方。使用双缓冲机制,有自己的 surface,在一个独立的线程里绘制。

优点: 使用双缓冲机制,可以在一个独立的线程中进行绘制,不会影响主线程,播放视频时画面更流畅 缺点:Surface不在View hierachy中,它的显示也不受View 的属性控制,SurfaceView 不能嵌套使用。在7.0版本之前不能进行平移,缩放等变换,也不能放在其它ViewGroup 中,在7.0版本之后可以进行平移,缩放等变换。

View和surfaceView的区别

  1. View适用于主动更新的情况,而SurfaceView则适用于被动更新的情况,比如频繁刷新界面。
  2. View在主线程中对页面进行刷新,而SurfaceView则开启一个子线程来对页面进行刷新。
  3. SurfaceView在底层机制中就实现了双缓冲机制,可以控制刷新频率,绘图时不会出现闪烁问题。

5.10.SurfaceView为什么可以直接子线程绘制 ?

View做线程保护主要是因为view的surface,view的绘制会层层上传直到ViewRoot,然后由ViewRoot调用performTraversals()绘制,而后调用下一层的draw(Canvas canvas)方法,然而实际上一个ViewRoot只有一个Surface,所以其中的View都是共用同一个surface,不让View在子线程中更新UI主要是不让子线程访问View的surface.
而surfaceview拥有自己的surface,并且由自己来管理,它就是设计给多线程访问的,来提高绘图的效率。

5.11.getWidth()方法和getMeasureWidth()区别

区别:

  • getMeasuredWidth:View在进行测量后得到的View内容占据的宽度。
  • getWidth:View在布局后整个View的宽度。

使用场景

  • getMeasuredWidth:在自定义view重写onLayout时;在我们用layoutinflater动态加载view后想获得view的原始宽度时。
  • getWidth:一般在view已经布局后呈现出来了,想获取宽度时。(除onLayout方法之外的地方)

5.12.requestLayout()、invalidate() 和 postInvalidate() 方法的区别?

  1. requestLayout会直接递归调用父窗口的requestLayout,直到ViewRootImpl,最终触发peformTraversals,由于mLayoutRequested为 true,会导致onMeasure()和onLayout()被调用,不一定会触发OnDraw, 将会根据标志位判断是否需要ondraw。

  2. view的invalidate递归调用父View的invalidateChildInParent,直到ViewRootImpl的invalidateChildInParent,最终触发peformTraversals,会导致当前view被重绘,由于mLayoutRequested为false,不会导致onMeasure 和onLayout被调用,而OnDraw会被调用。只能在UI线程调用。

  3. postInvalidate(),内部通过Handler发送了一个消息将线程切回到UI线程通知重新绘制 ,最终还是调用了View的invalidate() 。只是它可以在非UI线程中调用。

5.13. LinearLayout、FrameLayout 和 RelativeLayout 哪个效率高?

简单布局 FrameLayout>LinearLayout>RelativeLayout
复杂布局 RelativeLayout>LinearLayout>FrameLayout

  1. RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,也会调用子View2次onMeasure

  2. RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。

  3. 在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。
    最后再思考问题,为什么Google给开发者默认新建了个RelativeLayout,而自己却在DecorView中用了个LinearLayout。
    因为DecorView的层级深度是已知而且固定的,上面一个标题栏,下面一个内容栏。采用RelativeLayout并不会降低层级深度,所以此时在根节点上用LinearLayout是效率最高的。而之所以给开发者默认新建了个RelativeLayout是希望开发者能采用尽量少的View层级来表达布局以实现性能最优,因为复杂的View嵌套对性能的影响会更大一些。

  4. 要用两层LinearLayout,尽量改用一个RelativeLayout。

5.14.自定义 View 的流程和注意事项

大多数自定义View要么是在onDraw方法中画点东西,和在onTouchEvent中处理触摸事件。

自定义View步骤 :
onMeasure,可以不重写,不重写的话就要在外面指定宽高,建议重写;
onDraw,看情况重写,如果需要画东西就要重写;
onTouchEvent,也是看情况,如果要做能跟手指交互的View,就重写;

自定义 View注意事项: 如果有自定义布局属性的,在构造方法中取得属性后应及时调用recycle方法回收资源;onDraw和onTouchEvent方法中都应尽量避免创建对象,过多操作可能会造成卡顿。

自定义ViewGroup步骤:
onMeasure(必须),在这里测量每一个子View,还有处理自己的尺寸;
onLayout(必须),在这里对子View进行布局;
如有自己的触摸事件,需要重写onInterceptTouchEvent或onTouchEvent。

自定义ViewGroup注意事项: 如果想在ViewGroup中画点东西,又没有在布局中设置background的话,会画不出来,这时候需要调用 setWillNotDraw方法,并设置为false; 如果有自定义布局属性的,在构造方法中取得属性后应及时调用recycle方法回收资源; onDraw和onTouchEvent方法中都应尽量避免创建对象,过多操作可能会造成卡顿。

5.14. 自定义控件优化方案

1.降低View.onDraw()的复杂度;onDraw不要创建新的局部对象;onDraw不执行耗时操作 。
2.避免过度绘制 (Overdraw)、降低刷新频率----待补充

6.View事件分发(面试八股文之一)

6.1 了解Activity的构成

一个Activity包含了一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域:一个是TitleView,另一个是ContentView,而我们平时所写的就是展示在ContentView中的

6.2 触摸事件的类型

ACTION_DOWN
ACTION_MOVE(移动的距离超过一定的阈值会被判定为ACTION_MOVE操作)
ACTION_UP

View事件分发本质就是对MotionEvent事件分发的过程。即当一个MotionEvent发生后,系统将这个点击事件传递到一个具体的View上。

6.3 事件分发流程

  1. 注意:在Android系统中,拥有事件传递处理能力的类有以下三种:

    Activity:拥有分发和消费两个方法。
    ViewGroup:拥有分发、拦截和消费三个方法。
    View:拥有分发、消费两个方法。
    

    即只有ViewGroup有拦截事件

  2. 图解(只适用于理解事件分发机制)

  1. 事件分发的传递规则

    点击事件产生后会由默认会先走Activity的分发,传递给PhoneView,再传递给DecorView,最后传递给顶层的ViewGroup。接着走ViewGroup的分发,然后到ViewGroup的拦截,后面再到View的分发事件,最后会传到View的消费事件,如果View不消费,紧接着回传到ViewGroup的消费事件,如果ViewGroup也不消费,最后回到Activity的消费事件。

    整个事件分发构成了一个U型结构,下面总结了分发的细节流程:

    1. ViewGroupdispatchTouchEvent返回true或false,touch事件不会往子view中传递。只有在返回super.dispatchTouchEvent时候touch事件才会传递到子view。

      ture:当前View消耗所有事件

      false:停止分发,交由上层控件的onTouchEvent方法进行消费,如果本层控件是Activity,则事件将被系统消费。false的时候只会触发action_down。

    2. ViewGrouponInterceptTouchEvent返回false或者super.onInterceptTouchEvent时,touch事件会传递到子view。

      返回true事件不会向下传递,交给自己的onTouchEvent处理。

    3. ViewdispatchTouchEvent返回true或false,touch事件不会传给自己的ontouchEvent事件,返回false,只会触发action_down,move和up不会触发;返回true,才会触发move和up。返回super.dispatchTouchEvent,touch事件才会交给自己的onTouchEvent处理。

    4. ViewonTouchEvent返回false,只会有action_down事件,touch事件交给上一层处理,如果返回true才会消费,事件不会向上传递,如果返回super.ontouchEvent,得看clickable是不是返回true。

  2. ACTION_CANCEL什么时候触发,触摸button然后滑动到外部抬起会触发点击事件吗,再滑动回去抬起会么?

    一般ACTION_CANCEL和ACTION_UP都作为View一段事件处理的结束。如果在父View中拦截ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。

    如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现action_cancel。

  3. 点击事件被拦截,但是想传到下面的View,如何操作?
    重写子类的requestDisallowInterceptTouchEvent()方法返回true就不会执行父类的onInterceptTouchEvent(),即可将点击事件传到下面的View。

  4. 如何解决View的事件冲突?举个开发中遇到的例子?
    常见开发中事件冲突的有ScrollView与RecyclerView的滑动冲突、RecyclerView内嵌同时滑动同一方向。

    滑动冲突的处理规则:

    对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
    对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部View拦截事件,何时由内部View拦截事件。
    对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
    滑动冲突的实现方法:

    外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。
    内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合requestDisallowInterceptTouchEvent方法。

7.Context

Context
翻译为上下文,也可以理解为环境,是提供一些程序的运行环境基础信息。
是一个抽象类。继承结构如下:
在这里插入图片描述

ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。

ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity,所以Activity和Service以及Application的Context是不一样的,只有Activity需要主题,Service不需要主题。

Context一共有三种类型,分别是Application、Activity和Service。在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

Application中的Context和Activity中的Context的区别

Activity.this取的是当前Activity的Context,它的生命周期则只能存活于当前Activity
getApplicationContext() 生命周期是整个应用,当应用程序摧毁的时候,它才会摧毁

详细可以阅读:
郭霖:Android Context完全解析,你所不知道的Context的各种细节
鸿洋:Android Context 上下文 你必须知道的一切

8.序列化

Serialiable与Parcelable的区别

在使用内存的时候,Parcelable 类比Serializable 性能高,首选使用Parcelable 类

Serializable 在序列化的时候会产生大量的临时变量,从而引起频繁的GC

数据持久化,Parcelable 不能使用在要将数据存储在磁盘上的情况。尽管Serializable 效率低点,但在这种情况下,还是建议你用Serializable

9.动画

Android中动画大致分为3类:帧动画补间动画(View Animation)、属性动画(Object Animation)。

帧动画:通过xml配置一组图片,动态播放。很少会使用。

补间动画(View Animation):大致分为旋转、透明、缩放、位移四类操作。很少会使用。

属性动画(Object Animation):属性动画是现在使用的最多的一种动画,它比补间动画更加强大。属性动画大致分为两种使用类型,分别是 ViewPropertyAnimator 和 ObjectAnimator。前者适合一些通用的动画,比如旋转、位移、缩放和透明,使用方式也很简单通过 View.animate() 即可得到 ViewPropertyAnimator,之后进行相应的动画操作即可。后者适合用于为我们的自定义控件添加动画,当然首先我们应该在自定义 View 中添加相应的 getXXX() 和 setXXX() 相应属性的 getter 和 setter 方法,这里需要注意的是在 setter 方法内改变了自定义 View 中的属性后要调用 invalidate() 来刷新View的绘制。之后调用 ObjectAnimator.of 属性类型()返回一个 ObjectAnimator,调用 start() 方法启动动画即可。

补间动画与属性动画的区别:

补间动画是父容器不断的绘制 view,看起来像移动了效果,其实 view 没有变化,还在原地。

属性动画是通过不断改变 view 内部的属性值,真正的改变 view。

10.Android5.0-10.0版本变更及开发适配

  1. Android 5.0

    Material Design

    ART虚拟机:ART模式在用户安装App时进行预编译AOT(Ahead-of-time),将android5.X的运行速度提高了3倍左右。

  2. Android 6.0

    应用权限管理
    运行时权限机制->危险权限需要动态申请权限
    官方指纹支持
    Doze电量管理

  3. Android 7.0

    FileProvider:在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。要应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider类。

    APK signature scheme v2:Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。

    只勾选V1签名就是传统方案签署,但是在 Android 7.0 上不会使用V2安全的验证方式。
    只勾选V2签名7.0以下会显示未安装,Android 7.0 上则会使用了V2安全的验证方式。
    同时勾选V1和V2则所有版本都没问题。
    

    org.apache不支持问题

    build.gradle里面加上这句话

    defaultConfig {
        useLibrary 'org.apache.http.legacy'
    }
    

    或者在AndroidManifest.xml添加下面的配置

    <uses-library
        android:name="org.apache.http.legacy"
        android:required="false" />
    

    SharedPreferences闪退:

    // MODE_WORLD_READABLE:Android 7.0以后不能使用这个获取,会闪退
    // 应修改成MODE_PRIVATE
    SharedPreferences read = getSharedPreferences(RELEASE_POOL_DATA, MODE_WORLD_READABLE);
    
  4. Android 8.0
    Notification(通知权限):Android 8.0之后通知权限默认都是关闭的,无法默认开启以及通过程序去主动开启,需要程序员读取权限开启情况,然后提示用户去开启。
    判断权限是否开启:

    /**
     * 判断通知权限是否开启
     * @param context 上下文
     */
    public static boolean isNotificationEnabled(Context context){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).areNotificationsEnabled();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            ApplicationInfo appInfo = context.getApplicationInfo();
            String pkg = context.getApplicationContext().getPackageName();
            int uid = appInfo.uid;
    
            try {
                Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());
                Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);
                Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION");
                int value = (Integer) opPostNotificationValue.get(Integer.class);
                return (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;
            } catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {
                return true;
            }
        } else {
            return true;
        }
    

    前往设置开启权限:

    /**
     * 打开设置页面打开权限
     *
     * @param activity activity
     * @param requestCode 这里的requestCode和onActivityResult中requestCode要一致
     */
    public static void startSettingActivity(@NonNull Activity activity, int requestCode) {
        try {
            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + activity.getPackageName()));
            intent.addCategory(Intent.CATEGORY_DEFAULT);
            activity.startActivityForResult(intent, requestCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

自适应启动图标:从Android 8.0系统开始,应用程序的图标被分为了两层:前景层和背景层。也就是说,我们在设计应用图标的时候,需要将前景和背景部分分离,前景用来展示应用图标的Logo,背景用来衬托应用图标的Logo。需要注意的是,背景层在设计的时候只允许定义颜色和纹理,但是不能定义形状。注意图标图层的大小,两层的尺寸必须为108x108dp,前景图层中间的72x72dp图层就是在手机界面上展示的应用图标范围。这样系统在四面各留出18dp以产生有趣的视觉效果,如视差或脉冲(动画视觉效果由受支持的启动器生成,视觉效果可能因发射器而异)。

<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

安装APK:
Android 8.0去除了“允许未知来源”选项,如果我们的App具备安装App的功能,那么AndroidManifest文件需要包含REQUEST_INSTALL_PACKAGES权限,未声明此权限的应用将无法安装其他应用。当然,如果你不想添加这个权限,也可以通过getPackageManager().canRequestPackageInstalls()查询是否有此权限,没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES这个action将用户引导至安装未知应用权限界面去授权。

静态广播无法正常接收:
Google官方声明:从android 8.0(API26)开始,对清单文件中静态注册广播接收者增加了限制,建议大家不要在清单文件中静态注册广播接收者,改为动态注册。当然,如果你还是想用静态注册的方式也是有方法的,Intent里添加Component参数可实现。

发送静态广播的特殊处理

Intent intent = new Intent( "广播的action" );
intent.setComponent( new ComponentName( "包名(如:com.yhd.rocket)","接收器的完整路径(如:com.yhd.rocket.receiver.RoReceiver)" ) );
sendBroadcast(intent);
  1. Android 9.0
    刘海屏API支持:
    Android 9 支持最新的全面屏,其中包含为摄像头和扬声器预留空间的屏幕缺口。 通过 DisplayCutout类可确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些屏幕缺口区域是否存在及其位置,使用 getDisplayCutout() 函数。

    CLEARTEXT communication to http://xxx not permitted by network security policy:
    问题原因: Android P 限制了明文流量的网络请求,非加密的流量请求(http)都会被系统禁止掉。解决方案:
    方案一:将http请求改为https
    方案二:添加usesCleartextTraffic属性

    <application
        android:usesCleartextTraffic="true">
    </application>    
    

    方案三:添加资源文件(复杂)
    1、在资源文件新建xml目录,新建文件network_security_config.xml

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config cleartextTrafficPermitted="true" />
    </network-security-config>
    

    2、清单文件配置:

    <application
        android:networkSecurityConfig="@xml/network_security_config">
    </application>
    

    全面限制静态广播的接收
    升级安卓9.0之后,隐式广播将会被全面禁止,在AndroidManifest中注册的Receiver将不能够生效,你需要在应用中进行动态注册。

  2. Android 10

    暗黑模式
    分区存储
    隐私增强(后台能否访问定位)
    限制程序访问剪贴板
    应用黑盒
    权限细分需兼容
    后台定位单独权限需兼容

    设备唯一标示符需兼容:从Android10开始普通应用不再允许请求权限android.permission.READ_PHONE_STATE。而且,无论你的App是否适配过Android Q(既targetSdkVersion是否大于等于29),均无法再获取到设备IMEI等设备信息。

    //受影响的API
    Build.getSerial();
    TelephonyManager.getImei();
    TelephonyManager.getMeid()
    TelephonyManager.getDeviceId();
    TelephonyManager.getSubscriberId();
    TelephonyManager.getSimSerialNumber();
    

    targetSdkVersion<29 的应用,其在获取设备ID时,会直接返回null
    targetSdkVersion>=29 的应用,其在获取设备ID时,会直接抛出异常SecurityException
    如果您的App希望在Android 10以下的设备中仍然获取设备IMEI等信息,可按以下方式进行适配:

    <uses-permission android:name="android.permission.READ_PHONE_STATE"
            android:maxSdkVersion="28"/>
    

    后台打开Activity 需兼容
    非 SDK 接口限制 需兼容

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-02-28 15:41:20  更:2022-02-28 15:45:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 16:19:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码