Android 基础知识点

317 阅读12分钟

Android 编译知识点

多flavor导致的源码依赖问题

  1. www.cnblogs.com/yrstudy/p/1…
  2. flavorDimension不一致,而flavor一致。那你就需要使用 missingDimensionStrategy 'demo2', 'flavor3', 'flavor4' *
  3. flavorDimension一致,而flavor不一致。那你就需要使用 matchingFallbacks*

Android 基础知识点

子线程 真的不能更新UI?

Only the original thread that created a view hierarchy can touch its views.
准确说应该是 只有创建了view树的线程,才能访问它的子view。并没有说子线程一定不能访问UI

Toast可以在子线程show吗?

答案是可以的
由于Toast是通过handler show的,所以子线程显示Toast需要调用Looper。

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();    
        Toast.makeText(MainActivity.this, "子线程showToast", Toast.LENGTH_SHORT).show();
        Looper.loop();
    }
}).start();

Handler

参考链接

原理

  • 处理Message 和 Runnable (Runnable会被封装进Message,所以本质上还是Message)
  • Handler 的背后有 Looper、MessageQueue 支撑,Looper 负责消息分发,MessageQueue 负责消息管理;
  • 在创建 Handler 之前一定需要先创建 Looper;
  • Looper 有退出的功能,但是主线程的 Looper 不允许退出;
  • 异步线程的 Looper 需要自己调用 Looper.myLooper().quit(); 退出;
  • Handler.handleMessage() 所在的线程是 Looper.loop() 方法被调用的线程,也可以说成 Looper 所在的线程,并不是创建 Handler 的线程; 使用内部类的方式使用 Handler 可能会导致内存泄露,即便在 Activity.onDestroy 里移除延时消息,必须要写成静态内部类;

作者:程序亦非猿 链接:juejin.cn/post/684490… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Handler实现方式

  • 继承Handler
  • 实现Handler.Callback接口 (处理消息的优先级高)

节省内存的创建消息方式

  • Message.obtain()
  • handler.obtainMessage()

内存泄露

  • 现象:Handler发送延时消息,如果在延时期间关闭了 Activity,那么该 Activity 会泄露。
  • 原因:这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。
  • 解决办法:将 Handler定义成静态的内部类,在内部持有Activity的弱引用,并及时移除所有消息。

为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?

  • ActivityThread 就是主线程。事实上它并不是一个线程,而是主线程操作的管理者。
  • Activity 的生命周期都是依靠主线程的Looper.loop,都是收到对应消息(msg=H.PAUSE_ACTIVITY)后调用的回调函数。

Handler 中有Loop 死循环,为什么没有阻塞主线程

真正卡死主线程的操作是OnCreate/onResume等回调方法执行时间太长,会导致掉帧,甚至ANR. Looper.loop本身不会导致应用卡死。而且主线程AvtivityThread(不真正的线程),Activity 的生命周期都是依靠主线程的Looper.loop。生命周期回调,都是收到对应消息(msg=H.PAUSE_ACTIVITY)后调用的。一旦主线程的消息循环退出,则程序就退出了。当无消息时,阻塞,释放cpu,有消息再唤醒执行。

 public static void main(String[] args) {
       //....
       //创建Looper和MessageQueue对象,用于处理主线程的消息
       Looper.prepareMainLooper();
       //创建ActivityThread对象
       ActivityThread thread = new ActivityThread();
       //建立Binder通道 (创建新线程)
       thread.attach(false);
       Looper.loop(); //消息循环运行
       throw new RuntimeException("Main thread loop unexpectedly exited");
   }
}

Activity启动模式

两种设置方式,一个是AndroidManifest中设置launcherMode,另一种是通过Intent setFlag设置。
- 标准模式(Standard)
- 栈顶复用(SingleTop)-> 在栈顶则调用 onNewIntent
- 栈内复用(SingleTask)->栈中有,它上面的Activity都出栈,并调用 onNewIntent-> App的主页Home,常用此模式
- 单例模式(SingleInstance)->创建新栈,并且只有它。  

