带你梳理一遍 Android 核心知识

文导读|   点击标题阅读

互联网寒冬下,程序员如何突围提升自己?

Flutter 与 React Native 谁主沉浮?

女面试官:我拉链开了你怎么提醒我?

作者:薛定猫的谔

https://juejin.im/post/5c46db4ae51d4503834d8227

超长好文,建议慢慢品用,由于篇幅超越微信限制,略微有点删减,不影响阅读。


0、一句话知识点


1. Android 9 (API level 28) 开始废弃了 Loader API,包括 LoaderManager 和 CursorLoader 等类的使用。推荐使用 ViewModel 和 LiveData 在 Activity 或 Fragment 生命周期中加载数据;


2. Activity 可以通过 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 保持屏幕常亮,这是最推荐、最简单、最安全的保持屏幕常亮的方法,给 view 添加 android:keepScreenOn="true" 也是一样的。


这个只在这个 Activity 生命周期内有效,所以大可放心,如果想提前解除常亮,只需要清除这个 flag 即可。


3. WAKE_LOCK 可以阻止系统睡眠,保持 CPU 一直运行,需要 android.permission.WAKE_LOCK 权限。


通过 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag") 创建实例


通过 wakeLock.acquire() 方法请求锁,通过 wakelock.release() 释放锁


4. WakefulBroadcastReceiver 结合 IntentService 也可以阻止系统睡眠;


5. Android 8.0 (API level 26) 开始支持自适应启动图标,自适应启动图标必须由前景和背景两部分组成,尺寸必须都是 108 x 108 dp,其中内部的 72 x 72 dp 用来显示图标,靠近四个边缘的 18 dp 是保留区域,用来进行视觉交互


6. 对于字体大小自适应的 TextView 宽和高都不能是 wrap_content,autoSizeTextType 默认是 none,设置为 uniform 开启自适应,默认最小 12sp,最大 112sp,粒度 1px。autoSizePresetSizes 属性可以设置预置的一些大小


7. Android 8.0 (API level 26) 开始支持 XML 自定义字体,兼容库可以兼容到 Android 4.1 (API level 16),字体文件路径为 res/font/,使用属性为 fontFamily,获取 Typeface 为 getResources().getFont(R.font.myfont);,兼容库使用 ResourcesCompat.getFont(context, R.font.myfont)


8. Android 9 (API level 28) 支持控件放大镜功能,Magnifier 的 show() 方法的参数是相对于被放大 View 的左上角的坐标


9. 工程中的 Drawable 资源只能有一个状态,你不应该手动更改它的任何属性,否则会影响到其它使用这个 Drawable 资源的地方


10. Android 7.0 (API level 24) 开始支持在 XML 文件中使用自定义 Drawable,公共顶级类使用全限定名作为标签名即可 <com.myapp.MyDrawable>,公共静态内部类可以使用 class 属性 class="com.myapp.MyTopLevelClass$MyDrawable"


11. Android 5.0 (API level 21) 开始支持为 Drawable 设置 tint


12. Android 5.0 (API level 21) 开始支持矢量图,支持库可以支持到 Android 2.1 (API level 7+),兼容低版本是需要 Gradle 插件版本大于 2.0+ 时添加 vectorDrawables.useSupportLibrary = true 并使用 VectorDrawableCompat 和 AnimatedVectorDrawableCompat


1、应用资源


1. 添加资源限定符的顺序为: 


SIM 卡所属的国家代码和移动网代码 → 语言区域代码 → 布局方向 → 最小宽度 → 可用宽度 → 可用高度 → 屏幕大不大 → 屏幕长不长 → 屏幕圆不圆 → 屏幕色域宽不宽 → 屏幕支持的动态范围高不高 → 屏幕方向 → 设备的 UI 模式 → 夜间模式 → 屏幕像素密度 → 触摸屏类型 → 键盘类型 → 主要的文字输入方式  → 导航键是否可用 → 主要的非触摸导航方式 → 支持的 API level


2. 一个资源目录的每种资源限定符最多只能出现一次;


3. 必须提供缺省的资源文件;


4. 资源目录名是大小写不敏感的;


5. drawable 资源取别名:


 
 

 
 
<?xml version="1.0" encoding="utf-8"?><resources>    <drawable name="icon">@drawable/icon_ca</drawable></resources>
<resources>
    <drawable name="icon">@drawable/icon_ca</drawable>
</resources>

6. 布局文件取别名:


 
 
<?xml version="1.0" encoding="utf-8"?><merge>    <include layout="@layout/main_ltr"/></merge>
<merge>
    <include layout="@layout/main_ltr"/>
</merge>


只有动画、菜单、raw 资源 以及 xml/ 目录中的资源不能使用别名


7. 寻找使用最优资源的流程:


640?wx_fmt=other


8. 在应用程序运行时,设备的配置可能会发生变化(如屏幕方向变化、切换到多窗口模式,切换了系统语言),默认情况下系统会销毁重建正在运行的 Activity ,所以应用程序必须保证销毁重建的过程中用户的数据和页面状态完好无损地恢复。


如果不想系统销毁重建你的 Activity 只需要在 manifest 文件的 <activity> 标签的 android:configChanges 属性中添加你想自己处理的配置更改,多个配置使用 "|" 隔开,此时系统就不会在这些配置更改后销毁重建你的这个 Activity 而是直接调用它的 onConfigurationChanged() 回调方法,你需要在这个回调中自己处理配置更改后的行为。


9. Activity 的销毁重建不但发生在设备配置更改后,只要用户离开了某个 Activity,那么那个 Activity 就随时可能被系统销毁。所以销毁重建是无法避免的,也不应该逃避,而是应该想办法保存和恢复状态


10. 由于各种各样的硬件都能安装 Android 操作系统,Android 操作系统之间也可能千差万别,而应用程序的一些功能是与这些软硬件息息相关的,如拍照应用需要设备必须有摄像头才能正常工作。


应用可以通过 <uses-feature> 标签声明只有满足这些软硬件要求的设备才能安装,通过它的 android:required 属性设置该要求是不是必须的,程序中可以通过 PackageManager.hasSystemFeature() 方法判断.

2、动态申请权限


1. Android 6.0 (API level 23) 开始 targetSdkVersion >= 23 的应用必须在运行时动态申请权限


