2020日常问题

1,238 阅读56分钟

-1.灵感

动效
  1. 十大酷炫的转场特效

0.技术大神

没看完的文章
  1. 标题
  2. 标题
  3. 标题
  4. 标题
  5. 标题
  6. 标题
  7. 标题
  8. 标题
  9. 标题
  10. 标题
  11. 标题
  12. 标题
  13. 标题
  14. 标题
  15. 标题
  16. 标题
  17. 标题
  18. 标题
  19. 标题
  20. 标题
  21. 标题
  22. 标题
  23. 标题
  24. 标题
  25. 标题
  26. 标题
  27. 标题
  28. 标题
  29. 标题
  30. 标题
  31. 标题
  32. 标题
  33. 标题
  34. 标题
  35. 把断言(Assert)用的淋漓精致,提高代码的健壮性
  36. Android自定义View实现边缘阴影效果
  37. 开源 | AabResGuard:AAB资源混淆工具
  38. SketchyComponent
  39. 这是你从未见过的组件库--Android上的手绘风格组件
  40. Bitmap优化详谈
  41. 你一定不知道的链式调用新姿势
  42. Android 自定义优雅的BezierSeekBar
陈文管
  1. Android 增量更新全解
  2. 美图手机音乐Widget动画实现
  3. Android开源框架指南
  4. adb shell 指令手册
  5. Toast 自定义布局重复添加异常分析
  6. Android Home键之后后台启动Activity延迟5秒
  7. Android 子线程更新UI详解
  8. Android ANR详解
paincker
  1. Android利用ObjectAnimator实现3D翻转动画
  2. ScrollView滚动事件和滚动状态(开始、停止)的监听实现
  3. 你可能不知道的Android Studio/IDEA使用技巧
  4. Android Bitmap操作内存问题总结(图片处理、截屏等)
  5. Gson源码设计学习
  6. Gson TypeAdapter使用技巧几例:数据免判空、解析后校验、预处理
  7. Android Lint:自定义Lint调试与开发
  8. Android事件分发知识点整理
  9. Android ConstraintLayout实例学习(含源码)
  10. 一种基于动态代理实现的Android/Java总线通信组件
  11. 基于AOP思想的Android方法耗时开源分析工具TimeTracer
  12. SparseArray源码分析与性能优化
  13. Java引用类型简介与应用
  14. Android滚动组件图片加载优化与滚动速度的精确监听
  15. Android性能优化流程与思路
liaohuqiu
  1. android-UCToast
  2. Android 中的 Enum 到底占多少内存?该如何用?

1.SVG

www.jianshu.com/p/0555b8c1d…
www.jianshu.com/p/5524f58d2…

ImageView iv = (ImageView) findViewById(R.id.iv);
iv.setImageResource(R.drawable.vec);
iv.setBackgroundResource(R.drawable.vec)

SVG转Bitmap

android {
    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
 private static Bitmap getBitmap(VectorDrawable vectorDrawable) {
    Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
            vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
    vectorDrawable.draw(canvas);
    Log.e(TAG, "getBitmap: 1");
    return bitmap;
}
private static Bitmap getBitmap(Context context, int drawableId) {
    Log.e(TAG, "getBitmap: 2");
    Drawable drawable = ContextCompat.getDrawable(context, drawableId);
    if (drawable instanceof BitmapDrawable) {
        return BitmapFactory.decodeResource(context.getResources(), drawableId);
    } else if (drawable instanceof VectorDrawable) {
        return getBitmap((VectorDrawable) drawable);
    } else {
        throw new IllegalArgumentException("unsupported drawable type");
    }
}
Bitmap bitmap = getBitmap(getContext(), R.drawable.ic_airport);

2.Android加载SVG实现交互式地图绘制

www.csdn.net/gather_2f/M…

3.CollapsingToolbarLayout 坍缩之后的高度可以通过android:minHeight=""属性来设置

CollapsingToolbarLayout 坍缩之后的高度可以通过android:minHeight=""属性来设置.
www.jianshu.com/p/c5fe0c025…

4.ConstraintLayout

4.1 bias
blog.csdn.net/truechenshi…
bias=子View左相关的长度/(子View左相关的长度+其右相关的长度),默认值为0.5
居中情况下,bias的默认值为0.5,取值范围是0~1,以子View的left或top为始起边界.

android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.80"
app:layout_constraintWidth_percent="0.50"

View宽度为parent的0.5,左边距为parent的0.4倍,右边距为parent的0.1倍.
(1-0.50)*0.80 = 0.40;
1 - 0.50 - 0.40 = 0.10;
4.2 Guideline
ConstraintLayout中Chains和Guideline的使用

  • Guideline是辅助线,android:orientation属性来确定横向还是纵向.
    • vertical: Guideline的宽度为0,高度是parent也就是ConstraintLayout的高度
    • horizontal: 高度为0,宽度是parent的宽度.Guideline不会显示,默认是GONE.
      默认不显示原因:
      public static final int GONE = 0x00000008;
      public Guideline(Context context, AttributeSet attrs) {
          super(context, attrs);
          super.setVisibility(8);
      }
      
  • layout_constraintGuide_begin: 距离左侧或顶部的固定距离.
  • layout_constraintGuide_end: 距离右侧或底部的固定距离.
  • layout_constraintGuide_percent: 位于父控件中的宽度或高度百分比的位置,如0.8 代表 80%宽/高位置.

4.SeekBar

  • 解决thumb和背景分隔
<!-- 是否设置一个间隙,让滑块与底部图片分隔 -->
android:splitTrack="false"
  • 修改SeekBar进度条高度.注意,此高度不是SeekBar整体高度.
    • 设置android:maxHeight和android:minHeight.一般者两个值一致.代表进度条最大最小高度.
android:layout_height="24dp"
android:maxHeight="2dp"
android:minHeight="2dp"

5.ProgressBar

6.Bundle遍历

Intent intent = getIntent();
Bundle bundle = intent.getExtras();
Set<String> keySet = bundle.keySet();
for(String key : keySet){
    Object val = bundle.get(key);
    Log.i("BundleInfo" , "key:"+key+";value:"+val);
}

7.Broadcast 和 BroadcastReceiver的权限限制

blog.csdn.net/mafei852213…

8.Git

  • push可能出现error:
    error: unpack failed: error Missing tree ABCD****
    使用: --no-thin git push origin HEAD:refs/for/分支名称 --no-thin www.cnblogs.com/ayseeing/p/…
    git push --no-thin origin dev
    

9.跨进程

  • 跨进程调用传输的实例,其Bean要实现 Parcelable 接口. 并且用到的字段,都要在如下方法中赋值:
    public class Person implements Parcelable {
        public String name;
        public Long age = -1L;
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(this.name);
            dest.writeLong(this.age);
        }
    
        protected Song(Parcel in) {
            this.name = in.readString();
            this.age = in.readLong();
        }
    
        ****
    }
    
    否则将Person实例从进程A传输到进程B,B会发现收到的Person实例的属性都是默认值.例如age都是-1.

10.android studio中搜索第三方库插件

1.android studio 中的setting >>Plugins  >> 点击中下方的browse repositories
2.搜索框中搜索 "ok,gradle" ,点击install并重启
3.重启完成后,再次打开setting >>key map ,在右上方的搜索框中搜索 ok,gradle!,右键添加键盘的快捷键
4.使用刚刚定义的快捷键,唤醒插件就能快速的搜索三方库

11.Class

  • Class clazz.isInterface()
    clazz类是不是一个接口
    

12.Method

  • Method func.getDeclaringClass()
    func方法是在哪个类里面声明的.则将该类的Class返回
    public Class<?> getDeclaringClass() {
        ****
    }
    

13.Java8默认方法

blog.csdn.net/u010003835/…

  1. 默认方法: 在interface/接口中,方法签名前加上了default关键字的的实现方法.
    public interface defaultInterface{
        default void t1(){
            System.out.println("接口中默认方法");
        }
    }
    public class defaultClass implements defaultInterface{}
    defaultClass item = new defaultClass();
    item.t1();
    打印: 接口中默认方法
    
  2. 从Java8开始,接口引入了默认方法
  3. 为什么引入默认方法
    • 因为接口和实现类之间的耦合太严重,向接口中添加1个方法,需要在所有实现类中修改.引入默认方法,可以自由扩充接口中方法.

14.Java动态代理

a.codekk.com/detail/Andr…

15.Java Type

blog.csdn.net/lkforce/art…
www.jianshu.com/p/0f3eda48d…

  • Type 是 Java 编程语言中所有类型的公共高级接口,用于描述java中用到的所有类型.它们包括原始类型、泛型、泛型数组、类型变量和基本类型.
  • Type接口包含了一个实现类(Class)和四个实现接口(TypeVariable, ParameterizedType, GenericArrayType, WildcardType)
  • Class:普通Java类,数组,自定义类,8中基本类型:byte,short,int,long,float,double,char,boolean
  • ParameterizedType:泛型.如List,Map等.
    //获取泛型的Type类型数组
    Type[] getActualTypeArguments();
    
  • GenericArrayType:泛型数组.如List[],Map[]
    //获得泛型数组中item的Type
    Type getGenericComponentType();
    
  • WildcardType:当需要描述的类型是泛型类,而且泛型类中的泛型被定义为(? extends xxx)或者(? super xxx)这种类型,比如List<? extends TestReflect>,这个类型首先将由ParameterizedType实现,当调用ParameterizedType的getActualTypeArguments()方法后得到的Type就由WildcardType实现.
    //List<? extentds XXX>
    getUpperBounds:types[0]是XXX类型
    getLowerBounds:types是1个空数组
    //List<? super XXX>
    getUpperBounds:types[0]是Object类型
    getLowerBounds:types[0]是XXX类型
    
    //获取泛型上边界
    Type[] getUpperBounds();
    //获取泛型下边界
    Type[] getLowerBounds();
    
  • TypeVariable:类型变量.即泛型中的变量;例如:T、K、V等变量,可以表示任何类.这种实现形式是在泛型类中使用的
public class TestType<T> {
    public void test(TestType p0,
                     List<TestType> p1,
                     Map<String, TestType> p2,
                     List<String>[] p3,
                     Map<String, TestType>[] p4,
                     List<? extends TestType> p5,
                     Map<? extends TestType, ? super TestType> p6,
                     T p7
    ){}