synchronized和lock的区别?

- synchronized会主动释放锁,而lock需要手动调用unlock释放锁;
- synchronized是java内置的关键字,而lock是个java类

关于协程的概念

- 简单的介绍:协程又称微线程,是一个线程执行。协程看上去也是子程序,但是不同的是可以在子程序内部中断转而去执行其他子程序,然后在合适的时候再返回中断位置继续执行。
- 协程特点:
- 执行效率高:没有多线程的线程间切换的开销;
- 不需要多线程的锁机制:因为只有一个线程,所以不需要

单例模式

```java
public class SingleInstance {

    private static SingleInstance mInstance = null;

    private SingleInstance(){}

    public static SingleInstance getInstance(){
        if(mInstance==null){
            synchronized (SingleInstance.class){
                if(mInstance==null){
                    mInstance = new SingleInstance();
                }
            }
        }
        return mInstance;
    }
}
```
有次被问到为什么要有两次空判断?
- 第一次空判断和好理解,可以很大程度上减少锁机制的次数;
- 第二次判空是因为,如果a,b两个线程都到了synchronized处,而假设a拿到了锁,进入到代码块中创建了对象,然后释放了锁,由于b线程在等待锁,所以a释放后,会被b拿到,因此此时判空就保证了实例的唯一性。

gson序列化数据时如何排除某个字段?

- 方式一:给字段加上 transient 修饰符
- 方式二:排除Modifier指定类型的字段。这个方法需要用GsonBuilder定制一个GSON实例。
- 方式三:使用@expose注解。没有被 @expose 标注的字段会被排除

ButterKnife与Xutils注解的区别?以及Retrofit中的注解是如何处理的?

- ButterKnife采用的是编译时注解,在编译时生成辅助类,在运行时通过辅助类完成操作。编译时注解运行效率较高,不需要反射操作。
- XUtils采用的是运行时注解,在运行时通过反射进行操作。运行时注解相对效率较低。
- Retrofit与EventBus采用的都是运行时注解,也就是通过反射技术处理的

AyncTask

  • AsyncTask就是一个Handler和线程池的封装
  • 核心线程数是有限的(CPU_COUNT+1),不适合大量的后台任务处理
  • AsyncTask类必须在主线程初始化,因为return executeOnExecutor(sDefaultExecutor, params)这里也只能在UI线程走。
  • 可以支持并行和串行,当想要串行执行时,直接执行execute()方法,如果需要并行执行,则要执行executeOnExecutor(Executor)

LeakCanery

- LeakCanary实现内存泄漏的主要判断逻辑是这样的。当我们观察的Activity或者Fragment销毁时,我们会使用一个弱引用去包装当前销毁的Activity或者Fragment,并且将它与本地的一个ReferenceQueue队列关联。我们知道如果GC触发了,系统会将当前的引用对象存入队列中。
- 如果没有被回收,队列中则没有当前的引用对象。所以LeakCanary会去判断,ReferenceQueue是否有当前观察的Activity或者Fragment的引用对象,第一次判断如果不存在,就去手动触发一次GC,然后做第二次判断,如果还是不存在,则表明出现了内存泄漏。
- 2.3版本
- LeakCanary通过注册ContentProvider,实现应用启动时自动初始化,完成核心功能模块的工具类的初始化和Activity、Fragment生命周期的监听注册。
- LeakCanaryCore的核心原理就是通过WeakReference和ReferenceQueue的组合,来检测期望被回收的对象。当期望被回收的对象没有被释放,就会dump heap生成hprof文件,借由shark库进行解析,构建泄漏对象最短引用路径。最后将结果在通知栏和Activity进行渲染显示。