2. 权限请求对话框是操作系统进行管理的,应用无法也不应该干预。


3. 系统对话框描述的是权限组而不是某个具体权限


4. 调用 requestPermissions() 并不意味着系统一定会弹出权限请求对话框,也就是说不能假设调用该方法后就发生了用户交互,因为如果用户之前勾选了 “禁止后不再询问” 或者系统策略禁止应用获取权限,那么系统会直接拒绝此次权限请求,没有任何交互


5. 如果某个权限跟应用的主要功能无关,如应用中广告可能需要位置权限,用户可能很费解,此时在申请权限之前弹出对话框向用户解释为什么需要这个权限是个不错的选择。但不要在所有申请权限之前都弹出对话框解释,因为频繁地打断用户的操作或让用户进行选择容易让用户不耐烦


6. Fragment 中的 onRequestPermissionsResult() 方法只有在使用 Fragment#requestPermissions() 方法申请权限时才可能接收到回调,建议将权限放在所属 Activity 中申请和处理


7. 应用应该尽量少地申请权限,像让用户拍一张照片或者选择一张图片完全不需要相机权限和外存权限,可以通过隐式 Intent 拉起系统相机或其他应用完成,应用只需要在 onActivityResult() 回调中接收数据就行了。但是有一点一定要注意,如果你在 AndroidManifest.xml 文件中声明了相机权限,你就必须得动态申请并获得相机权限才能拉起系统相机.


 
 
// 请求通讯录权限的模板代码如下private void showContactsWithPermissionsCheck() {    if (ContextCompat.checkSelfPermission(MainActivity.this,            Manifest.permission.READ_CONTACTS)            != PackageManager.PERMISSION_GRANTED) {        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,                Manifest.permission.READ_CONTACTS)) {            // TODO: 弹框解释为什么需要这个权限. 【下一步】 -> 再次请求权限        } else {            ActivityCompat.requestPermissions(MainActivity.this,                    new String[]{Manifest.permission.READ_CONTACTS},                    RC_CONTACTS);        }    } else {        showContacts();    }}private void showContacts() {    startActivity(ContactsActivity.getIntent(MainActivity.this));}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,                                       @NonNull int[] grantResults) {    super.onRequestPermissionsResult(requestCode, permissions, grantResults);    switch (requestCode) {        case RC_CONTACTS:            if (grantResults.length > 0                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {                showContacts();            } else {                if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,                        Manifest.permission.READ_CONTACTS)) {                    // TODO: 弹框引导用户去设置页主动授予该权限. 【去设置】 -> 应用信息页                } else {                    // TODO: 弹框解释为什么需要这个权限. 【下一步】 -> 再次请求权限                }            }            break;        default:            break;    }}@Overrideprotected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {    super.onActivityResult(requestCode, resultCode, data);    if (requestCode == RC_SETTINGS) {        // TODO: 在用户主动授予权限后重新检查权限,但不要在这里进行事务提交等生命周期敏感操作    }}
private void showContactsWithPermissionsCheck() {
    if (ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.READ_CONTACTS)
            != PackageManager.PERMISSION_GRANTED) {
        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.READ_CONTACTS)) {
            // TODO: 弹框解释为什么需要这个权限. 【下一步】 -> 再次请求权限
        } else {
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.READ_CONTACTS},
                    RC_CONTACTS);
        }
    } else {
        showContacts();
    }
}
private void showContacts() {
    startActivity(ContactsActivity.getIntent(MainActivity.this));
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults)
 
{
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case RC_CONTACTS:
            if (grantResults.length > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                showContacts();
            } else {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                        Manifest.permission.READ_CONTACTS)) {
                    // TODO: 弹框引导用户去设置页主动授予该权限. 【去设置】 -> 应用信息页
                } else {
                    // TODO: 弹框解释为什么需要这个权限. 【下一步】 -> 再次请求权限
                }
            }
            break;
        default:
            break;
    }
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == RC_SETTINGS) {
        // TODO: 在用户主动授予权限后重新检查权限,但不要在这里进行事务提交等生命周期敏感操作
    }
}


注意看上述代码中TODO标识。

3、Shortcut


1. 类似于 iOS 的 3D Touch,长按启动图标弹出几个快捷入口,入口最好不要超过 4 个,像搜索、扫描二维码、发帖等应用程序最常用功能的入口被称为静态 shortcut,不会随着用户不同或随着用户使用而改变。


还有一种像从某个存档点继续游戏、任务进度等与用户相关的上下文敏感入口被称为动态 shortcut,会因用户不同或随着用户使用不断变化。还有一种在 Android 8.0 (API level 26) 及以上系统版本上像固定网页标签等用户主动固定到桌面的快捷方式被称为固定 shortcut


2. 静态 shortcut 系统可以自动备份和恢复,动态 shortcut 需要应用自己备份和恢复,固定 shortcut 的图标系统无法备份和恢复因此需要应用自己完成


3. android:shortcutId 和 android:shortcutShortLabel 属性是必须的,android:shortcutShortLabel 不能超过 10 个字符,android:shortcutLongLabel 不能超过 25 个字符,android:icon 不能包含 tint


4. 获取 ShortcutManager 的方式有两个: getSystemService(ShortcutManager.class) 和 getSystemService(Context.SHORTCUT_SERVICE)


5. 创建固定 shortcut:


 
 
ShortcutManager mShortcutManager =        context.getSystemService(ShortcutManager.class);if (mShortcutManager.isRequestPinShortcutSupported()) {    ShortcutInfo pinShortcutInfo =            new ShortcutInfo.Builder(context, "my-shortcut").build();    Intent pinnedShortcutCallbackIntent =            mShortcutManager.createShortcutResultIntent(pinShortcutInfo);    PendingIntent successCallback = PendingIntent.getBroadcast(context, 0,            pinnedShortcutCallbackIntent, 0);    mShortcutManager.requestPinShortcut(pinShortcutInfo,            successCallback.getIntentSender());}class);
if (mShortcutManager.isRequestPinShortcutSupported()) {
    ShortcutInfo pinShortcutInfo =
            new ShortcutInfo.Builder(context, "my-shortcut").build();
    Intent pinnedShortcutCallbackIntent =
            mShortcutManager.createShortcutResultIntent(pinShortcutInfo);
    PendingIntent successCallback = PendingIntent.getBroadcast(context, 0,
            pinnedShortcutCallbackIntent, 0);
    mShortcutManager.requestPinShortcut(pinShortcutInfo,
            successCallback.getIntentSender());
}