    @RequiresApi(api = Build.VERSION_CODES.P)
    public static void main(String[] args) {

        Method[] methods= TestType.class.getMethods();

        for(int i=0;i<methods.length;i++){
            Method oneMethod=methods[i];

            if(oneMethod.getName().equals("test")){
                Type[] types=oneMethod.getGenericParameterTypes();

                //第一个参数,TestType p0
                Class type0=(Class)types[0];
                System.out.println("type0:"+type0.getName());

                //第二个参数,List<TestType> p1
                Type type1=types[1];
                Type[] parameterizedType1=((ParameterizedType)type1).getActualTypeArguments();
                Class parameterizedType1_0=(Class)parameterizedType1[0];
                System.out.println("parameterizedType1_0:"+parameterizedType1_0.getName());

                //第三个参数,Map<String,TestType> p2
                Type type2=types[2];
                Type[] parameterizedType2=((ParameterizedType)type2).getActualTypeArguments();
                Class parameterizedType2_0=(Class)parameterizedType2[0];
                System.out.println("parameterizedType2_0:"+parameterizedType2_0.getName());
                Class parameterizedType2_1=(Class)parameterizedType2[1];
                System.out.println("parameterizedType2_1:"+parameterizedType2_1.getName());


                //第四个参数,List<String>[] p3
                Type type3=types[3];
                Type genericArrayType3=((GenericArrayType)type3).getGenericComponentType();
                ParameterizedType parameterizedType3=(ParameterizedType)genericArrayType3;
                Type[] parameterizedType3Arr=parameterizedType3.getActualTypeArguments();
                Class class3=(Class)parameterizedType3Arr[0];
                System.out.println("class3:"+class3.getName());

                //第五个参数,Map<String,TestType>[] p4
                Type type4=types[4];
                Type genericArrayType4=((GenericArrayType)type4).getGenericComponentType();
                ParameterizedType parameterizedType4=(ParameterizedType)genericArrayType4;
                Type[] parameterizedType4Arr=parameterizedType4.getActualTypeArguments();
                Class class4_0=(Class)parameterizedType4Arr[0];
                System.out.println("class4_0:"+class4_0.getName());
                Class class4_1=(Class)parameterizedType4Arr[1];
                System.out.println("class4_1:"+class4_1.getName());


                //第六个参数,List<? extends TestType> p5
                Type type5=types[5];
                Type[] parameterizedType5=((ParameterizedType)type5).getActualTypeArguments();
                Type[] parameterizedType5_0_upper=((WildcardType)parameterizedType5[0]).getUpperBounds();
                Type[] parameterizedType5_0_lower=((WildcardType)parameterizedType5[0]).getLowerBounds();
                System.out.println("class5_0_upper:"+((Class)parameterizedType5_0_upper[0]).getName());
                System.out.println("class5_0_lower:"+parameterizedType5_0_lower.toString());

                //第七个参数,Map<? extends TestType,? super TestType> p6
                Type type6=types[6];
                Type[] parameterizedType6=((ParameterizedType)type6).getActualTypeArguments();
                Type[] parameterizedType6_0_upper=((WildcardType)parameterizedType6[0]).getUpperBounds();
                Type[] parameterizedType6_0_lower=((WildcardType)parameterizedType6[0]).getLowerBounds();
                Type[] parameterizedType6_1_upper=((WildcardType)parameterizedType6[1]).getUpperBounds();
                Type[] parameterizedType6_1_lower=((WildcardType)parameterizedType6[1]).getLowerBounds();
                System.out.println("class6_0_upper:"+((Class)parameterizedType6_0_upper[0]).getName());
                System.out.println("class6_0_lower:"+parameterizedType6_0_lower.toString());
                System.out.println("class6_1_upper:"+((Class)parameterizedType6_1_upper[0]).getName());
                System.out.println("class6_1_lower:"+((Class)parameterizedType6_1_lower[0]).getName());

                Type type7=types[7];
                TypeVariable typeVariable = (TypeVariable) type7;
                String canonicalName = typeVariable.getClass().getCanonicalName();
                String name = typeVariable.getName();
                String typeName = typeVariable.getTypeName();
                Type[] bounds = typeVariable.getBounds();
                GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
                System.out.println("class7 name:"+name+";typeName:"+typeName+";canonicalName:"+canonicalName+";genericDeclaration:"+genericDeclaration.toString());
                for(Type item : bounds){
                    System.out.println("class7 item:" + ((Class)item).getName());
                }
            }
        }
    }
}

type0:com.huanhailiuxin.project2020.hencoder.type.TestReflect
parameterizedType1_0:com.huanhailiuxin.project2020.hencoder.type.TestReflect
parameterizedType2_0:java.lang.String
parameterizedType2_1:com.huanhailiuxin.project2020.hencoder.type.TestReflect
class3:java.lang.String
class4_0:java.lang.String
class4_1:com.huanhailiuxin.project2020.hencoder.type.TestReflect
class5_0_upper:com.huanhailiuxin.project2020.hencoder.type.TestReflect
class5_0_lower:[Ljava.lang.reflect.Type;@61bbe9ba
class6_0_upper:com.huanhailiuxin.project2020.hencoder.type.TestReflect
class6_0_lower:[Ljava.lang.reflect.Type;@610455d6
class6_1_upper:java.lang.Object
class6_1_lower:com.huanhailiuxin.project2020.hencoder.type.TestReflect
class7 name:T;typeName:T;canonicalName:sun.reflect.generics.reflectiveObjects.TypeVariableImpl;genericDeclaration:class com.huanhailiuxin.project2020.hencoder.type.TestReflect
class7 item:java.lang.Object

16.Dialog

  1. Dialog引起的泄漏
    www.jianshu.com/p/3aa1a706d…
    www.cnblogs.com/zhangkefan/…
    blog.csdn.net/u010956965/…
    WindowLeaked
    
    04-30 17:33:30.634932 26458 26458 E WindowManager: android.view.WindowLeaked: Activity ***.***Activity has leaked window DecorView@6ecf636[***Activity] that was originally added here
    04-30 17:33:30.634932 26458 26458 E WindowManager: 	at android.view.ViewRootImpl.<init>(ViewRootImpl.java:634)
    04-30 17:33:30.634932 26458 26458 E WindowManager: 	at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:377)
    04-30 17:33:30.634932 26458 26458 E WindowManager: 	at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:95)
    04-30 17:33:30.634932 26458 26458 E WindowManager: 	at android.app.Dialog.show(Dialog.java:342)
    ***
    04-30 17:33:30.634932 26458 26458 E WindowManager: 	at ***.***Activity.onKeyDown(SourceFile:2729)
    
    原因:在Activity的onDestroy中,没有dismiss正在展示的Dialog.
  2. Dialog在灭屏状况下执行dismiss,亮屏后,会先展示,再消失
    安卓应用在其他应用上悬浮显示权限添加
    Android 在其他应用上悬浮显示View
    Android AlertDialog level(置顶)
    【朝花夕拾】Android安全之(一)权限篇
    1: AndroidManifest.xml 中添加权限
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    
    2: 为当前App添加该特殊权限校验,若校验不通过,跳转让用户手动开启
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (!Settings.canDrawOverlays(this)) {
            //不设置包名,则跳转到列表
            /*Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);*/
            //设置包名对应的Uri,则直接跳转到该App对应的'显示在其他应用的上层'
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivity(intent);
            return;
        }
    }
    
    3: 为Dialog的Window设置属性
    Dialog dialog = ***;
    Window window = dialog.getWindow();
    if(window != null){
        WindowManager.LayoutParams params = window.getAttributes();
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        window.setAttributes(params);
    }
    

17.Servie

  1. onTaskRemoved
    www.cocoachina.com/articles/64…
    www.imooc.com/wenda/detai…
    blog.csdn.net/johnlee175/…
    www.voidcn.com/article/p-h…
    stackoverflow.com/questions/2…
  2. AIDL 回调中的 IllegalStateException
    • java.lang.IllegalStateException:
      • "beginBroadcast() called while already in a broadcast"
      • "finishBroadcast() called outside of a broadcast"
    • 在aidl中回调注册的 callback 对象时,需要使用 beginBroadcast() 和 finishBroadcast(),必须成对使用. 官方示例:
           * int i = callbacks.beginBroadcast();
           * while (i &gt; 0) {
           *     i--;
           *     try {
           *         callbacks.getBroadcastItem(i).somethingHappened();
           *     } catch (RemoteException e) {
           *         // The RemoteCallbackList will take care of removing
           *         // the dead object for us.
           *     }
           * }
           * callbacks.finishBroadcast();</pre>
      
    • 官方示例的问题在于: callbacks.getBroadcastItem(i).somethingHappened(); 可能会触发其他异常,不仅仅是RemoteException. 当触发非RemoteException,可能导致程序异常终止,从而不能执行到 callbacks.finishBroadcast();因而需要将 RemoteException 修改为 Exception.或者添加try-catch-finally代码块,在finally中执行finishBroadcast.
  3. 超简单的Binder,AIDL和Messenger的原理及使用流程

18.切换系统导航监听

19.通话状态监听

www.iteye.com/blog/bcf-17…
www.jianshu.com/p/a362404f8…
blog.csdn.net/centor/arti…
android 通话状态监听(自定义接听挂断按钮与通话界面,根据公司的业务逻辑可以实现自己的来电秀功能)
在 Android 应用中监测来电信息
Android-9种通话状态(精确)

  1. PhoneStateListener
  2. TelephonyManager

20.adb命令

  1. adb shell修改手机分辨率
    • adb shell dumpsys window displays
    • adb shell dumpsys window visible-apps
    • adb shell wm size 1080x1920
    • adb shell wm density 480
    • //wm size reset
    • //wm density reset

21.View的位置信息

Android View的距离和位置信息

  • getLocationOnScreen()和getLocationInWindow() getLocationOnScreen()用来获取一个View在屏幕中的位置,而getLocationInWindow()用来获取一个View在其所在窗口中的位置。
  • 无论Activity是否是全屏的,getLocationInWindow()和getLocationOnScreen()的结果都是一样的。但这只是对Activity,对Dialog来说,情况却并不是这样
  • getLocationOnScreen()得到的是相对于屏幕的坐标,也就是坐标原点在屏幕的左上角.
    int[] screenLocation = new int [2];
    int[] windowLocation = new int [2];
    View v.getLocationOnScreen(windowLocation)
    //得到View v距离屏幕顶部的距离
    int y=screenLocation[1]
    

22.RecycleView

  1. RecycleView4种定位滚动方式演示

23.通知栏 Notification

  1. 取消通知栏声音及震动

24.Selector改变TextView字体颜色

1:在 res 下新建 color 文件夹,创建 text_color_selector 的selector
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/color1" android:state_selected="true" />
    <item android:color="@color/color2" android:state_selected="false" />
</selector>

2:引用
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textColor="@color/text_color_selector"
    android:textSize="18sp" />
  • 可以在res下创建color文件夹,创建对应selector,然后在TextView中引用:android:textColor="@color/text_color_selector".
  • 也可以直接在drawable文件夹下创建对应selector,然后在TextView中引用:android:textColor="@drawable/text_color_selector".
  • 经试验都有效.

25.ZXING

  1. 二维码白边
    二维码大白边一步一步修复指南
  2. 源码解析
    ZXing源码解析一:让源码跑起来

26.屏幕参数

  1. 获取屏幕宽高
    Android 获取屏幕宽度和高度的几种方法
    public int getScreenWidth(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        if (wm == null) return -1;
        Point point = new Point();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            wm.getDefaultDisplay().getRealSize(point);
        } else {
            wm.getDefaultDisplay().getSize(point);
        }
        return point.x;
    }
    public int getScreenHeight(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        if (wm == null) return -1;
        Point point = new Point();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            wm.getDefaultDisplay().getRealSize(point);
        } else {
            wm.getDefaultDisplay().getSize(point);
        }
        return point.y;
    }
    
    • 应用程序显示区域
      • 不包括状态栏之类的系统装饰.
        getSize(Point),getRectSize(Rect)和getMetrics(DisplayMetrics)。
    • 实际显示区域
      • 包含系统装饰的内容的显示部分.
        getRealSize(Point),getRealMetrics(DisplayMetrics)。
    • Resource.getSystem().getDisplayMetrics().widthPixels , Resource.getSystem().getDisplayMetrics().heightPixels 也不代表屏幕实际的像素值.
    重点在于 available
    
    /**
     * The absolute width of the available display size in pixels.
     */
    public int widthPixels;
    /**
     * The absolute height of the available display size in pixels.
     */
    public int heightPixels;
    

27.Exception

  1. NullPointerException
  2. Android未捕获异常处理机制
  3. 统一异常处理

28.Lambda

  1. Kotlin 的 Lambda 表达式,大多数人学得连皮毛都不算
  2. 8000字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理
  3. 啪啪,打脸了!领导说:try-catch必须放在循环体外!

29.开机 关机

  1. 让我们来一起监听Andrid开关机事件吧
  2. Android 无法接收开机广播的问题
  3. android开机自启广播无效果的曲线解决方案
  4. Android如何监听开机广播和关机广播