App之间共享

  • FileProvider setResult返回文件的内容URI和临时访问权限的Intent
  • 分享少量文本或数值数据,则应发送包含该数据的 Intent
  • 两个App共享资源,设置相同的android:sharedUserId 参考
    1. 包名+PackageManager获取Resources,再通过资源名获取ID,再获取资源。参考
    2. 包名创建Context + 反射其中方法 参考

Gson

release 格式化出的对象为null

解决办法是,在proguard文件中,用-keep 方式跳过其混淆

分区存储

Android 10(API 级别 29)外部存储设备的分区访问权限,外部存储设备的公共区域是不让访问的,如果强行访问,会在创建或读写文件的api上报错

  • 如果是app私有的,存在getExternalFilesDir()返回的文件夹下,也就是Android/data/包名/files/文件夹;
  • 如果是需要分享的,需要采用媒体库(MediaStore)的方式来存取,
  • 在媒体集或应用目录之外,写任何文件都需要系统的文件选择器

目录划分

  • 应用内存储存 /data/data/<app包名>/ 文件夹下。 files 和Cache. 磁盘空间不足,系统会删除Cache目录。
  • 外部存储 /Android/data/<app包名>/
  • 共有目录 其他

权限

文件位置|所需权限|所需权限|其他应用是否可以访问?|卸载后是否移除文件 |---|---|---|---| 应用内部存储|无|getFilesDir() 或 getCacheDir() |是 应用外部存储|无|getExternalFilesDir|是 媒体集合(照片、视频、音频)|访问其他应用创建的需要权限 READ_EXTERNAL_STORAGE|MediaStore|否 下载的内容(非媒体文件)|无|存储框架(系统文件选择器)|否

适配

  • 目标版本 API < = 29 时 , 应用仍可请求 requestLegacyExternalStorage 属性。应用可以利用此标记暂时停用与分区存储相关的变更
  • 目标版本 API > 29时,强制执行分区存储。
内容类型访问方法所需权限其他应用是否可以访问?卸载应用时是否移除文件?
应用内部存储使用 getFilesDir() 或 getCacheDir() 方法
应用外部存储使用 getExternalFilesDir() 或 getExternalCacheDir() 方法
媒体集MediaStore API访问其他应用的文件需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限是,但需权限
文档和其他文件,包括已下载的文件存储访问框架是,可以通过系统文件选择器访问

共享文件

  • Android N 系统,Android 框架执行的 StrictMode,API 禁止向您的应用外公开file://
  • FileProvider这个类就是把一个文件File,转换为 content://URI的
  • FileProvider是ContentProvider子类
  • 在AndroidManifest.xml中标签下声明一个provider
  • 需要对真实的filepath进行映射,所以需要编写一个xml文档,name假名字,path真名。
  • 创建File对象,Android 7.0后,通过FileProvider.getUriForFile获取Uri.7.0之前,Uri.fromFile
  • 7.0之后,需要对Uri授权
//截图保存在外部存储
Intent intent = new Intent();
Uri photoUri;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
	photoUri = Uri.fromFile(file);
} else {
	photoUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", file);
	intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, "");
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_STREAM, photoUri);
intent = Intent.createChooser(intent, "分享");
mContext.startActivity(intent);

数据库

  • 三层 ContentProvider + SqilteHelper +SQLlite
  • ContentProvider 实现openFile就可以共享文件. 7.0后的FileProvider继承了它。
  • ConentProvider 安全 参考1 参考2
    1. application同级,自定义私有权限 permission , protectionLevel signature签名权限.
    2. provider 设置读写的 自定义私有权限
    3. 第三方应用通过uses-permission 申请权限
    4. SqliteQueryBuilder中通过setProjectMap设置对外提供列名和真实列名的映射

App 启动流程

851999-a9c2c456c9f91596.webp

Canvas save store区别

趋势图绘制,以及第三方绘图库的使用