4、系统栏适配


1. Android 4.1 (API level 16) 开始可以通过 setSystemUiVisibility() 方法在各个 view 层次中(一般是在 DecorView 中)配置 UI flag 实现系统栏(状态栏、导航栏统称)配置,最终汇总体现到 window 级


2. View.SYSTEM_UI_FLAG_FULLSCREEN 可以隐藏状态栏,View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 可以隐藏导航栏。但是: 用户的任何交互包括触摸屏幕都会导致 flag 被清除进而系统栏保持可见,一旦离开当前 Activity flag 就会被清除,所以如果在 onCreate() 方法中设置了这个 flag 那么按 HOME 键再回来状态栏又保持可见了。


非要这样设置的话一般要放在 onResume()  或 onWindowFocusChanged() 方法中,而且这样设置只有在目标 View 可见时才会生效,状态栏/导航栏的显示隐藏会导致显示内容的大小尺寸跟着变化。



3. View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 可以让内容显示在状态栏后面,View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 可以让内容显示在导航栏后面,这样无论系统栏显示还是隐藏内容都不会跟着变化。


但不要让可交互的内容出现在系统栏区域内,通过将 android:fitsSystemWindows 属性设置为 true 可以让父容器调整 padding 以便为系统栏留出空间,如果想自定义这个 padding 可以通过覆写 View 的 fitSystemWindows(Rect insets) 方法(API level 20 以上覆写 onApplyWindowInsets(WindowInsets insets) 方法)完成


4. lean back 全屏模式: View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隐藏状态栏和导航栏,任何交互都会清除 flag 使系统栏保持可见


5. Immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隐藏状态栏和导航栏,从被隐藏的系统栏边缘向内滑动会使系统栏保持可见,应用无法响应这个手势


6. sticky immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隐藏状态栏和导航栏,从被隐藏的系统栏边缘向内滑动会使系统栏暂时可见,flag 不会被清除,且系统栏的背景是半透明的,会覆盖应用的内容,应用也可以响应这个手势,在用户没有任何交互或者没有系统栏交互几秒钟后系统栏会自动隐藏


7. 真正的沉浸式全屏体验需要 6 个 flag: 


View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | 

View.SYSTEM_UI_FLAG_LAYOUT_STABLE | 

View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 

View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | 

View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | 

View.SYSTEM_UI_FLAG_FULLSCREEN


8. 监听系统栏可见性(sticky immersive 全屏模式无法监听):


 
 
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {    @Override    public void onSystemUiVisibilityChange(int visibility) {        if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {            // TODO: The system bars are visible. Make any desired        } else {            // TODO: The system bars are NOT visible. Make any desired        }    }});
    @Override
    public void onSystemUiVisibilityChange(int visibility) {
        if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
            // TODO: The system bars are visible. Make any desired
        } else {
            // TODO: The system bars are NOT visible. Make any desired
        }
    }
});


9. 全面屏适配只需要指定支持的最大宽高比即可: 


<meta-data android:name="android.max_aspect" android:value="2.4"/>


10. Android 9 (API level 28) 开始支持刘海屏 cutout 的配置,window 的属性 layoutInDisplayCutoutMode 默认是 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,竖屏时可以渲染到刘海区,横屏时不允许渲染到刘海区。


LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 横竖屏都可以渲染到刘海区。


LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 横竖屏都不允许渲染到刘海区,可以在 values-v28/styles.xml 文件中通过 android:windowLayoutInDisplayCutoutMode 指定默认的刘海区渲染模式


11. 华为手机通过 <meta-data android:name="android.notch_support" android:value="true" /> 属性声明应用是否已经适配了刘海屏,如果没适配,那么在横屏或者竖屏不显示状态栏时会禁止渲染到刘海区,可以参考: 《华为刘海屏手机安卓O版本适配指导》。


12. 小米手机通过 <meta-data android:name="notch.config" android:value="portrait|landscape" /> 设置默认的刘海区渲染模式,开发者文档: 


小米刘海屏 Android O 适配

https://dev.mi.com/console/doc/detail?pId=1293


小米刘海屏 Android P 适配 

https://dev.mi.com/console/doc/detail?pId=1341


13. 其他手机的开发者文档有: OPPO 手机的 


OPPO凹形屏适配说明

https://open.oppomobile.com/wiki/doc#id=10159


VIVO 手机的 

异形屏应用适配指南 https://dev.vivo.com.cn/documentCenter/doc/103


锤子手机的

Smartisan 开发者文档https://resource.smartisan.com/resource/61263ed9599961d1191cc4381943b47a.pdf


14. Android 5.0 (API level 21) 开始支持通过 window 的 setStatusBarColor() 方法设置状态栏背景色,要求 window 必须添加 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的 flag 并且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 的 flag


15. Android 6.0 (API level 23) 开始可以通过 setSystemUiVisibility() 方法设置 View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR flag 兼容亮色背景的状态栏,同样要求 window 必须添加 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的 flag 并且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 的 flag


16. 小米手机在 MIUI 开发版 7.7.13 之前需要通过反射兼容亮色背景的状态栏,开发者文档:《MIUI 9 & 10“状态栏黑色字符”实现方法变更通知》

https://dev.mi.com/console/doc/detail?pId=1159


17. 魅族手机同样需要通过反射兼容亮色背景的状态栏,开发者文档: 

《状态栏变色》

http://open-wiki.flyme.cn/doc-wiki/index#id?79


5、动画


view 动画系统只能作用于 view 对象,只能改变 view 的部分样式,只是简单改变了 view 绘制,并没有改变 view 真正的位置和属性。核心类是 android.view.animation.Animation 和它的 ScaleAnimation 等子类,一般使用 AnimationUtils.loadAnimation() 方法加载。不建议使用,除非为了方便又能满足现在和将来的需求


1. 属性动画系统是一个健壮的、优雅的动画系统,可以对任意对象的属性做动画。核心类是 android.animation.Animator 的子类 ValueAnimator、ObjectAnimator、AnimatorSet