30.固定屏幕

  1. Android固定屏幕
  2. android6.0 固定屏幕功能
  3. Android 8.1 屏幕固定功能ScreenPinning须知
  4. Android6.0 屏幕固定功能详解
  5. 在固定屏幕后,如果我们启动其他TaskRecord的Activity是不能启动的
  6. 取消固定屏幕
    • Activity提供了API: stopLockTask
    • 如果是在当前Activity启动的固定屏幕,stopLockTask可以取消固定屏幕.如果不是,则调用该方法无效,设备仍将处于固定屏幕状态.
    /**
     * Stop the current task from being locked.
     *
     * <p>Called to end the LockTask or screen pinning mode started by {@link #startLockTask()}.
     * This can only be called by activities that have called {@link #startLockTask()} previously.
     *
     //如果当前设备不是在当前Activity启动的固定屏幕,该方法无效.
     //当前设备仍将处于固定屏幕状态.
     * <p><strong>Note:</strong> If the device is in LockTask mode that is not initially started
     * by this activity, then calling this method will not terminate the LockTask mode, but only
     * finish its own task. The device will remain in LockTask mode, until the activity which
     * started the LockTask mode calls this method, or until its whitelist authorization is revoked
     * by {@link DevicePolicyManager#setLockTaskPackages(ComponentName, String[])}.
     *
     * @see #startLockTask()
     * @see android.R.attr#lockTaskMode
     * @see ActivityManager#getLockTaskModeState()
     */
    public void stopLockTask() {
        try {
            ActivityManager.getService().stopLockTaskModeByToken(mToken);
        } catch (RemoteException e) {
        }
    }
    

31.Bitmap

  1. BitmapFactory.Options

32.流量节省

  1. 优化网络流量消耗
  2. 流量节省程序是否打开
    ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    
    在连接wifi后:
    connMgr.isActiveNetworkMetered() 返回 false.
    但实质上, 流量节省程序是否打开,应该直接看 connMgr.getRestrictBackgroundStatus() 的值.
    ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED:
    流量节省打开
    ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED:
    流量节省打开,但该APP位于白名单中
    ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED:
    流量节省关闭
    

33.EditText

  1. 设置EditText是否可编辑
    xml:
    android:focusable="true"
    android:focusableInTouchMode="true"
    
    java:
    editText.setFocusableInTouchMode(true);
    editText.setFocusable(true);
    
  2. TextWatcher 引起的ANR
    详解EditText输入监听TextWatcher
    getText()和getEditableText()使用比较
    Android中TextWatcher afterTextChanged无限迭代异常
    • 在afterTextChanged中对s做修改.或者调用getText(),最终会触发无线循环调用.
    @Override
    public void afterTextChanged(Editable s) {
        //正确方式是直接使用S即可.
        //s不可能为null
        int length = s.toString();
        if (length <= 0) {
            ***
        } else {
            ***
        }
    }
    
    //s不可能为null
    TextView:
    private void setText(CharSequence text, BufferType type,
                     boolean notifyBefore, int oldlen) {
        ***
        //text为null,会替换为""
        if (text == null) {
            text = "";
        }
        ***
        sendAfterTextChanged((Editable) text);
    }
    

34.轮廓 ViewOutlineProvider Outline

  1. ViewOutlineProvider轮廓裁剪(5.0以上特性)
  2. 自定义 View:用贝塞尔曲线绘制酷炫轮廓背景
  3. Android使用ViewOutlineProvider实现圆角
  4. 源码
    • 要想通过ViewOutlineProvider生成的Outline绘制阴影,需要调用Outline.setRoundRect, Outline.setOval, Outline.setConvexPath , set(@NonNull Outline src) ,改变 mMode 的默认值(MODE_EMPTY)
    • 要想通过ViewOutlineProvider生成的Outline实现View的轮廓裁剪, Outline需要最终调用 setRoundRect.
    • View的默认ViewOutlineProvider生成的的Outline可以实现轮廓裁剪.
    View.java
    //View默认的ViewOutlineProvider是ViewOutlineProvider.BACKGROUND
    ViewOutlineProvider mOutlineProvider = ViewOutlineProvider.BACKGROUND;
    /**
     * Sets the {@link ViewOutlineProvider} of the view, which generates the Outline that defines
     * the shape of the shadow it casts, and enables outline clipping.
     * <p>
     * The default ViewOutlineProvider, {@link ViewOutlineProvider#BACKGROUND}, queries the Outline
     * from the View's background drawable, via {@link Drawable#getOutline(Outline)}. Changing the
     * outline provider with this method allows this behavior to be overridden.
     * <p>
     * If the ViewOutlineProvider is null, if querying it for an outline returns false,
     * or if the produced Outline is {@link Outline#isEmpty()}, shadows will not be cast.
     * <p>
     * Only outlines that return true from {@link Outline#canClip()} may be used for clipping.
     *
     * @see #setClipToOutline(boolean)
     * @see #getClipToOutline()
     * @see #getOutlineProvider()
     */
    //ViewOutlineProvider用于View的轮廓裁切,并生成定义View阴影形状的Outline实例.
    //当生成的Outline实例的isEmpty为true,则不能为View绘制阴影.
    //当生成的Outline实例的canClip为false,则不能为View进行轮廓裁切.
    public void setOutlineProvider(ViewOutlineProvider provider) {
        mOutlineProvider = provider;
        invalidateOutline();
    }
    /**
     * Sets whether the View's Outline should be used to clip the contents of the View.
     * <p>
     * Only a single non-rectangular clip can be applied on a View at any time.
     * Circular clips from a {@link ViewAnimationUtils#createCircularReveal(View, int, int, float, float)
     * circular reveal} animation take priority over Outline clipping, and
     * child Outline clipping takes priority over Outline clipping done by a
     * parent.
     * <p>
     * Note that this flag will only be respected if the View's Outline returns true from
     * {@link Outline#canClip()}.
     *
     * @see #setOutlineProvider(ViewOutlineProvider)
     * @see #getClipToOutline()
     */
    //设置是否使用View关联的Outline来进行轮廓裁剪.
    //即使clipToOutline为true,也需要Outline.canClip()fanhuitrue,该方法才会生效.
    //即生效条件: clipToOutline==true 且 Outline最终调用过setRoundRect.
    public void setClipToOutline(boolean clipToOutline) {
        damageInParent();
        if (getClipToOutline() != clipToOutline) {
            mRenderNode.setClipToOutline(clipToOutline);
        }
    }
    
    ViewOutlineProvider.java
    //View默认的ViewOutlineProvider,最总调用了setRoundRect
    1:
    Drawable background.getOutline(outline);
    -->
    Drawable.java
    public void getOutline(@NonNull Outline outline) {
        outline.setRect(getBounds());
        outline.setAlpha(0);
    }
    -->
    Outline.java
    public void setRect(@NonNull Rect rect) {
        setRect(rect.left, rect.top, rect.right, rect.bottom);
    }
    public void setRect(int left, int top, int right, int bottom) {
        //最终调用了setRoundRect
        setRoundRect(left, top, right, bottom, 0.0f);
    }
    2:
    outline.setRect(0, 0, view.getWidth(), view.getHeight());
    -->
    setRoundRect
    public static final ViewOutlineProvider BACKGROUND = new ViewOutlineProvider() {
        @Override
        public void getOutline(View view, Outline outline) {
            Drawable background = view.getBackground();
            if (background != null) {
                background.getOutline(outline);
            } else {
                outline.setRect(0, 0, view.getWidth(), view.getHeight());
                outline.setAlpha(0.0f);
            }
        }
    };
    
    Outline.java
    @Mode
    //mMode 默认就是 MODE_EMPTY
    public int mMode = MODE_EMPTY;
    /**
     * Returns whether the Outline is empty.
     * <p>
     * Outlines are empty when constructed, or if {@link #setEmpty()} is called,
     * until a setter method is called
     *
     * @see #setEmpty()
     */
    //当 mMode 是 MODE_EMPTY.返回true.
    public boolean isEmpty() {
        return mMode == MODE_EMPTY;
    }
    /**
     * Returns whether the outline can be used to clip a View.
     * <p>
     * Currently, only Outlines that can be represented as a rectangle, circle,
     * or round rect support clipping.
     *
     * @see android.view.View#setClipToOutline(boolean)
     */
    //当 mMode 不是 MODE_CONVEX_PATH.返回true.
    public boolean canClip() {
        return mMode != MODE_CONVEX_PATH;
    }
    //看 mMode 如何变化:
    setEmpty:
    -> mMode = MODE_EMPTY;
    
    set(@NonNull Outline src):
    -> mMode = src.mMode;
    
    setRoundRect(int left, int top, int right, int bottom, float radius):
    -> mMode = MODE_ROUND_RECT;
    
    setOval(int left, int top, int right, int bottom):
    -> mMode = MODE_CONVEX_PATH;
    
    setConvexPath(@NonNull Path convexPath):
    -> mMode = MODE_CONVEX_PATH;
    

35.阴影

  1. Android materialDesign 风格阴影改变阴影颜色
  2. setOutlineAmbientShadowColor 及 setOutlineSpotShadowColor
  3. 承香墨影 聊聊 Material Design里,阴影的那些事儿!

36.Interpolator

  1. PathInterpolator , PathInterpolatorCompat
    PathInterpolatorCompat.java
    /**
     * Helper for creating path-based {@link Interpolator} instances. On API 21 or newer, the
     * platform implementation will be used and on older platforms a compatible alternative
     * implementation will be used.
     */
    public final class PathInterpolatorCompat {
        public static Interpolator create(Path path) {
            if (Build.VERSION.SDK_INT >= 21) {
                //SDK版本>=21,直接使用原始PathInterpolator
                return new PathInterpolator(path);
            }
            //SDK版本<21,则使用兼容版本
            return new PathInterpolatorApi14(path);
        }
    }
    
    • 这条 Path 描述的其实是一个 y = f(x) (0 ≤ x ≤ 1) (y 为动画完成度,x 为时间完成度)的曲线,所以同一段时间完成度上不能有两段不同的动画完成度(这个好理解吧?因为内容不能出现分身术呀),而且每一个时间完成度的点上都必须要有对应的动画完成度(因为内容不能在某段时间段内消失呀)。 HenCoder Android 自定义 View 1-6:属性动画 Property Animation(上手篇)

37.TypeEvaluator

  1. Interpolator用于设置 动画持续时间 和 动画完成度 之间的关系
  2. TypeEvaluator用于设置 动画完成度 和 指定属性值 之间的关系

38.敲一遍

  1. 自定义View练习(五)高仿小米时钟 - 使用Camera和Matrix实现3D效果
    GitHub
  2. LinearGradient与闪动文字效果
    如何移动LinearGradient: 上面我们讲了如何给文字加上渐变效果,其实让它动起来办法很简单,还记得我们说过Shader有一个setLocalMatrix(Matrix localM) 方法可以设置位置矩阵么,我们只需要给LinearGradient设置上逐渐平移的矩阵就可以了
    Shader.java
    
    /**
     * Set the shader's local matrix. Passing null will reset the shader's
     * matrix to identity. If the matrix has scale value as 0, the drawing
     * result is undefined.
     *
     * @param localM The shader's new local matrix, or null to specify identity
     */
    public void setLocalMatrix(@Nullable Matrix localM) {
        if (localM == null || localM.isIdentity()) {
            if (mLocalMatrix != null) {
                mLocalMatrix = null;
                discardNativeInstance();
            }
        } else {
            if (mLocalMatrix == null) {
                mLocalMatrix = new Matrix(localM);
                discardNativeInstance();
            } else if (!mLocalMatrix.equals(localM)) {
                mLocalMatrix.set(localM);
                discardNativeInstance();
            }
        }
    }
    
  3. ColorArcProgressBar

39.硬件加速

HenCoder Android 自定义 View 1-8 硬件加速
Google 硬件加速

  1. targetSdkVersion>=14,硬件加速默认处于启用状态.