Textureview和Surfaceview区别

  • Surfaceview由于是独立的一层View,更像是独立的一个Window,不能加上动画、平移、缩放;
  • 两个SurfaceView不能相互覆盖。
  • TextureView更像是一般的View,像TextView那样能被缩放、平移,也能加上动画。- TextureView只能在开启了硬件加速的Window中使用,并且消费的内存要比SurfaceView多,并伴随着1-3帧的延迟。
  • 如何选择?
  • 在我音视频应用工程里面,需要实现缩放,移动,层叠功能,所以选择的是TextureView。
  • 之前接触过天气项目,需要在桌面显示天气状态的动画,这种属于全屏铺满,用的是SurfaceView。

数据库升级

SQLite数库对ALTER TABLE命令支持非常有限,只能在表末尾添加列,不能修改列定义,不能删除已有的列。那么如果要修改表呢?我们可以采用临时表的办法。具体来说有四步:
- 将现有表重命名为临时表
- 创建新表
- 将临时表的数据导入新表(注意处理修改的列)
- 删除临时表。

唯一ID方案(稳定不变、不同设备不重复)

  • IMEI, Android6.0需动态申请权限,10.0申请权限也没用。新APP不建议,旧的做兼容。
  • SERIAL 序列号, 8.0返回Unknow,getSerial()需权限,10.0申请权限也没用。
  • MAC地址,6.0后为02:00:00:00:00:00,9.0又会支持随机Mac地址
  • AndroidId,刷机、重置、root会变,8.0之后唯一性决定于应用签名、用户和设备三者的组合
  • 方案:imei+serial+androidId 拼接的字符串生成uuid。如都获取不到,直接生成UUID
  • 内部存储(SP),外部隐藏文件都保存生成的UUID

嵌套滚动机制

区别

  • 正常的事件分发机制,是父View拦截事件以后,子View将不会再接到事件,而嵌套滚动机制是子View先拿到事件,然后将他的坐标传递给父View,父View会消耗部分事件,然后再将事件还给子View来消耗,从而实现父View跟子View都能响应事件的情况。

核心机制

  • 子View和父View,分别实现 NestedScrollingChild NestedScrollingParent接口
  • RecyclerView 实现了NestedScrollingChild2 接口
  • CoordinatorLayout 实现了NestedScrollingParent2接口
  • NestedScrollView 实现了NestedScrollingParent2 和NestedScrollingChild2 子(发起者)|父(被回调) |---|---| startNestedScroll|onStartNestedScroll、onNestedScrollAccepted dispatchNestedPreScroll|onNestedPreScroll dispatchNestedScroll|onNestedScroll stopNestedScroll|onStopNestedScroll

事件处理流程

  • 调用child的startNestedScroll()来发起嵌套滚动流程(实质是寻找能够配合child进行嵌套滚动的parent)。
  • parent的onStartNestedScroll()会被回调,如果此方法返回true,则onNestedScrollAccepted()也会被回调。
  • child每次滚动前,可以先询问parent是否要滚动,即调用dispatchNestedPreScroll(),这会回调到parent的onNestedPreScroll(),parent可以在这个回调中先于child滚动。
  • Parent的disdispatchNestedPreScroll()之后,child可以进行自己的滚动操作,调用dispatchNestedScroll。
  • child滚动以后,会回调到parent的onNestedScroll(),在这里parent可以进行后于child的滚动。
  • 滚动结束,child调用stopNestedScroll(),Parent的onStopNestedScroll被调用。

使用

参考

拓展

  • scrollTo():表示的是移动到哪个坐标点,坐标点的位置就会移动到屏幕原点的位置
  • scrollBy():表示的是移动的增量dx和dy,如果为负值则移动的是相反方向(为正时代表右向左或者下向上)
  • getScrollX():屏幕原点X坐标减去调用视图左上角X坐标,例如蓝色图得到的为0-(-480)=480
  • getScrollY():屏幕原点Y坐标减去调用视图左上角Y坐标,例如蓝色图得到的为0-0=0
  • getRawX()、getRawY()返回的是触摸点相对于屏幕的位置,
  • getX()、getY()返回的则是触摸点相对于View的位置。