2. 通过调用 ValueAnimator 的 ofInt()、ofFloat() 等工厂方法获取 ValueAnimator 对象,通过它的 addUpdateListener() 方法可以监听动画值并在里面进行自定义操作


3. ObjectAnimator 作为 ValueAnimator 的子类可以自动地为目标对象的命名属性设置动画,但是对目标对象有严格的要求: 目标对象必须有对应属性的 setter 方法,如果在工厂方法中只提供了一个动画值那么它会作为终止值,起始值为目标对象的当前值,此时为了获取当前属性值目标对象必须有对应属性的 getter 方法。有些属性的更改不会导致 view 重新渲染,此时需要主动调用 invalidate() 方法强制触发重绘


4. AnimatorListenerAdapter 提供了 Animator.AnimatorListener 接口的空实现


5. 多数情况下可以直接使用系统提供的几个动画 duration,如 getResources().getInteger(android.R.integer.config_shortAnimTime)


6. 可以调用任意 view 对象的 animate() 方法获取 ViewPropertyAnimator 对象,链式调用这个对象的 scaleX()、alpha() 等方法可以简单方便地同时对 view 的多个属性做动画


7. 为了更好地重用和管理属性动画,最好使用 XML 文件来描述动画并放到 res/animator/ 目录下,ValueAnimator 对应 <animator> ,ObjectAnimator 对应 <objectAnimator>,AnimatorSet 对应 <set>,使用 AnimatorInflater.loadAnimator() 可以加载这些动画


8. 动态 Drawable 的实现有两种,最传统最简单的就是像电影关键帧一样依次指定关键帧和每一帧的停留时间,AnimationDrawable 对应于 XML 文件中的 <animation-list>,保存目录为 res/drawable/,AnimationDrawable 的 start() 方法可以在 onStart() 中调用。还有一种是 AnimatedVectorDrawable,需要 res/drawable/ 中的 <animated-vector> 引用 res/drawable/ 中的 <vector> 对其使用 res/animator/ 中的 <objectAnimator> 动画


9. 突然更改显示的内容会让视觉感受非常突兀不和谐,而且可能意识不到哪些内容突然变了,所以很多场景下需要使用动画过渡一下,而不是突然更改显示的内容


10. 显示隐藏 view 的常用动画有三个: crossfade 动画,card flip 动画,circular reveal 动画


11. crossfade 动画就是内容淡出另一个内容淡入交叉进行,也被称为溶入动画。实现方式为: 事先将淡入 view 的 visibility 设置为 GONE → 开始动画时将淡入 view 的 alpha 设置为 0,visibility 设置为 VISIBLE → 将淡入 view 的 alpha 动画到 1,将淡出 view 的 alpha 动画到 0 并在动画结束时将淡出 view 的 visibility 设置为 GONE


12. card flip 动画就是卡片翻转动画,需要四个动画描述: card_flip_right_in、card_flip_right_out、card_flip_left_in、card_flip_left_out


13. Android 5.0 (API level 21) 开始支持 circular reveal 圆形裁剪动画,实现方式为: 事先将 view 的 visibility 设置为 INVISIBLE → 利用 ViewAnimationUtils.createCircularReveal() 方法创建半径从 0 到 Math.hypot(cx, cy) 的圆形裁剪动画 → 将 view 的 visibility 设置为 VISIBLE 然后开启动画


14. 直线动画移动 view 只需要借助 ObjectAnimator.ofFloat() 方法动画设置 view 的 translationX 或 translationY 属性即可


15. 曲线动画移动 view 还需要借助 Android 5.0 (API level 21) 开始提供的 PathInterpolator 插值器(对应于 XML 文件中的 <pathInterpolator>),他需要个 Path 对象描述运动的贝塞尔曲线。可以使用 ObjectAnimator.ofFloat(view, "translationX", 100f) 同时设置 PathInterpolator 也可以直接设置 view 动画路径 ObjectAnimator.ofFloat(view, View.X, View.Y, path)。系统提供的 fast_out_linear_in.xml、fast_out_slow_in.xml、linear_out_slow_in.xml 三个基础的曲线插值器可以直接使用


16. 放大预览动画只需要同时动画更改目标 view 的 X,Y,SCALE_X,SCALE_Y 属性即可,不过要先计算好两个 view 最终的位置和初始缩放比


17. Android 提供了预加载的布局改变动画,可以通过 android:animateLayoutChanges="true" 属性告诉系统开启默认动画,或者通过 LayoutTransition API 设置


18. Activity 内部的布局过渡动画: 过渡动画框架可以在开始 Scene 和结束 Scene 开始过渡动画,Scene 存储着 view hierarchy 状态,包括所有 view 和其属性值,开始 Scene 可以通过 setExitAction() 定义过渡动画开始前要执行的操作,结束 Scene 可以通过 Scene.setEnterAction() 定义过渡动画完成后要执行的操作。


如果 view hierarchy 是静态不变的,可以通过布局文件描述和加载 Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this),否则可以手动创建 new Scene(mSceneRoot, mViewHierarchy)。Transition 的内置子类包括 AutoTransition、Fade、ChangeBounds,可以在 res/transition/ 目录下定义内置的 <fade xmlns:android="http://schemas.android.com/apk/res/android" />,多个组合包裹在 <transitionSet> 标签中,然后使用 TransitionInflater.from(this).inflateTransition(R.transition.fade_transition) 加载。


还可以手动创建 new Fade()。开始过渡动画时只需要执行 TransitionManager.go(mEndingScene, mFadeTransition) 即可。默认是对 Scene 中所有的 view 作动画,可以通过 addTarget() 或 removeTarget() 在开始过渡动画前进行调整。如果不想在两个 view hierarchy 间进行过渡,而是在同一个 view hierarchy 状态更改后执行过渡动画,那就不需要使用 Scene 了,先利用 TransitionManager.beginDelayedTransition(mRootView, mFade) 让系统记录 view 的更改,然后增删 view 来更改 view hierarchy 的状态,系统会在重绘 UI 时执行延迟过渡动画。


由于 SurfaceView 由非 UI 线程更新,所以它的过渡可能有问题,TextureView 在一些过渡类型上可能有问题,AdapterView 与过渡动画框架不兼容,TextView 的大小过渡动画可能有问题