39.自定义View

  1. 父View对子View的尺寸限制: 父View将开发者对子View的要求进行处理计算后所得到的的更精确的要求.HenCoder Android UI 2-2 全新定制 View 的尺寸

  2. 开发者对子View的要求: xml文件中子View中以 layout开头的属性.

  3. View在XML布局文件中的以layout开头的属性,不是给View自己看的,是给该View的父View看的.

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <androidx.appcompat.widget.AppCompatImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginTop="100dp"
            android:layout_marginBottom="100dp"
            android:src="@drawable/head"
            />
    

    AppCompatImageView的layout_width, layout_height, layout_marginTop, layout_marginBottom 等以 layout开头的属性,是给LinearLayout看的.LinearLayout在执行自己的onMeasure时候,会将以上属性生成 int widthMeasureSpec, int heightMeasureSpec 两个值,作为 父View对AppCompatImageView尺寸的限制 ,在调用AppCompatImageView的 measure 时候作为参数传入.最终作为AppCompatImageView的 onMeasure 的属性传入.

  4. 全新自定义View尺寸:

    1. 在onMeasure中计算尺寸
    2. 对计算得到的 width 和 height, 调用 resolveSize , 以使计算得到的尺寸满足 父View的限制.
    3. setMeasuredDimension
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          //1:selfExpectWidth , selfExceptHeight :自定义View计算自身期望的尺寸
          int selfExpectWidth = 200;
          int selfExceptHeight = 200;
          //2:调用 resolveSize , 对自定义View自身期望的尺寸 进行调整,以符合 '父 View 的限制'
          selfExpectWidth = resolveSize(selfExpectWidth, widthMeasureSpec);
          selfExceptHeight = resolveSize(selfExpectWidth, heightMeasureSpec);
          //3:调用setMeasuredDimension保存尺寸
          setMeasuredDimension(selfExpectWidth, selfExceptHeight);
      }
      
    4. 代码实例
      public class Layout2View extends View {
          ***
          @Override
          protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
              //1:selfExpectWidth , selfExceptHeight :自定义View计算自身期望的尺寸
              int selfExpectWidth = 200;
              int selfExceptHeight = 200;
              //2:调用 resolveSize , 对自定义View自身期望的尺寸 进行调整,以符合 '父 View 的限制'
              selfExpectWidth = resolveSize(selfExpectWidth, widthMeasureSpec);
              selfExceptHeight = resolveSize(selfExceptHeight, heightMeasureSpec);
              //3:调用setMeasuredDimension保存尺寸
              setMeasuredDimension(selfExpectWidth, selfExceptHeight);
              
              //log打印mode,size
              int specMode = MeasureSpec.getMode(widthMeasureSpec);
              int specSize = MeasureSpec.getSize(widthMeasureSpec);
              switch (specMode) {
                  case MeasureSpec.AT_MOST:
                      L.d("Layout2View onMeasure: MeasureSpec.AT_MOST .specSize:"+specSize);
                      break;
                  case MeasureSpec.EXACTLY:
                      L.d("Layout2View onMeasure: MeasureSpec.EXACTLY .specSize:"+specSize);
                      break;
                  case MeasureSpec.UNSPECIFIED:
                      L.d("Layout2View onMeasure: MeasureSpec.UNSPECIFIED .specSize:"+specSize);
                      break;
                  default:
                      L.d("Layout2View onMeasure: default .specSize:"+specSize);
                      break;
              }
          }
          @Override
          protected void onDraw(Canvas canvas) {
              int width = getMeasuredWidth();
              int height = getMeasuredHeight();
              float radius = Math.min(width, height) / 2;
              paint.setColor(Color.RED);
              canvas.drawCircle(width / 2, height / 2, radius, paint);
              //将View实际尺寸显示出来
              paint.setColor(Color.BLACK);
              paint.setTextSize(30);
              Paint.FontMetrics fontMetrics = paint.getFontMetrics();
              float baseline = (height - (fontMetrics.bottom - fontMetrics.top)) / 2 - fontMetrics.top;
              paint.setTextAlign(Paint.Align.CENTER);
              canvas.drawText("View实际尺寸: " + width + "*" + height, width / 2, baseline, paint);
          }
      }
      
      <LinearLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:orientation="vertical">
          <Layout2View
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginBottom="20dp" />
          <Layout2View
              android:layout_width="400px"
              android:layout_height="500px"
              android:layout_marginBottom="20dp" />
          <Layout2View
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:layout_marginBottom="20dp" />
      </LinearLayout>
      
      android:layout_width="wrap_content" :
      Jet2020: Layout2View onMeasure: MeasureSpec.AT_MOST .specSize:1080
      
      android:layout_width="400px" :
      Jet2020: Layout2View onMeasure: MeasureSpec.EXACTLY .specSize:400
      
      android:layout_width="match_parent" :
      Jet2020: Layout2View onMeasure: MeasureSpec.EXACTLY .specSize:1080
      
    5. 由上面log打印可见:
      • 当View的宽/高为 wrap_content: 其 widthMeasureSpec/heightMeasureSpec 对应的 mode 是 MeasureSpec.AT_MOST; size是父View的宽/高.
      • match_parent : mode 是 MeasureSpec.EXACTLY; size是父View的宽/高.
      • 精确尺寸在,例如400px : mode 是 MeasureSpec.EXACTLY; size是定义的精确尺寸.
  5. View.resolveSize

    public static int resolveSize(int size, int measureSpec) {
        return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
    }
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            //wrap_content 对应着 MeasureSpec.AT_MOST
            //size对应着父View的宽/高
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            //match_parent 和 精确尺寸/400px 对应着 MeasureSpec.EXACTLY
            //match_parent 的size对应着父View的宽/高
            //精确尺寸, size就是定义的精确值
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
    
  6. View/ViewGroup的布局过程 HenCoder Android 自定义 View 2-1 布局基础

    1. View/ViewGroup的布局过程,就是根据开发者的要求,计算出正确的尺寸和位置,并将其按照计算出的尺寸和位置进行'摆放',包括测量阶段 和 布局阶段.
    2. 测量阶段
      • View/ViewGroup的measure被其父View调用,measure是1个调度方法,在onMeasure中执行实际的自我测量.
        1. 普通View: 在onMeasure中计算自身尺寸,并调用setMeasuredDimension保存计算出的宽高.
        2. ViewGroup: 根据每个子View的期望尺寸和ViewGroup自身可用尺寸,执行makeMeasureSpec得到子View的宽高属性值,将其作为参数调用子View的measure对其进行测量.
          并根据子View测量后的尺寸,得到每个子View的实际尺寸和位置,进行保存.
          最后根据所有子View的尺寸和位置,计算出自身的尺寸,调用setMeasuredDimension进行保存.
      • 不是所有ViewGroup都需要保存子View的位置,例如LinearLayout根据垂直或水平布局方向,在onLayout方法内,就可以通过获取每个子View的尺寸,在水平/垂直方向累加,将子View放到合适位置.
      • 有些情况,ViewGroup需要对子View进行2次或多次测量才能得到其正确的尺寸及位置.如图:
    3. 布局阶段
      • View/ViewGroup的layout被其父View调用,layout方法参数确定其被摆放的具体位置.
        /**
         * Assign a size and position to a view and all of its
         * descendants
         * @param l Left position, relative to parent
         * @param t Top position, relative to parent
         * @param r Right position, relative to parent
         * @param b Bottom position, relative to parent
         */
        @SuppressWarnings({"unchecked"})
        public void layout(int l, int t, int r, int b) {
        
        layout会调用onLayout,执行实际的内部布局.内部布局,指的是将所有子View摆放到正确位置.onLayout对View和ViewGroup不一样.
        1. 普通View: 没有子View,其onLayout是空方法.
        2. ViewGroup: 会调用所有子View的layout方法,将之前保存的每个子View的位置(int l, int t, int r, int b)传入,摆放到正确位置上.
    4. 布局过程的自定义有3类
      1. extends TextView/ImageView,对原有组件进行扩展:
        • 重写onMeasure,首先调用super.onMeasure,触发原始自我测量逻辑;
        • 执行 getMeasuredWidth(),getMeasuredHeight()获取原始测量宽高,并对原始测量宽高进行调整;
        • 调用setMeasuredDimension进行保存;
      2. extends View.创建全新的自定义View:
        • 流程见 '4. 全新自定义View尺寸'
      3. extends ViewGroup.创建全新的自定义ViewGroup: HenCoder Android 自定义 View 2-3 定制 Layout 的内部布局
        • 计算子View尺寸:
          1. 根据开发者的要求(xml中子View以layout开头的属性),和自己的可用空间,获得调用子View的measure方法的2个参数. 通用的获取方式如下:
        • 通用的自定义ViewGroup的测量及布局逻辑.
        public class CustomLayout extends ViewGroup{
            List<Rect> childLayoutParams = new ArrayList<Rect>();
        
            //重写onMeasure
            @Override
            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                //1:获取ViewGroup宽高的mode及size
                int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
                int selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
                int selfHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
                int selfHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
                //2:对子View进行遍历,根据开发者对每个子View的尺寸要求,及ViewGroup自身的可用空间,获取子View的宽高属性,调用其measure进行测量,并保存子View的尺寸及位置
                //将要计算的子View宽高属性
                int childWidthMeasureSpec;
                int childHeightMeasureSpec;
                //子View测量后的宽高
                int childMeasuredWidth;
                int childMeasuredHeight;
                for(int i=0;i<getChildCount();i++){
                    //获取子View
                    View child = getChildAt(i);
                    //获取开发者对子View的尺寸要求/布局要求.就是ViewGroup.LayoutParams实例.
                    LayoutParams lp = child.getLayoutParams();
                    //lp.width有3种情况: 
                    //ViewGroup.LayoutParams.MATCH_PARENT , 
                    //ViewGroup.LayoutParams.WRAP_CONTENT , 具体像素值.
                    //源码:Information about how wide the view wants to be. Can be one of the constants FILL_PARENT (replaced by MATCH_PARENT in API Level 8) or WRAP_CONTENT, or an exact size
                    int width = lp.width;
                    int height = lp.height;
                    switch(width){
                        //子View的layout_width是match_parent
                        case ViewGroup.LayoutParams.MATCH_PARENT:
                            if(selfWidthSpecMode == MeasureSpec.EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST){
                                //当前ViewGroup的layout_width是match_parent,具体像素值, 或 wrap_content
                                //match_parent : 则selfWidthSpecSize对应着当前ViewGroup的父View的具体宽度
                                //具体像素值 : 则selfWidthSpecSize对应着当前ViewGroup具体像素值
                                //wrap_content : 则selfWidthSpecSize对应着当前ViewGroup的父View的具体宽度
                                //ViewGroup自身可用空间,就是MeasureSpec.getSize得到的值.
                                
                                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, MeasureSpec.EXACTLY);
                            }else{
                                //selfWidthSpecMode == MeasureSpec.UNSPECIFIED
                                //代表当前ViewGroup宽度无上限
                                //当前ViewGroup宽度无上限,而子View又要填满ViewGroup,传入参数0,UNSPECIFIED即可.
                                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                            }
                            break;
                        //子View的layout_width是wrap_content
                        //wrap_content有一层隐藏含义:该子View的尺寸最多和父View一样大,不能超出父View的范围
                        case ViewGroup.LayoutParams.WRAP_CONTENT:
                            if(selfWidthSpecMode == MeasureSpec.EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST){
                                //MeasureSpec.AT_MOST : 子View的宽度不要超过ViewGroup的可用宽度
                                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, MeasureSpec.AT_MOST);
                            }else{
                                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                            }
                            break;
                        //子View的layout_width是 具体像素值
                        //这种情况下,应该直接返回该精确像素值.不需要再计算.
                        default:
                            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
                            break;
                    }
                    //childHeightMeasureSpec 的计算逻辑和 childWidthMeasureSpec 保持一致.
                    switch(height){
                        ****
                    }
                    //获取到调用该子View的measure方法所需的 childWidthMeasureSpec 和 childHeightMeasureSpec 后,执行.
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                    //然后获取该子View测量后的宽高
                    childMeasuredWidth = child.getMeasuredWidth();
                    childMeasuredHeight = child.getMeasuredHeight();
                    //将该子View的尺寸及位置保存下来
                    Rect rect = new Rect(currLeft, currTop, currLeft + childMeasuredWidth, currTop + childMeasuredHeight);
                    childLayoutParams.add(rect);
                }
                //3:根据所有子View的尺寸及位置,计算自身的尺寸,并调用setMeasuredDimension保存
                int correctWidthMeasureSpec = **;
                int correctHeightMeasureSpec = **;
                setMeasuredDimension(correctWidthMeasureSpec, correctHeightMeasureSpec);
            }
            
            //重写onLayout
            @Override
            protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
                //对于ViewGroup而言,onLayout目的就是将每个子View摆放到之前计算到的合适位置
                for(int i=0;i<childLayoutParams.size();i++){
                    //遍历每个子View,及之前计算出来的对应位置参数
                    Rect position = childLayoutParams.get(i);
                    View child = getChildAt(i);
                    //调用子View的layout方法,将其摆放到对应位置
                    child.layout(position.left, position.top, position.right, position.bottom);
                }
            }
        }
        
  7. onSizeChanged

    • 在layout方法之中调用.layout中发现View的尺寸发生改变才会调用.如果尺寸没有变化,即使layout调用多次也不会重复调用onSizeChanged.
      This is called during layout when the size of this view has changed. 
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
      }
      
  8. PathDashPathEffect 可用于绘制类似仪表盘刻度.

    • Path是有方向性的

