Android 编译知识点
多flavor导致的源码依赖问题
- www.cnblogs.com/yrstudy/p/1…
- flavorDimension不一致,而flavor一致。那你就需要使用 missingDimensionStrategy 'demo2', 'flavor3', 'flavor4' *
- 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 参考
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
- application同级,自定义私有权限 permission , protectionLevel signature签名权限.
- provider 设置读写的 自定义私有权限
- 第三方应用通过uses-permission 申请权限
- SqliteQueryBuilder中通过setProjectMap设置对外提供列名和真实列名的映射
App 启动流程
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的位置。