19. Activity 之间的过渡动画: 需要 Android 5.0 (API level 21) ,内置的进入退出过渡动画包括: explode 从中央进入或退出,slide 从一边进入或退出,fade 透明度渐变进入或退出。


内置的共享元素过渡动画包括: changeBounds 动态更改目标 view 的边界,changeClipBounds 动态裁剪目标 view 的边界,changeTransform 动态更改目标 view 的缩放和旋转,changeImageTransform 动态更改目标 view 的缩放和尺寸。


过渡动画需要两个 Activity 都要开启 window 的内容过渡: android:windowActivityTransitions 属性设置为 true 或者代码中手动 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS) 开启。setExitTransition() 和 setSharedElementExitTransition() 方法可以为起始 Activity 设置退出过渡动画,setEnterTransition() 和 setSharedElementEnterTransition() 方法可以为目标 Activity 设置进入过渡动画。


激活目标 Activity 的时候需要携带 ActivityOptions.makeSceneTransitionAnimation(this).toBundle() 的 Bundle,返回的时候要使用 finishAfterTransition() 方法。共享元素需要使用 android:transitionName 属性或者 View.setTransitionName() 方法指定名字,多个共享元素使用 Pair.create(view1, "agreedName1") 传递信息


20. 自定义过渡动画需要继承 Transition,实现 captureStartValues() 和 captureEndValues() 方法捕获过渡的 view 属性值并告诉过渡框架,具体实现为通过 transitionValues.view 检索当前 view,通过 transitionValues.values.put(PROPNAME_BACKGROUND, view.getBackground()) 存储属性值,为了避免冲突 key 的格式必须为 package_name:transition_name:property_name。


同时还要实现 createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) 方法,框架调用这个方法的次数取决于开始和结束 scene 需要更改的元素数


21. 动画可能会影响性能,必要时可以启用 Profile GPU Rendering 进行调试


6、BroadcastReceiver 相关


1. Android 9 (API level 28) 开始 NETWORK_STATE_CHANGED_ACTION 广播不再包含 SSID,BSSID 等信息


2. Android 8.0 (API level 26) 开始限制应用静态注册一些非当前应用专属的隐式广播的 BroadcastReceiver,免除这项限制的广播包括 ACTION_LOCKED_BOOT_COMPLETED 等不太可能影响用户体验的广播


3. Android 7.0 (API level 24) 开始不能发送和接收 ACTION_NEW_PICTURE 和 ACTION_NEW_VIDEO 系统广播,可以通过 JobInfo 和 JobParameters 完成。不能静态注册 CONNECTIVITY_ACTION 广播,如果想在网络变化时调度任务可以选择使用 WorkManager,如果只在应用运行期间监听网络变化使用 ConnectivityManager 比动态注册注销 BroadcastReceiver 更优雅


4. 应该尽量在代码中动态注册注销 BroadcastReceiver


5. onReceive() 方法中不能进行复杂工作否则会导致 ANR,onReceive() 方法一旦执行完,系统可能就认为这个广播接收器已经没用了,随时会杀掉包含这个广播接收器的进程,包括这个进程启动的线程。使用 goAsync() 方法可以在 PendingResult#finish() 方法执行前为广播接收器的存活争取更多的时间,但最好还是使用 JobScheduler 等方式进行长时间处理工作


6. 使用 sendBroadcast() 方法发的广播属于常规广播,所有能接收这个广播的广播接收器接收到广播的顺序是不可控的


7. 使用 sendOrderedBroadcast() 方法发的广播属于有序广播,根据广播接收器的优先级一个接一个地传递这条广播,相同优先级的顺序不可控,广播接收器可以选择继续传递给下一个,也可以选择直接丢掉


8. 使用 LocalBroadcastManager.getInstance(this).sendBroadcast() 方法发的广播属于应用进程内的本地广播,这样的广播只有应用自己知道,比系统级的全局广播更安全更有效率


9. 为了保证广播的 action 全局唯一,action 的名字最好使用应用的包名作为前缀,最好声明成静态字符串常量.


7、数据存储与共享


分享文件


为了安全地共享文件,分享的文件必须通过 content URI 表示,必须授予这个 content URI 临时访问权限。FileProvider 作为 ContentProvider 的特殊子类,它的 getUriForFile() 静态方法可以为文件生成 content URI。


 
 
<provider    android:name="android.support.v4.content.FileProvider"    android:authorities="com.example.myapp.fileprovider"    android:grantUriPermissions="true"    android:exported="false">    <meta-data        android:name="android.support.FILE_PROVIDER_PATHS"        android:resource="@xml/filepaths" /></provider>
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/filepaths" />

</provider>


 
 
<paths>    <files-path path="images/" name="myimages" /></paths>
    <files-path path="images/" name="myimages" />
</paths>

android:authorities 属性一般是以当前应用包名为前缀的字符串,用来标志数据的所有者,多个的话用分号隔开


  1. <files-path/> 代表 getFilesDir()

  2. <cache-path/> 代表 getCacheDir()

  3. <external-path/> 代表 Environment.getExternalStorageDirectory()

  4. <external-files-path> 代表 getExternalFilesDir(null)

  5. <external-cache-path> 代表 getExternalCacheDir()

  6. <external-media-path> 代表 getExternalMediaDirs()

 
 


 
 
File imagePath = new File(getFilesDir(), "images");File newFile = new File(imagePath, "default_image.jpg");Uri contentUri = FileProvider.getUriForFile(getContext(), "com.example.myapp.fileprovider", newFile);"images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = FileProvider.getUriForFile(getContext(), "com.example.myapp.fileprovider", newFile);


1. 给 Intent 添加 FLAG_GRANT_READ_URI_PERMISSION 或 FLAG_GRANT_WRITE_URI_PERMISSION 的 flag 授予对这个 content URI 的临时访问权限,该权限会被目标 Activity 所在应用的其它组件继承,会在所在的任务结束时自动撤销授权


2. 调用 Context.grantUriPermission(package, Uri, mode_flags) 方法也可以授予 FLAG_GRANT_READ_URI_PERMISSION 或 FLAG_GRANT_WRITE_URI_PERMISSION 权限,但只有在主动调用 revokeUriPermission() 方法后或者重启系统后才会撤销授权


 
 