40.View的事件分发机制

自定义 View3-1 触摸反馈,以及 HenCoder Plus

  1. 几个关键方法:
    • dispatchTouchEvent
    • onInterceptTouchEvent
    • onTouchEvent
    • requestDisallowInterceptTouchEvent
  2. onTouchEvent
    • 重写 onTouchEvent(),在里面写上你的触摸反馈算法,并返回 true(关键是 ACTION_DOWN 事件时返回 true)。
  3. onInterceptTouchEvent
    • 如果是会发生触摸冲突的 ViewGroup,还需要重写 onInterceptTouchEvent(),在事件流开始时返回 false,并在确认接管事件流时返回一次 true,以实现对事件的拦截。
  4. requestDisallowInterceptTouchEvent
    • 当子 View 临时需要阻止父 View 拦截事件流时,可以调用父 View 的 requestDisallowInterceptTouchEvent() ,通知父 View 在当前事件流中不再尝试通过 onInterceptTouchEvent() 来拦截。
  5. 几个点的代码验证
    1. 手指触摸一个子View,不断滑动,滑出子View的范围继续滑动,最后抬起手指.子View最终会收到什么事件?
      • MotionEvent.ACTION_UP
      • 即使手指滑动出子View范围,会继续收到MOVE,直至手指抬起,收到UP.
      public class MotionEventLayout extends LinearLayout {
          @Override
          public boolean onInterceptTouchEvent(MotionEvent ev) {
              boolean result = super.onInterceptTouchEvent(ev);
              switch (ev.getAction()) {
                  case MotionEvent.ACTION_DOWN:
                      L.d("MotionEventLayout onInterceptTouchEvent ACTION_DOWN . result:" + result);
                      break;
                  case MotionEvent.ACTION_MOVE:
                      L.d("MotionEventLayout onInterceptTouchEvent ACTION_MOVE . result:" + result);
                      break;
                  case MotionEvent.ACTION_UP:
                      L.d("MotionEventLayout onInterceptTouchEvent ACTION_UP . result:" + result);
                      break;
                  case MotionEvent.ACTION_CANCEL:
                      L.d("MotionEventLayout onInterceptTouchEvent ACTION_CANCEL . result:" + result);
                      break;
                  case MotionEvent.ACTION_OUTSIDE:
                      L.d("MotionEventLayout onInterceptTouchEvent ACTION_OUTSIDE . result:" + result);
                      break;
                  default:
                      L.d("MotionEventLayout onInterceptTouchEvent default . result:" + result);
                      break;
              }
              return result;
          }
          @Override
          public boolean onTouchEvent(MotionEvent event) {
              boolean result = super.onTouchEvent(event);
              switch (event.getAction()) {
                  case MotionEvent.ACTION_DOWN:
                      L.d("MotionEventLayout onTouchEvent ACTION_DOWN . result:" + result);
                      break;
                  case MotionEvent.ACTION_MOVE:
                      L.d("MotionEventLayout onTouchEvent ACTION_MOVE . result:" + result);
                      break;
                  case MotionEvent.ACTION_UP:
                      L.d("MotionEventLayout onTouchEvent ACTION_UP . result:" + result);
                      break;
                  case MotionEvent.ACTION_CANCEL:
                      L.d("MotionEventLayout onTouchEvent ACTION_CANCEL . result:" + result);
                      break;
                  case MotionEvent.ACTION_OUTSIDE:
                      L.d("MotionEventLayout onTouchEvent ACTION_OUTSIDE . result:" + result);
                      break;
                  default:
                      L.d("MotionEventLayout onTouchEvent default . result:" + result);
                      break;
              }
              return result;
          }
      }
      
      public class MotionEvent1View extends View {
          @Override
          public boolean onTouchEvent(MotionEvent event) {
              switch (event.getAction()){
                  case MotionEvent.ACTION_DOWN:
                      L.d("MotionEvent1View onTouchEvent ACTION_DOWN");
                      break;
                  case MotionEvent.ACTION_MOVE:
                      L.d("MotionEvent1View onTouchEvent ACTION_MOVE");
                      break;
                  case MotionEvent.ACTION_UP:
                      L.d("MotionEvent1View onTouchEvent ACTION_UP");
                      break;
                  case MotionEvent.ACTION_CANCEL:
                      L.d("MotionEvent1View onTouchEvent ACTION_CANCEL");
                      break;
                  case MotionEvent.ACTION_OUTSIDE:
                      L.d("MotionEvent1View onTouchEvent ACTION_OUTSIDE");
                      break;
                  default:
                      L.d("MotionEvent1View onTouchEvent default");
                      break;
              }
              return true;
          }
      }
      
      <MotionEventLayout
          android:layout_width="match_parent"
          android:layout_height="300dp"
          android:background="@color/colorAccent"
          android:gravity="center"
          android:orientation="vertical"
          android:padding="30dp">
          <MotionEvent1View
              android:layout_width="200dp"
              android:layout_height="100dp"
              android:background="@drawable/head" />
      </MotionEventLayout>
      
      日志:
      //最开始 ACTION_DOWN
      D/Jet2020: MotionEventLayout onInterceptTouchEvent ACTION_DOWN . result:false
      D/Jet2020: MotionEvent1View onTouchEvent ACTION_DOWN
      //变成 ACTION_MOVE
      D/Jet2020: MotionEventLayout onInterceptTouchEvent ACTION_MOVE . result:false
      D/Jet2020: MotionEvent1View onTouchEvent ACTION_MOVE
      ***
      //超出子View范围继续滑动,子View依然收到 ACTION_MOVE
      D/Jet2020: MotionEventLayout onInterceptTouchEvent ACTION_MOVE . result:false
      D/Jet2020: MotionEvent1View onTouchEvent ACTION_MOVE
      ***
      //手指抬起,最终子View收到 ACTION_UP
      D/Jet2020: MotionEventLayout onInterceptTouchEvent ACTION_UP . result:false
      D/Jet2020: MotionEvent1View onTouchEvent ACTION_UP
      
    2. 子View在onTouchEvent方法中,只有MotionEvent.ACTION_DOWN返回true,其余都返回false,会不会失去对事件流的处理?
      • 不会.
      • 只要MotionEvent.ACTION_DOWN返回true,即可获取该组事件流的处理权.
      public class MotionEvent2View extends View {
          @Override
          public boolean onTouchEvent(MotionEvent event) {
              switch (event.getAction()){
                  case MotionEvent.ACTION_DOWN:
                      L.d("MotionEvent2View onTouchEvent ACTION_DOWN");
                      //只在 MotionEvent.ACTION_DOWN 返回true
                      return true;
                  case MotionEvent.ACTION_MOVE:
                      L.d("MotionEvent2View onTouchEvent ACTION_MOVE");
                      break;
                  case MotionEvent.ACTION_UP:
                      L.d("MotionEvent2View onTouchEvent ACTION_UP");
                      break;
                  case MotionEvent.ACTION_CANCEL:
                      L.d("MotionEvent2View onTouchEvent ACTION_CANCEL");
                      break;
                  case MotionEvent.ACTION_OUTSIDE:
                      L.d("MotionEvent2View onTouchEvent ACTION_OUTSIDE");
                      break;
                  default:
                      L.d("MotionEvent2View onTouchEvent default");
                      break;
              }
              //其余情况都返回false
              return false;
          }
      }
      
      <MotionEventLayout
          android:layout_width="match_parent"
          android:layout_height="300dp"
          android:background="@color/colorPrimary"
          android:gravity="center"
          android:orientation="vertical"
          android:padding="30dp">
          <MotionEvent2View
              android:layout_width="200dp"
              android:layout_height="100dp"
              android:background="@drawable/head" />
      </MotionEventLayout>
      
      日志:
      //最开始 ACTION_DOWN , 子View在 onTouchEvent 返回true
      D/Jet2020: MotionEventLayout onInterceptTouchEvent ACTION_DOWN . result:false
      D/Jet2020: MotionEvent2View onTouchEvent ACTION_DOWN
      //后续所有事件, 子View在 onTouchEvent 返回false
      //依然可以收到后续事件流.
      D/Jet2020: MotionEventLayout onInterceptTouchEvent ACTION_MOVE . result:false
      D/Jet2020: MotionEvent2View onTouchEvent ACTION_MOVE
      ***
      //最终手指抬起,收到 ACTION_UP
      D/Jet2020: MotionEventLayout onInterceptTouchEvent ACTION_UP . result:false
      D/Jet2020: MotionEvent2View onTouchEvent ACTION_UP
      
    3. ACTION_CANCEL
      如果某一个子View处理了Down事件,那么随之而来的Move和Up事件也会交给它处理。
      但是交给它处理之前,父View还是可以拦截事件的,如果拦截了事件,
      那么子View就会收到一个Cancel事件,并且不会收到后续的Move和Up事件
      

41.TextView.getLayout android\text\Layout.java

  1. TextView获取每行的内容
  2. TextView:Layout、TextPaint、FontMetrics的使用
  3. Android TestView获取每一行文字的方法

42.Java数组

  1. 数组的声明
    • int[] a;
    • int a[];
  2. 数组转List: Arrays.asList
    • Arrays.asList实际返回类型,不是平时使用的java.util.ArrayList, 而是 Arrays 的一个继承了AbstractList 的内部类. 该内部类未实现 add, remove 方法,所以要增删元素,需要将其转换为 java.util.ArrayList.
    • ArrayList items = new ArrayList(Arrays.asList(arrays));
      //D:\SDK\sources\android-28\java\util\Arrays.java
      public static <T> List<T> asList(T... a) {
          return new ArrayList<>(a);
      }
      private static class ArrayList<E> extends AbstractList<E>
          implements RandomAccess, java.io.Serializable
      {
          //未重写 add, remove.
      }
      
      //D:\SDK\sources\android-28\java\util\AbstractList.java
      public boolean add(E e) {
          add(size(), e);
          return true;
      }
      public void add(int index, E element) {
          //会抛出异常
          throw new UnsupportedOperationException();
      }
      
  3. 数组排序: Arrays.sort(arrays);
    • 基本数据类型按照升序
    • 对象按照Comparable 的 compareTo返回值排序