List<ResolveInfo> activities = getPackageManager().queryIntentActivities(intent,        PackageManager.MATCH_DEFAULT_ONLY);if (activities.size() > 0) {    for (ResolveInfo resolveInfo : activities) {        grantUriPermission(resolveInfo.activityInfo.packageName,                outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);    }}...revokeUriPermission(outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        PackageManager.MATCH_DEFAULT_ONLY);
if (activities.size() > 0) {
    for (ResolveInfo resolveInfo : activities) {
        grantUriPermission(resolveInfo.activityInfo.packageName,
                outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    }
}
...
revokeUriPermission(outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);


ContentProvider


1. ContentProvider 的数据形式和关系型数据库的表格数据类似,因此 API 也像数据库一样包含增删改查(CRUD)操作,但为了更好地组织管理一个或多个 ContentProvider,最好通过 ContentResolver 操作 ContentProvider


2. 对于 ContentProvider 的增删改查操作,不能直接在 UI 线程上执行


3. Uri 和 ContentUris 类的静态方法可以方便地构造 content URI

 
 


 
 
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;mCursor = getContentResolver().query(        UserDictionary.Words.CONTENT_URI,        mProjection,        mSelectionClause,        mSelectionArgs,        mSortOrder);FROM words WHERE word = <userinput> ORDER BY word ASC;

mCursor = getContentResolver().query(
        UserDictionary.Words.CONTENT_URI,
        mProjection,
        mSelectionClause,
        mSelectionArgs,
        mSortOrder);


3. 为了防止 SQL 注入,禁止拼接 SQL 语句,如 mSelectionClause 不能直接包含 selectionArgs 参数值


4. ContentProvider 所在应用本身的组件可以随便访问它,不需要授权


5. 如果 ContentProvider 的应用不指定任何权限,那么其它应用就无法访问这个 ContentProvider 的数据


6. 使用者需要事先通过 <uses-permission> 标签获取访问权限


7. 创建 ContentProvider 需要继承 ContentProvider 并实现增删改查等一系列方法: onCreate() 在系统创建 provider 后马上调用,可以在这里创建数据库,但不要在这里做耗时操作。getType() 返回 content URI 的 MIME 类型。query()、insert()、update()、delete() 进行增删改查。除了 onCreate() 方法其它方法必须要保证是线程安全的。


8、Notification 相关


1. Android 5.0 (API level 21) 开始通知可以出现在锁屏页面


2. Android 7.0 (API level 24) 开始可以在通知中直接输入文本或执行一些自定义操作,如直接回复按钮


3. Android 8.0 (API level 26) 开始所有的通知必须属于一个 channel,channel 被用户看作是 categories,即通知类别,用户通过通知类别来精确管理各个应用或一个应用内的通知。


一个应用可以有多个通知类别,如私信类别、好友请求类别、应用更新类别等等。可以给每个通知类别指定通知的 importance,即重要程度,Urgent(紧急)会发出提示音并在屏幕上弹出通知,High(高)会发出提示音,Medium(中)不发出提示音,Low(低)不发出提示音并且不会出现在状态栏中。


在 Android 8.0 (API level 26) 以下的系统中通知的重要程度表现为 priority,即优先级。


对应关系分别为: IMPORTANCE_HIGH 对应 PRIORITY_HIGH 或 PRIORITY_MAX,IMPORTANCE_DEFAULT 对应 PRIORITY_DEFAULT,IMPORTANCE_LOW 对应 PRIORITY_LOW,IMPORTANCE_MIN 对应 PRIORITY_MIN。在应用启动时可以执行下面的代码创建通知类别,可以无副作用地多次执行


 
 
private void createNotificationChannel() {    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {        CharSequence name = getString(R.string.channel_name);        String description = getString(R.string.channel_description);        int importance = NotificationManager.IMPORTANCE_DEFAULT;        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);        channel.setDescription(description);        NotificationManager notificationManager = getSystemService(NotificationManager.class);        notificationManager.createNotificationChannel(channel);    }}
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        CharSequence name = getString(R.string.channel_name);
        String description = getString(R.string.channel_description);
        int importance = NotificationManager.IMPORTANCE_DEFAULT;
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
        channel.setDescription(description);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }
}


4. 通过 NotificationChannel 的 enableLights(),setLightColor() 等方法可以指定该通知类别默认的通知行为,但是一旦创建了应用就不能再对它做任何更改了,只有用户自己可以更改设置。可以通过 Intent 引导用户跳转至对应设置页


 
 
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());intent.putExtra(Settings.EXTRA_CHANNEL_ID, myNotificationChannel.getId());startActivity(intent);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, myNotificationChannel.getId());
startActivity(intent);


5. 查询用户当前的通知类别的设置可以通过 getNotificationChannel()、getNotificationChannels()、getVibrationPattern()、getImportance() 等方法获取


6. 使用 deleteNotificationChannel(id) 可以删除通知类别,但是在开发模式下可能需要重装应用或者清除数据才会完全删除


7. 通知类别也可以分组


 
 
// The id of the group.String groupId = "my_group_01";// The user-visible name of the group.CharSequence groupName = getString(R.string.group_name);NotificationManager mNotificationManager =        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);mNotificationManager.createNotificationChannelGroup(new NotificationChannelGroup(groupId, groupName));
String groupId = "my_group_01";
// The user-visible name of the group.
CharSequence groupName = getString(R.string.group_name);
NotificationManager mNotificationManager =
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannelGroup(new NotificationChannelGroup(groupId, groupName));


8. Android 5.0 (API level 21) 开始支持勿扰模式(Do Not Disturb)以禁止任何通知产生的声音和震动。Total silence(完全阻止)会阻止包括闹钟视频游戏在内的所有声音和震动,Alarms only(仅限闹钟)会阻止除了闹钟外的所有声音和震动,Priority only(自订)可以定制要屏蔽的信息通话等系统范围内的通知。setCategory() 方法可以设置所属的系统范围的勿扰类别


9. 每个通知类别可以选择是否覆盖勿扰模式的设置,当勿扰模式设置为“仅限优先事项”时,可以允许继续接收此类通知


10. Android 8.1 (API level 27) 开始每秒最多播放一次通知提示音,如果一秒内有多个通知那么只播放一秒内的第一个通知提示音,如果一秒内多次频繁更新一个通知,那么系统可能会丢弃一些通知更新


11. 最好使用 NotificationCompat 和 NotificationManagerCompat 等兼容库中的类以便方便地适配低版本系统


12. setSmallIcon() 方法可以设置小图标,应用名和时间是由系统设置的,setLargeIcon() 方法可以设置右边大图标,setContentTitle() 和 setContentText() 方法可以设置通知的标题和内容,setPriority() 方法可以为 Android 8.0 (API level 26) 以下的系统设置通知优先级。系统范围的预定义通知类别包括 NotificationCompat.CATEGORY_ALARM,NotificationCompat.CATEGORY_REMINDER 等类别,这个类别在勿扰模式中有用,可以通过 setCategory() 方法指定所属的系统范围通知类别


13. 默认的通知内容会收缩成一行,可以通过 setStyle() 方法设置其他可展开的通知样式,


 
 
.setStyle(new NotificationCompat.BigTextStyle().bigText(emailObject.getSubjectAndSnippet())) new NotificationCompat.BigTextStyle().bigText(emailObject.getSubjectAndSnippet())) 


可以设置大文本块样式。


 
 
.setStyle(new NotificationCompat.InboxStyle().addLine(messageSnippet1) new NotificationCompat.InboxStyle().addLine(messageSnippet1


可以设置多行的 inbox 样式。


 
 
.setStyle(new NotificationCompat.MessagingStyle(resources.getString(R.string.reply_name)).addMessage(message1)) new NotificationCompat.MessagingStyle(resources.getString(R.string.reply_name)).addMessage(message1)) 


可以设置消息样式,但是此样式会忽略 setContentTitle() 和 setContentText() 方法的设置,但可以通过 setConversationTitle() 方法设置该聊天所属的群组名。


 
 
setStyle(new android.support.v4.media.app.Notification.MediaStyle().setShowActionsInCompactView(1).setMediaSession(mMediaSession.getSessionToken())) new android.support.v4.media.app.Notification.MediaStyle().setShowActionsInCompactView(1).setMediaSession(mMediaSession.getSessionToken())) 


可以设置媒体样式的通知,属于 CATEGORY_TRANSPORT 类别。



14. 通知的点击事件可以通过 setContentIntent() 方法设置 PendingIntent 对象完成,setAutoCancel(true) 可以在点击后自动移除通知


 
 
Intent intent = new Intent(this, AlertDetails.class);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)        .setSmallIcon(R.drawable.notification_icon)        .setContentTitle("My notification")        .setContentText("Hello World!")        .setLargeIcon(myBitmap)        .setStyle(new NotificationCompat.BigPictureStyle()                .bigPicture(myBitmap)                .bigLargeIcon(null))        .setPriority(NotificationCompat.PRIORITY_DEFAULT)        .setContentIntent(pendingIntent)        .setAutoCancel(true);this, AlertDetails.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this0, intent, 0);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.drawable.notification_icon)
        .setContentTitle("My notification")
        .setContentText("Hello World!")
        .setLargeIcon(myBitmap)
        .setStyle(new NotificationCompat.BigPictureStyle()
                .bigPicture(myBitmap)
                .bigLargeIcon(null))
        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
        .setContentIntent(pendingIntent)
        .setAutoCancel(true);


15. 通过 NotificationManagerCompat#notify() 方法可以显示通知,你需要定义一个唯一的 int 值的 ID 作为这个通知的 ID,保存这个 ID 以便之后更新或移除这个通知:


 
 
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);notificationManager.notify(notificationId, mBuilder.build());this);
notificationManager.notify(notificationId, mBuilder.build());


16. setVisibility() 方法可以设置锁屏时的通知显示策略,VISIBILITY_PUBLIC(显示所有通知)表示完整地显示通知内容,VISIBILITY_SECRET(完全不显示内容)表示不显示通知的任何信息,VISIBILITY_PRIVATE(隐藏敏感通知内容)表示只显示图标标题等基本信息


17. NotificationManagerCompat#notify() 方法传递之前的通知 ID 可以更新之前的通知,调用 setOnlyAlertOnce() 方法以便只在第一次出现通知时提示用户


18. 用户可以主动清除通知,创建通知时调用 setAutoCancel() 方法可以在用户点击通知后清除通知,创建通知时调用 setTimeoutAfter() 方法可以在超时后由系统自动清除通知,可以随时调用 cancel() 或 cancelAll() 方法清除之前的通知


19. 点击通知后启动的 Activity 分为两种,一种是应用的正常用户体验流中的常规 Activity,拥有任务完整的返回栈。还有一种是仅仅用来展示通知的详细内容的特殊Activity,它不需要返回栈。


对于常规 Activity 需要先通过 android:parentActivityName 属性或者 android.support.PARENT_ACTIVITY 的 <meta-data> 标签指定层级关系,然后:


 
 
Intent resultIntent = new Intent(this, ResultActivity.class);TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);stackBuilder.addNextIntentWithParentStack(resultIntent);PendingIntent resultPendingIntent =        stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
PendingIntent resultPendingIntent =
        stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);


对于特殊 Activity 需要先指定 android:taskAffinity="" 和 android:excludeFromRecents="true" 以避免在之前的任务中启动,然后:


 
 
Intent notifyIntent = new Intent(this, ResultActivity.class);notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);PendingIntent notifyPendingIntent = PendingIntent.getActivity(        this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT);class);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(
        this0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
);

9、后台任务


1. 每个进程都有一个主线程用来完成任务,一般主线程结束了那么意味着整个任务完成了,进程就会自动结束退出了


2. Android 应用的主线程用来进行测量绘制 UI、协调用户操作、接收生命周期事件等工作,是与用户的感知直接关联的,所以通常也被叫做 UI 线程,如果在这个线程中做太多工作,那么会导致这个线程挂起或者卡顿,导致糟糕的用户体验。所以像解码 bitmap、读写磁盘、执行网络请求等需要长时间计算和处理的操作都应该放到单独的后台线程中去做