43.网络

  1. URL可以拆为3部分: 协议类型://服务器地址[:端口号]路径
    https://juejin.im/timeline
    协议类型:   https
    服务器地址: juejin.im
    路径:       /timeline
    
  2. 幂等操作: 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同.
  3. HTTP请求方法:
    • GET:
      • 用于获取数据,不修改资源,无Body.
      • 幂等
    • POST:
      • 用于增加,修改资源,有Body.
      • 非幂等
    • PUT:
      • 仅用于修改资源,有Body.
      • 幂等
    • DELETE:
      • 仅用于删除资源,无Body
      • 幂等
    • HEAD:
      • 和GET使用方法完全相同. 只是响应中无Body.
      • 幂等
      • 例如想下载资源,先发送HEAD请求,看资源长度,支不支持断点续传等--
  4. 状态码
    • 1开头:临时性消息
      • 100:
      • 101:
    • 2开头:成功
      • 200:
      • 201:
    • 3开头:重定向
      • 301: 永久重定向/Moved Permanently
        • 被请求的资源已永久移动到新位置,客户端应当自动把请求的地址修改为从服务器反馈回来的地址.
        • 301比较常用的场景是使用域名跳转。
          比如,我们访问http://www.baidu.com会跳转到https://www.baidu.com,
          发送请求之后,就会返回301状态码,然后返回一个location,提示新的地址,
          浏览器就会拿着这个新的地址去访问
          
      • 302: 暂时性转移/Temporarily Moved
        • 302用来做临时跳转.比如未登陆的用户访问用户中心重定向到登录页面.
      • 303:
      • 304:
    • 4开头:客户端错误
      • 400:
      • 401:
      • 403:
      • 404:
    • 5开头:服务器错误
      • 500: 服务器内部错误
  5. Header
    1. Host: 不是在网络上用于寻址的,而是在目标服务器上用于定位子服务器的.
      • 寻址:DNS
    2. Content-Length: 指定Body(请求体 或 响应体)的长度
    3. Transfer-Encoding: chunked
      HTTP协议扫盲(八 )响应报文之 Transfer-Encoding=chunked方式
      • 由于 Content-Length 字段必须真实反映实体长度,但是对于动态生成的内容来说,在内容创建完之前,长度是不可知的.
      每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF(\r\n)。
      最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。
      例:
      
      HTTP/1.1 200 OK
      Content-Type: text/plain
      Transfer-Encoding: chunked
      
      23\r\n
      This is the data in the first chunk\r\n
      1A\r\n
      and this is the second one\r\n
      3\r\n
      con\r\n
      8\r\n
      sequence\r\n
      0\r\n
      \r\n
      
      Content-Encoding 和 Transfer-Encoding 二者经常会结合来用,其实就是针对 Transfer-Encoding 的分块再进行 Content-Encoding压缩。
      
    4. Location: 指定重定向的⽬目标 URL
      实例:
      还是刚刚那个网址. https://www.bbsmax.com/A/pRdBaGrD5n/ .
      在浏览器中将https修改为http,回车访问.
      查看网络响应.
      
      请求Header:
      GET /A/pRdBaGrD5n/ HTTP/1.1
      Host: www.bbsmax.com
      ***
      
      响应Header
      //301 永久重定向
      HTTP/1.1 301 Moved Permanently
      //浏览器应该继续访问 https://www.bbsmax.com/A/pRdBaGrD5n/
      Location: https://www.bbsmax.com/A/pRdBaGrD5n/
      ***
      
      继续请求-->
      //继续请求https**
      Request URL: https://www.bbsmax.com/A/pRdBaGrD5n/
      Request Method: GET
      //成功返回响应结果
      Status Code: 200 OK
      
    5. User-Agent:
      • User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等
      • 用于服务器区分不同终端,返回与之匹配的内容
    6. Range / Accept-Range
      • Range Accept-Range可以用来做断点续传,多线程下载;
      • Accept-Range:bytes:响应报文中出现,表示服务器支持按字节来取范围数据;
      • Range:bytes=start-end:请求报文中出现,表示要取哪段数据;
      • Content-Range:start-end/total:响应报文中出现,表示发送的是哪段数据;
    7. Cache和Buffer的区别:
      • Cache是之前用过的东西以后可能会再用,先存着;
      • Buffer是没有用过的东西以后可能/一定会用,先存着;
      • Cache是针对用过的;Buffer是针对没用过的;
      • Buffer一定是针对工作流的;
    8. Cache: 在客户端或中间⽹网络节点缓存数据,降低从服务器器取数据的频率,以提⾼高⽹网络性能.
      • 响应中使用Cache-Control:
        • public :中间网络节点可以缓存该网络响应,数据是共用数据,和特定用户无关.
        • private :中间网络节点不能缓存该网络响应,数据是特定用户的个性化数据.
        • private不代表私密/加密的意思,单纯指不同用户个性化数据不可共用.
    9. Content-Type
      • application/json , image/jpeg , application/zip ...单项内容(⽂文本或⾮非⽂文本都可以),⽤用于 Web Api 的响应或者 POST / PUT 的请求,可用于文件上传.
      • 文件上传使用下面方式更简单,不必非得使用multitype/form-data . 单纯提交一个文件,相比 multitype/form-data 可以避免使用 boundary ,降低了带宽占用,提升了速度.
      POST /music/32/album HTTP/1.1
      Host: ***
      Content-Type: image/jpeg
      Content-Length: 43257
      SJGLDJGLSKGH;LDSKGH......
      
  6. 对称加密
    • 1个密钥+2个算法(加密算法,解密算法)
    • 明文-->(密钥+加密算法)-->密文
    • 密文-->(密钥+解密算法)-->明文
    • 算法: DES(56位密钥,密钥太短逐渐被弃用), AES(128位,192位,256位,当下最流行的对称加密算法)
    • 对称加密的POJIE:
      • 1:首先设法拿到一组或多组原文-密文对;
      • 2:设法找到一个密钥,可以将原文-->密文,并将密文-->原文,即为POJIE成功;
      • 3:反POJIE:一种优秀的对称加密算法的标准是,找不到比穷举法更更有效的手段,且POJIE时间足够⻓.
    • 对称加密的缺点:无法在不可信网络上传输密钥,一旦被第三方获取到密钥,则通信失败.
  7. 非对称加密
    • 2个密钥(公钥+私钥)+1个加密算法
    • A发送数据给B:
      • A:明文-->(B的公钥+加密算法)-->密文 ==> B:(B的私钥+加密算法)-->明文
    • 公钥和私钥互相可解.公钥发给对方,私钥自己保管.
    • 经典非对称加密算法:RSA(可用于加密和签名),DSA(仅用于签名,但速度更快)
    • 由于公钥和私钥互相可解,非对称加密可应用于数字签名技术.通常会对原始数据hash以后对hash签名,然后附加在原始数据的后面作为签名.
    • 对方公钥加密,自身私钥签名
    • 非对称加密的POJIE:
      1. 和对称加密不同的是,非对称加密的公钥很容易获得,制造 原文-密文对很容易;
      2. 关键在于如何找到正确的私钥,可以解密所有经过公钥加密后的密文.找到这样的私钥即为成功POJIE;
      3. 对称加密POJIE是不断尝试新密钥是否可以将拿到的原文-密文对加密和解密;
      4. 非对称加密POJIE,是不断尝试新私钥是否和公钥可以互解.
      5. 非对称加密算法优秀的标准和加密算法一样,找不到比穷举法更有效的手段,且穷举法POJIE时间足够长.
    • 非对称加密的优点:可在不可信网络上传输公钥.缺点:计算复杂,性能差.
    • 公钥私钥互相可解,那是否可以任意公开一个保密一个?
      • 不能.
      • 因为公钥和私钥的位置是不对等的,很多时候根据私钥可以很容易推算出公钥.但是通过公钥无法导出私钥
  8. 编码及解码:
    • 编码:把数据从一中形式转换为另一种形式. 解码:将数据恢复到原始格式.
    • 编码一定有解码与之对应,单向转换不是编码.
  9. Base64
    • 将二进制数据中每6位对应成Base64索引表中的一个字符,编排成1个字符串(每个字符占8位).
    • Base64属于编码,不属于加密(对称加密没有不公开,非对称加密的私钥不公开,而Base64编码解码逻辑是公开的).
    • Base64性能差,原数据每6位转换为8位(1个字符),存储和传输会增加1/3的消耗.使用Base64的好处就是使用字符串相对于普通二进制数据/文件的好处.
  10. 压缩和解压缩属于编解码
  11. 序列化
    • 将内存中对象转换为字节序列的过程,可以将字节序列存储到磁盘,或者进行网络传输;
    • 序列化不是编码,不属于数据格式的转换;
    • Serializable接口:
      1. xxx
      2. xxx
    • 其他
  12. Hash : 数据中提取出摘要信息
    • Hash不属于编码,单向不可逆不属于编码,也不属于加密,加密属于编码,解密属于解码
    • Hash实际用途:
      1. 唯一性验证-hashCode()
      2. 数据完整性验证,如下载文件附加MD5用户可校验文件是否被篡改或损坏
      3. 快速查找-HashMap
        • 重写hashCode为什么也要重写equals/hashCode和equals为什么要同时修改?
        • 看看HashMap源码就明白了
      4. 隐私保护
        • 明文存储密码不安全,存储密码Hash简单密码也容易被彩虹表暴力POJIE.加盐可解决彩虹表问题.可以将(用户密码+随机字符串 按指定规则组合后)取hash,将hash和随机字符串进行存储.
        • 加盐: www.jianshu.com/p/f1954e614…
  13. Cookie
    • Cookie工作机制
      1. 服务器器需要客户端保存的内容,放在 Set-Cookie headers 里返回,客户端会自动保存.
        Set-Cookie:user=ZhangSan
        
      2. 客户端保存的 Cookies,会在之后的所有请求里都携带 Cookie header 里发回给服务器.
        Cookie:user=ZhangSan
        
      3. 客户端保存 Cookie 是按照服务器域名来分类的.例如 shop.com 发回的 Cookie 保存下来,之后向 games.com 的请求中不会携带.
      4. 客户端保存的 Cookie 在超时后会被删除、没有设置超时时间的 Cookie (称作 Session Cookie)在浏览器器关闭后就会⾃自动删除;另外,服务器器也可以主动删除还未过期的客户端 Cookies.
    • Cookie作用
      1. 会话管理:如登陆状态.服务器响应数据Header包含 Set-Cookie:user_id = 123
      2. 记录用户偏好
      3. 分析用户行为
        追踪用户信息,永远不是用A网站记录访问A网站的用哪个用户.
        而是用1个统一的第三方网站,保存某个终端/设备访问所有网站的记录.
        
  14. Http确认授权的2种方式
    1. 通过Cokkie .如上述 user_id
    2. 通过Header中添加Authorization
      • Authorization两种主流方式:Basic , Bearer
      • Authorization: Basic username:password执行Base64编码的字符串
        Authorization: Basic 12345678
        
      • Authorization: Bearer bearer token
        • bearer token 需要通过 OAuth2的授权流程获取.
  15. OAuth2
  • 掘金一篇文章很不错 做前后端分离项目前,劝你先了解 OAuth2.0 的四种授权方式
  • OAuth2流程
    • 第三方网站向授权方网站申请第三方授权合作,拿到client id和client secret.client secret需要绝对保密,一直存在第三方网站服务器.
    • 用户使用掘金,点击GitHub登录,首先跳转到GitHub网站,并将client id传入.
    • GitHub根据client id,将掘金信息及掘金需要的GitHub用户的权限进行展示,询问用户是否同意
    • 用户点击'同意',GitHub重新跳转回掘金,并传入Authorization code作为用户同意凭证.
    • 掘金将Authorization code发送回掘金自己服务器
    • 掘金服务器将Authorization code及client secret一并发送给GitHub服务器(这个连接一定是HTTPS连接,是绝对安全的),GitHub验证通过,则返回access token/bearer token.
    • 获取到 access token/bearer token 后,OAuth2流程已经结束.
    • 后续:掘金获取到access token,后台服务器就向GitHub发送请求,用于获取该用户的信息/操作用户账户.然后根据用户信息创建'掘金账号',并将'掘金账号'和GitHub的用户信息进行关联.
  • 引入Authorization code ,并且要在后台服务器将 Authorization code,client secret发送给GitHub获取access token,为什么不直接返回access token?
    • 为了安全.因为client secret由第三方服务器保存绝对安全,后台服务器和GitHub使用HTTPS获取access token,access token不会暴露给客户端,避免客户端直接使用access token进行网络传输被窃取.
  • Refresh token
    • 服务器返回access_token的相应数据格式类似:
    {
        "token_type": "Bearer",
        "access_token": "111",
        "refresh_token": "222",
        "expires_time": "111"
    }
    
    • 用法:access token 有失效时间,在它失效后,调用 refresh token 接口,将 client id, client secret, Authorization code, refresh-token 一起通过HTTPS发送到第三方授权网站,重新获取新的 access-token.
    • 目的:安全.当access token失窃,由于它有失效时间,因此坏⼈人只有较短的时间使用.同时,由于refresh token 永远只存在与第三方服务的服务器中,refresh token ⼏几乎没有失窃的⻛风险
  1. HTTPS
    HTTPS 为什么是安全的
    1. 定义: HTTP over SSL,建立在TLS/SSL上的HTTP.就是加密通信的HTTP
    2. 工作原理:
      • 在客户端和服务器之间协商出一套对称加密密钥
      • 每次发送消息前进行加密,收到消息后进行解密
    3. 为什么不直接使用非对称加密
      • 非对称加密使用了复杂的数学原理,计算相当复杂.每次都是用非对称加密,会严重影响网络通信性能
    4. HMAC
      • HMAC运算利用hash算法,以一个消息M和一个密钥K(Secret)作为输入,生成一个定长的消息摘要作为输出。
        原始消息:"111111111111111"
        普通hash算法后:"aaaaaaaa"
        HMAC:"aabbccdd"
        
      • 只有发送方发送消息时使用的Secret和接收方验证时使用的一致,对同样原始消息进行HMAC摘要运算才能得到相同的HMAC结果.
      • 而Secret是双方协商出来保存在本地,不会在网络上进行传输,不存在被窃取的可能.
    5. HTTPS 连接建立步骤
      1. Client Hello
      2. Server Hello
      3. 服务器器证书 信任建⽴立
      4. Pre-master Secret
      5. 客户端通知:将使⽤用加密通信
      6. 客户端发送:Finished
      7. 服务器器通知:将使⽤用加密通信
      8. 服务器器发送:Finished
      
    6. 比如A想访问B网站,B网站的证书是BA;A在在发送请求路程中间,被C网站截获,C网站将自己的证书CA返回给A. CA本身也是一个合法的证书,A怎么去辨认?
      • 通过证书信息中的域名(host)去辨认.比如A要访问alibaba,结果被tencent截获,返回了tencent的证书,虽然tencent证书本身合法,但是tencent的证书信息中,host是tencent,不是alibaba,和A要访问的域名不一致,就不会进行后续的验证工作.
      • 证书不能伪造,也不能篡改.因为证书签发机构的私钥第三方是拿不到的,改了任何信息,都会导致修改后信息的HASH和证书签名被签发机构公钥运算后的结果对不上,导致认证失败!
    7. 在 Android 中使⽤用 HTTPS
      • 正常情况下直接使用即可
      • 以下情况不能直接使用,需要自己写证书验证过程
        1. 用的是⾃自签名证书(例例如只⽤用于内⽹网的 https)
          • 直接使用OkHttp中的CertificatePinner可以很方便的实现自签名证书验证
            https://blog.csdn.net/challenge51all/article/details/82909965
            可以直接看 CertificatePinner 的注释,有使用方法:
            
            将异常中的公钥散列粘贴到证书pinner的配置中:
            String hostname = "publicobject.com";
            CertificatePinner certificatePinner = new CertificatePinner.Builder()
               .add(hostname, "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
               .add(hostname, "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
               .add(hostname, "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
               .add(hostname, "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
               .build();
            OkHttpClient client = new OkHttpClient();
            client.setCertificatePinner(certificatePinner);
            Request request = new Request.Builder()
                 .url("https://" + hostname)
                 .build();
            client.newCall(request).execute();
            
        2. 证书信息不不全,缺乏中间证书机构(可能性不不⼤大)
        3. ⼿手机操作系统较旧,没有安装最新加⼊入的根证书
        4. 自己怎么写证书验证过程: 通过 HTTPS 和 SSL 确保安全
  2. HTTPS 连接建立步骤详细
    1. 客户端向服务器发送"Client Hello",包含以下内容:
      • 客户端可以接受的 所有的SSL/TLS的版本
      • 客户端可以接受的 所有的 非对称加密算法+对称加密算法+hash算法组合.每个组合称为1个Cipher Suite(密钥算法套件).
      • 客户端随机数
      • Server name:告知服务器要下发到哪个子服务器
    2. 服务器收到客户端发来的"Client Hello"后,选出要使用的SSL/TLS版本+Cipher Suite,再加上服务器随机数,再一起发给客户端:"Server Hello"
      • 保存客户端随机数
      • 从客户端提供的所有 SSL/TLS版本 中选出要使用的版本
      • 从客户端提供的所有 Cipher Suite 中选出要使用的 Cipher Suite
      • 然后将选好的 SSL/TLS版本+Cipher Suite,再加上服务器随机数,一起发给客户端:"Server Hello"
      • 选好的 SSL/TLS版本+Cipher Suite+客户端随机数+服务器随机数,服务器和客户端一人一份
    3. 服务器向客户端发送 服务器证书,若客户端对服务器证书的验证通过,则客户端对服务器的信任建立成功
      • 服务器证书结构:
        1. 证书本身信息(绿色部分)
        2. 证书签发机构信息(橙色部分)
        3. 证书签发机构的签发机构信息(蓝色部分):签发机构的签发机构,就存在我们电脑/手机根证书列表里,对于设备上存在的根证书,我们只能必须信任
      • 客户端对服务器证书进行验证,满足2个条件,则服务器证书验证通过:
        1. 确保的确是证书签发机构对服务器证书进行了签名
        2. 确保证书签发机构是可信的
      • 服务器证书中证书签名来源
        1. 首先把 证书信息,证书公钥,证书签名算法这3个信息揉在一起, 做1个hash,得到原始值OriStr.具体什么hash算法在 证书签名算法里面有.
        2. 证书签名算法中包含:具体hash算法,签名用那种非对称加密算法
        3. 然后用证书签发机构的私钥对OriStr进行加密运算,得到最终的 证书签名
      • 客户端怎么验证证书合法
        1. 客户端把 证书信息,证书公钥,证书签名算法通过 证书签名算法中的hash算法得出 hash值 ClientHash
        2. 用证书签发机构的公钥对 证书签名进行hash,得到 ServerHash
        3. 如果 ClientHash和ServerHash相等,就说明的确是证书签发机构对证书进行了签名(只有用 证书签发机构的私钥 加密,公钥解密,才可能ServerHash=ClientHash)
        4. 如何验证 证书签发机构 是可信的:用设备上存在的 签发机构的签发机构 来验证签发机构是否可信
          • 对设备上存在的 签发机构的签发机构/根证书,我们只能信任
          • 验证流程和验证证书签名是一致的:
            1. 客户端把 签发机构基本信息,签发机构公钥,签发机构签名算法通过 签发机构签名算法中的hash算法得出 hash值 Hash1
            2. 客户端用 签发机构的签发机构的公钥 对 签发机构签名进行hash,得到 Hash2
            3. 如果 Hash1=Hash2,说明证书签发机构也是可信的
    4. Pre-master Secret
      • 客户端自己算出1个Pre-master Secret,保存之,并将Pre-master Secret使用服务器证书公钥加密后发送给服务器,服务器解密后保存Pre-master Secret.两边一人一份
      • 客户端和服务器利用两边同样的 客户端随机数+服务器随机数+Pre-master Secret,用固定算法计算出 Master Secret
      • 所有的客户端和服务器沟通,都是用同样的算法计算出Master Secret
      • Master Secret得到后,客户端随机数+服务器随机数+Pre-master Secret就没用了,客户端和服务器都保存Master Secret
      • 客户端和服务器用Master Secret计算出:
        1. 真正加密通讯时使用的2个对称加密秘钥
          • 客户端给服务器发消息使用的对称加密密钥M1:客户端密钥
          • 服务器给客户端发消息使用的对称加密秘钥M2:服务器密钥
        2. 客户端给服务器,以及服务器给客户端发送消息时用来做HMAC算法的MAC Secret
          • 客户端MAC Secret
          • 服务器MAC Secret
    5. 客户端向服务器发送一条通知:将使用加密通信
    6. 客户端向服务器发送:Finished
      1. 客户端将步骤1-步骤5 所有信息使用 M1加密得到密文C1
      2. 客户端将步骤1-步骤5 所有信息使用 客户端MAC Secret 进行HMAC运算得到签名S1
      3. 客户端将C1及S1一起发送至服务端
      4. 服务端接收到消息:
        1. C1使用 M1解密得到原文C2.并将C2和自身保存的步骤1-5进行比对,确认解密成功.
        2. 对步骤1-5使用 客户端MAC Secret 进行HMAC运算得到签名S2. 若S1=S2, 确认签名验证成功.
    7. 服务器向客户端发送一条通知:我也将使用加密通信
    8. 服务器向客户端发送:Finished
      1. 服务端将步骤1-步骤7 所有信息使用 M2加密得到密文C1
      2. 服务端将步骤1-步骤7 所有信息使用 服务器MAC Secret 进行HMAC运算得到签名S1
      3. 服务端将C1及S1一起发送至客户端
      4. 客户服务端接收到消息:
        • 使用同样方式确认 解密成功 及 签名验证成功
    9. 以上8步通过,客户端就向服务器发送第一条HTTPS消息.
      • HTTPS请求的样子:
        应用层消息:后面是一大串无法解读的字符串
        
        即使消息被第3方截获,也只能知道是1个应用层消息,却无法解读,因为客户端密钥和服务器密钥第3方无法拿到. 
        客户端和服务器互发消息,除了会使用客户端密钥和服务器密钥进行加密,也都会使用HMAC进行签名.
        
  3. Retrofit
  4. OkHttp
    1. Authenticator 设置Header Authorization ,用于 Basic 或 Bearer 认证
      • OkHttp 会自动重试未验证的请求. 当响应是401 Not Authorized时,Authenticator会被要求提供认证信息.
      • Authenticator 的实现中需要返回一个新的包含认证信息的请求.
      OkHttpClient client = new OkHttpClient.Builder()
              .authenticator(new Authenticator() {
                  @Override
                  public Request authenticate(Route route, Response response) {
                      String credential = null;
                      //1:Basic 认证
                      String username = "Jet";
                      String password = "JetPassword";
                      credential = "Basic " + Base64Utils.(username+":"+password);
                      //2:Bearer 认证
                      //bearer token 需要通过 OAuth2的授权流程获取,或者通过 'refresh token'接口获取
                      String bearer_token = "***";
                      credential = "Bearer " + bearer_token;
                      return response.request().newBuilder()
                              .header("Authorization", credential)
                              .build();
                  }
              })
              .build();
      
    2. OkHttp中3个重要概念: Connection , Stream , Call
    3. StreamAllocation

44.权限申请 permission

  1. android 权限申请PermissionUtil --在activity之外申请权限 GitHub
  2. 严振杰
  3. Android service中弹出dialog 权限变动与用法
  4. android permission权限与安全机制解析(下)
  5. Android优雅地申请动态权限
  6. Android 在Service中弹出窗口及SYSTEM_ALERT_WINDOW权限解决方法

45.ANR

  1. Android ANR 问题第一弹
  2. Android ANR 问题第二弹------Input事件是如何超时导致ANR的

46.有向无环图及其他(不一定对)

  1. 有向:两点之间有方向性. 无向:两点之间无方向性
  2. 链 :链表就是一条有方向的线
  3. 树 :树是有分叉的,但是任意两个节点只有一条路径能到达另外一点,不能形成闭合图形.
  4. 图 :图是可以有闭合图形的.
  5. 有向无环图/DAG:
    • D对应单词是 Directed有向. A对应的单词是 Acyclic无环. G指的是 Graph图.
    • 有向无环图指的是一个无回路的有向图
    • 整张图上不允许出现沿着箭头从一个节点出发最后能又回到起点的情况

47.Gradle

  1. Gradle是什么
  2. Gradle环境变量配置
  3. gradlew和gradle的区别
  4. Gradle进程与参数
    1. client VM
      • 执行任意gradle命令,首先会创建1个JVM进程用于解析命令行参数. 这个进程叫做 client VM
    2. build VM
      • client VM 会再创建1个守护进程,来执行真正的构建任务. 这个守护进程叫做 build VM
      • 守护进程在完成当前构建后不会立即退出,会在后台休眠,一旦有新的构建任务,则直接复用,避免频繁创建/初始化新的进程,提升速度.
    3. gradle --status 查看后台守护进程状态
    4. gradle参数
      1. 参数3个来源:
        • gradle 时可以通过命令行向其传递参数
        • Android项目根目录下的 gradle.properties 文件
        • GRADLE_USER_HOME gradle.properties 文件
      2. 同一个属性会发生覆盖. 优先级:
        • 命令行参数 > GRADLE_USER_HOME gradle.properties 文件 > 项目跟目录 gradle.properties 文件
        • 关于多文件定义同一属性优先级问题: 5.4.1版本对应文档
          • 感觉作者的意思不是区分不同路径下,而是属性属于什么类型.对于同类型属性,不同文件夹下面的 gradle.properties 文件,哪个优先级更高没有说.
      3. GRADLE_USER_HOME gradle.properties 文件 具体是哪个文件? 没搞清楚.
  5. Gradle任务/Task
    1. 当在某个目录下执行gradle命令,会从该目录下查找2个文件
      1. settings.gradle
        settings.gradle 用于同时构建多个模块时, 管理模块之间的依赖关系, 构建顺序等
        
      2. build.gradle
        build.gradle 负责描述具体模块的构建
        
    2. Task
      • Task/任务,是Gradle执行的最小单元.
      • 每次构建中,每个Task只会被执行1次.
      • Task之间的依赖关系,组成了一个有向无环图/DAG,这是Gradle执行构建的核心算法.
    3. 创建自定义Task
      1. 内置 tasks 对象用于创建和操作任务. tasks 就相当于 Gradle 为我们提供的 API.使用tasks.create方法创建Task实例
      2. 声明 DefaultTask 子类,然后使用tasks.create方法创建该子类Task实例
        tasks.create("t1"){
            println("t1")
        }
        
        open class HelloTask : DefaultTask(){
            init{
                println("HelloTask init")
            }
            @TaskAction
            fun hello(){
                println("HelloTask hello")
            }
        }
        tasks.create<HelloTask>("HelloTask")
        
    4. Task之间的依赖关系 dependsOn
      1. 使用Task名称声明依赖关系
        tasks.create("foo") {
            doLast {
                println("foo doLast")
            }
        }
        tasks.create("bar") {
            //使用task名称声明依赖关系
            dependsOn("foo")
            doLast {
                println("bar doLast")
            }
        }
        
      2. 使用Task对象声明依赖关系
        val taskFoo by tasks.creating {
            doLast {
                println("taskFoo")
            }
        }
        val taskBar by tasks.creating {
            dependsOn(taskFoo)
            doLast {
                println("taskBar")
            }
        }
        
    5. Task之间的执行顺序 shouldRunAfter , mustRunAfter
      1. shouldRunAfter 以下情况失效,会先执行
        • A dependsOn B 且 B shouldRunAfter A,则shouldRunAfter失效,B依然会先执行;
        • parallel execution/并行执行 时,如果某个Task除了shouldRunAfter以往所有条件都满足,则立即执行.
      2. mustRunAfter 很严格. 与依赖冲突会报错.
        val taskB by tasks.creating {
            doLast {
                println("taskB doLast")
            }
        }
        val taskA by tasks.creating {
            mustRunAfter(taskB)
            doLast {
                println("taskA doLast")
            }
        }
        
        gradle taskA taskB 和 gradle taskB taskA 结果都一样,都是先打印 taskB doLast .
        
  6. Android Plugin DSL Reference
  7. gradle常见错误
    1. Failed to resolve: common
      • 将Project的build.gradle文件中的google()挪到jcenter()上面一行就可以了
      repositories {
          //google在jcenter上面
          google()
          jcenter()
          maven { url 'https://maven.aliyun.com/repository/google/'}
          maven { url 'https://maven.aliyun.com/repository/jcenter/'}
          maven { url 'https://maven.aliyun.com/repository/public/' }
          maven { url 'https://maven.aliyun.com/repository/spring/'}
      }
      
    2. Uninitialized object exists on backward branch 142
      E:\JetPG\Android\Android Studio\jre
      
    3. A problem occurred configuring project ':app'
    4. 'buildSrc' cannot be used as a project name as it is a reserved name
      • 删除 settings.gradle中的include(":buildSrc") 即可
  8. 项目根目录下的build.gradle
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript ({
        //A
        //A这里的repositories用于配置 依赖/B 的仓库地址
        //点开google:
        //The URL used to access this repository is {@literal "https://dl.google.com/dl/android/maven2/"}.
        //点开jcenter:
        //The URL used to access this repository is {@literal "https://jcenter.bintray.com/"}.
        repositories ({
            google();
            jcenter();
        });
        
        //B
        //依赖/B 是给谁用的,是给 gradle plugin 用的
        //例如 apply plugin: 'com.android.application' 这个plugin.
        //'com.android.application' 这个plugin 是从网上取的.从网上什么地方?需要配置.在哪儿配置?
        //在 dependencies 中的 classpath "com.android.tools.build:gradle:4.0.1" .
        //classpath "com.android.tools.build:gradle:4.0.1" 指定的plugin,要从哪些仓库去下载?
        //在A进行配置.
        dependencies ({
            //classpath "com.android.tools.build:gradle:4.0.1" 等价于
            //add("classpath","com.android.tools.build:gradle:4.0.1")
            //classpath "com.android.tools.build:gradle:4.0.1"
            //即:
            dependencies {
                configurationName dependencyNotation
            }
            对应了 DependencyHandler 的 add方法
            public interface DependencyHandler extends ExtensionAware {
                /**
                 * Adds a dependency to the given configuration.
                 *
                 * @param configurationName The name of the configuration.
                 * @param dependencyNotation
                 *
                 * The dependency notation, in one of the notations described above.
                 * @return The dependency.
                 */
                @Nullable
                Dependency add(String configurationName, Object dependencyNotation);
            }
            //
            
            add("classpath","com.android.tools.build:gradle:4.0.1")
        });
    });
    
    allprojects {
        //C
        //C中的仓库,则用于配置所有模块/项目中'开源库'的依赖仓库
        repositories {
            google();
            jcenter();
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
  9. moudle下的build.gradle
    1. buildTypes的应用
    2. Build Variant个数
    3. api 和 implementation 的区别
      • api会传递依赖,implementation不会传递依赖
      • 传递依赖: app模块依赖androidlib1库,androidlib1库又依赖androidlib2库.若androidlib1使用api依赖androidlib2, 当androidlib2发生改动,会引起androidlib1及app都发生重新编译 .而使用implementation仅会引起androidlib1发生重新编译.
      • 什么时候使用api : 当app需要直接使用androidlib2库中的代码时候.例如:
        import com.huanhailiuxin.androidlib2.lib2;
        public class MainActivity extends AppCompatActivity {
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                BuildTypeUtils.drawBadge(this);
                lib2 lib2 = null;
            }
        }
        
        若此时在 androidlib1 的build.gradle中,使用 implementation(project(":androidlib2")) ,
        则 MainActivity会标红提示 'Cannot resolve symbol 'lib2''
        
  10. task中的 doFirst 和 doLast 及 普通代码块 的区别
    • 普通代码块在 configuration 阶段就被执行
    • doFirst , doLast 在 Execution 阶段被执行
    • doFirst : 在1个task中可以写多次 doFirst , 将包含的 action 添加到 action队列的头部.
      /**
       * <p>Adds the given closure to the beginning of this task's action list. The closure is passed this task as a
       * parameter when executed.</p>
       *
       * @param action The action closure to execute.
       * @return This task.
       */
      Task doFirst(Closure action);
      
    • doLast : 在1个task中可以写多次 doLast , 将包含的 action 添加到 action队列的尾部.
      /**
       * <p>Adds the given closure to the end of this task's action list.  The closure is passed this task as a parameter
       * when executed.</p>
       *
       * @param action The action closure to execute.
       * @return This task.
       */
      Task doLast(Closure action);
      

48.发布开源库

  1. 发布到jcenter
  2. 发布到jitpack

49.Gradle插件开发

  1. 整体目标:

    • 掌握gradle当前常见需求点开发
    • 掌握gradle插件开发及上传流程
  2. DSL: DSL是Domain Specific Language的缩写,既领域专用语言.Gradle的DSL专门用于配置项目的构建,不能做其他工作.而像Java,C/C++这些就属于通用语言,可以做任何工作.

  3. maven本地仓库的配置以及如何修改默认.m2仓库位置

    • 默认本地maven仓库路径为${user.home}/.m2/repository
    • 自己电脑上为例: C:\Users\Jet\.m2
    • 修改仓库位置:
      • 若 C:\Users\Jet\.m2 下有settings.xml,则修改其中localRepository的值;
      • 若 C:\Users\Jet\.m2 下无settings.xml,则创建settings.xml文件,内容如下:
        <settings>
            <localRepository>自定义maven仓库位置</localRepository>
        </settings>
        
        例如:
        <settings>
            <localRepository>E:\JetMavenRepostitory</localRepository>
        </settings>
        
  4. 按照小册上操作,引入插件后,打印结果没问题,但是AS中显示红色,并提示找不到.

    • E:\Jet\Android\JetMavenRepostitory 是自定义的本地Maven仓库路径
  5. Android中Gradle插件和Transform

  6. Android动态编译技术:Plugin Transform Javassist操作Class文件

  7. 掘金搜索 'Gradle插件'

  8. juejin.cn/post/684490…

  9. juejin.cn/post/684490…

  10. juejin.cn/post/684490…

  11. juejin.cn/post/684490…

  12. juejin.cn/post/684490…

  13. juejin.cn/post/684490…

  14. juejin.cn/post/684490…

  15. juejin.cn/post/684490…

  16. juejin.cn/post/684490…

  17. juejin.cn/post/684490…

  18. juejin.cn/post/684490…

  19. www.jianshu.com/p/417589a56…

  20. www.jianshu.com/p/66b8eafc2…

  21. juejin.cn/post/684490…

  22. juejin.cn/post/684490…

  23. juejin.cn/post/684490…

  24. 深度探索 Gradle 自动化构建技术(四、自定义 Gradle 插件)

  25. blog.csdn.net/binbinqq86/…

  26. liuwangshu.cn/application…