3. 后台线程虽然是用户感觉不到的,但通常却是最消耗系统资源的,有的线程大部分时间都在占用 CPU 完成复杂的计算,我们管这种称为 CPU 密集型操作,有的线程大部分时间都在进行 I/O 的读写操作,我们管这种叫做 I/O 密集型操作。我们可以根据不同的操作类型选择不同的策略来处理以便最大化系统的吞吐量同时最小化所需代价。同时长时间运行的后台线程也加剧了电量的消耗,所以不管是操作系统还是开发者都需要 对这些后台线程的行为进行限制


4. 在创建一个后台任务之前,我们需要先要对它分析一下,它是要马上执行还是可以延迟执行?它需要系统满足指定条件才能执行吗?它需要在精确的时间点执行吗?


WorkManager


1. 通过 WorkManager 可以优雅地执行 可延迟执行的 异步任务,当应用退出后仍然可以继续执行,当满足系统条件(联网、充电、重启)时仍然可以触发任务的执行


2. 特别适合用来向后台发送日志或分析数据,或者用来周期性的与服务器同步数据


3. WorkManager 在 Android 6.0 (API level 23) 及以上系统上借助 JobScheduler 实现,在之前的系统上借助 BroadcastReceiver 和 AlarmManager 实现


4. WorkManager 可以对任务添加网络条件和充电状态等条件限制,可以调度一次性的或周期性的任务,可以监听和管理被调度的任务,可以将多个任务连在一起


5. 一次性的任务可以使用 OneTimeWorkRequest,周期性的任务使用 PeriodicTimeWorkRequest


6. 如果指定了多个限制,那么只有在所有限制都满足的情况下任务才会执行:


 
 
Constraints constraints = new Constraints.Builder()    .setRequiresDeviceIdle(true)    .setRequiresCharging(true)     .build();OneTimeWorkRequest compressionWork =                new OneTimeWorkRequest.Builder(CompressWorker.class)     .setConstraints(constraints)     .build();
    .setRequiresDeviceIdle(true)
    .setRequiresCharging(true)
     .build();
OneTimeWorkRequest compressionWork =
                new OneTimeWorkRequest.Builder(CompressWorker.class)
     .setConstraints(constraints)
     .build();


7. 任务交给系统后可能会马上被执行,可以通过 setInitialDelay(10, TimeUnit.MINUTES) 设置一个最小延时


8. 如果需要重试任务可以在 Worker 中使用 Result.retry() 完成,采用的补偿策略默认是 EXPONENTIAL 指数级的,可以使用 setBackoffCriteria() 方法调整策略


9. 可以通过 setInputData() 方法为任务设置输入数据,在 Worker 中可以通过 getInputData() 方法获取到输入数据,Result.success() 和 Result.failure() 可以携带输出数据。数据应该尽可能的简单,不能超过 10KB


10. addTag 方法可以给任务打 Tag,然后就可以使用 WorkManager.cancelAllWorkByTag(String) 和 WorkManager.getWorkInfosByTagLiveData(String) 等方法方便操作任务了


11. 如果一个任务的先决任务没有完成那么会被认为是 BLOCKED 态


12. 如果任务的限制和定时满足要求那么会被认为是 ENQUEUED 态


13. 如果任务正在执行那么会被认为是 RUNNING 态


14. 如果任务返回了 Result.success() 那么会被认为是 SUCCEEDED 态,这是最终态,只有 OneTimeWorkRequest 可能进入这个状态


15. 如果任务返回了 Result.failure() 那么会被认为是 FAILED 态,这是最终态,只有 OneTimeWorkRequest 可能进入这个状态,所有相关的任务也会被标记为 FAILED 且不会被执行


16. 显式取消一个没终止的 WorkRequest 会被认为是 CANCELLED 态,所有相关的任务也会被标记为 CANCELLED 且不会被执行


17. WorkManager.getWorkInfoById(UUID) 和 WorkManager.getWorkInfoByIdLiveData(UUID) 等方法可以定位想要的任务进行观察


18. 可以将任务连在一起:


 
 
WorkManager.getInstance()    .beginWith(Arrays.asList(filter1, filter2, filter3))    .then(compress)    .then(upload)    .enqueue();.getInstance()
    .beginWith(Arrays.asList(filter1filter2filter3))
    .then(compress)
    .then(upload)
    .enqueue();


Foreground service


对于用户触发的必须马上执行且必须执行完的后台任务,需要使用 Foreground services 实现,它既告诉系统这个应用正在执行重要的任务不能被杀掉,又通过通知栏告诉用户有后台工作正在执行.


AlarmManager


如果任务需要在精确的时间点执行,可以使用 AlarmManager


DownloadManager


如果需要执行一个长时间的 HTTP 下载任务,可以使用 DownloadManager。


DownloadManager 独立于应用之外,可以在下载失败、更改网络连接、系统重启后进行重试


 
 
public static long downloadApk(String url, String title, String desc) {    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));    request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI)            .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)            .setTitle(title)            .setDescription(desc)            .setDestinationInExternalFilesDir(MyApplication.getInstance(), null, "apks")            .allowScanningByMediaScanner();    DownloadManager downloadManager = (DownloadManager) MyApplication.getInstance().getSystemService(Context.DOWNLOAD_SERVICE);    return downloadManager.enqueue(request);}String url, String title, String desc) {
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
    request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI)
            .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
            .setTitle(title)
            .setDescription(desc)
            .setDestinationInExternalFilesDir(MyApplication.getInstance(), null"apks")
            .allowScanningByMediaScanner();
    DownloadManager downloadManager = (DownloadManager) MyApplication.getInstance().getSystemService(Context.DOWNLOAD_SERVICE);
    return downloadManager.enqueue(request);
}

更多学习和讨论,欢迎加入我们的知识星球,这里有1000+小伙伴,让你的学习不寂寞~·

看完本文有收获?请转发分享给更多人


我们的知识星球第三期开期了,已达到1100人了,能连续做三期已很不容易了,有很多老用户续期,目前续期率达到50%,说明了大家对我们的知识星球还是很认可的,欢迎大家加入尽早我们的知识星球,更多星球信息参见:

欢迎加入Java和Android架构社群

如何进阶成为Java的Android版和架构师?

说两件事

640?wx_fmt=jpeg

微信扫描或者点击上方二维码领取的Android \ Python的\ AI \的Java等高级进阶资源

更多学习资料点击下面的“阅读原文 ”获取

640?wx_fmt=gif

谢谢老板,点个好看↓

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值