日常细节记录

  1. 强引用置为null时,不会立即回收对象,帮助回收器加快回收。但是会帮助GC 等到下次回收周期时即会回收

  2. 死锁的四个必要条件:互斥,占有且等待,不可抢占,循环等待

  3. CAS(原子操作)是乐观锁用到的主要机制,乐观锁是不用加锁去执行操作,如果产生冲突则失败重试,直到成功为止,也叫做“自旋”。与乐观锁相对应的是悲观锁,synchronized就是悲观锁,也叫“独占锁”需要加锁进行操作,并且加锁代码块中的只能有一个线程进行操作。

  4. 点击App图标,系统最开始执行的是ActivityThread的main()方法

  5. 应用的启动方式:

    1. 冷启动:启动应用时,后台没有该应用的进程。系统创建一个新的进程来进行重新分配。
    2. 热启动:启动应用时,后台已有该应用的进程。
  6. Application的生命周期:onCreate()//应用开始时执行->onLowMemory()//内存低时执行->onTrimMemory()//关闭应用时执行 onTerminate()//在真机上不会调用

  7. 进程相关: 优先级最低的进程首先被杀死、进程的等级会因为其他进程的依赖而提高一个进程服务于另一个进程,则它的优先级不会比它服务的进程优先级低 按重要性分类:

    1. 前台进程:进程持有一个正在与用户交互的Activity或者和交互Activity绑定的Service,前台运行的Service(执行startForeground()),执行onReceive()的BroadcastReceiver
    2. 可见进程:进程持有一个被用户可见但没有显示在最前端的Activity(调用到了onPause())或者和可见Activity绑定的Service
    3. 服务进程:进程持有一个startService()启动的Service进程,例如播放音乐,下载文件等Service
    4. 后台进程:进程持有一个用户不可见的Activity(调用到onStop()没有到onDestroy()),进程被存放在一个LRU列表中,即很长时间没用的Activity会被优先杀死
    5. 空进程:进程不包含任何活跃的应用组件,唯一的作用是为了缓存需要,缩短下次启动的时间
  8. 统计应用启动时间:adb shell am start -W [packageName]/[packageName.MainActivity]

  9. volatile作用是可见性(当一个线程修改了某一个全局变量的值,其他线程能否知道这个修改),有序性(禁止指令重排优化,防止代码执行指令被重新排序)。volatile并不能保证线程安全即保证不了线程间操作的原子性。

  10. SharedPreference中applycommit方法的区别:commit同步保存更改,apply异步保存到磁盘,原子提交,性能较高,但不保存结果。SharedPreference不支持多线程操作,MODE_MULTI_PROCESS这个标记位并没有实际作用。可以利用ContentProvider去实现多进程,_方案后续会有介绍_。

  11. MD5不是加密算法,是一种散列算法。加密算法一般是对称加密算法

  12. 使用ADB启动Activity:adb shell am start -n 包名/需启动Activity路径

  13. Fragment中replaceadd区别:

    • replcae:把容器内所有内容进行替换,都需要重新走一遍fragment的生命周期
    • add:添加不会清空容器内的内容。
  14. 在三星手机上,进行原生分享文件时需要设置mimeType

  15. invalidate()postInvalidate()requestLayout()的区别:

    • invalidate():当子View调用invalidate方法时,会给View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递至ViewRootImpl中,最终触发performTraversals方法,进行View的重绘(即调用onDraw方法)。该方法只能在UI线程中调用
    • postInvalidate():与invalidate作用一致,都是使View进行重绘,该方法是在非UI线程中调用的。内部主要实现是提供一个Handler实现,然后直接调用了invalidate方法,继续执行重绘流程。
    • requestLayout():调用了这个方法会重新执行View的绘制流程,即重新执行测量(onMeasure),布局(onLayout),绘制(onDraw)方法。利用责任链模式-不断向上传递该事件,直到找到能处理该事件的上级
  16. Bitmap所占用的内存 = 图片长度 x 图片宽度 x (屏幕分辨率÷图片资源目录分辨率)²×一个像素点占用的字节数

  17. HashMap扩容机制 在1.8之前只要达到负载就进行扩容,1.8之后是防止Hash冲突才进行扩容,如果不冲突不会触发扩容。

  18. px转换为dppx/(DPI/160) = dp,例如1920 * 1080 , 480dpi,最终转化就会得到360dp

  19. 内部类可以访问外部类 private变量。在内部类需要引用外部类的private变量时,会默认生成一个access$XXX(),内部会返回当前对象

  20. Java与Dart一样都是采用了值传递求值策略

    | 求值策略 | 求值时间 | 传值方式 |
    | ———— | ——– | —————— |
    | 值传递 | 调用前 | 值的结果(值的副本) |
    | 引用传递 | 调用前 | 原始值 |
    | 名传递 | 调用后 | 与值无关的一个名 |

    其中最常见的是值传递(主要应用于Java、Dart、OC等),然后是引用传递(C、C++、C#)。

    这两者主要的区别如下:

    值传递:调用函数时将参数复制一份传递到函数中,函数中对参数进行修改,也不会影响到实际参数
    引用传递:调用函数时传递的是实际参数的地址,函数中对参数进行修改时,也会影响到实际参数的值

    值传递无论参数类型是值类型或引用类型,都会调用栈上创建的一个副本。

    • 对于值类型,栈上的副本是整个原始值的复制
    • 对于引用类型,由于引用类型的实例在堆上,栈上的副本是该变量在堆上的引用地址
  1. UncaughtExceptionHandleruncaughtException()中接收应用所发生的异常,如果在该方法内再次发生异常就会导致进入无限崩溃状态

  2. build.gradlecompileSdkVersion,minSdkVersion,targetSdkVersion这三个参数的说明:

    compileSdkVersion:编译应用使用的SDK版本,单纯在编译时使用。使用最新的编译SDK的好处就在于可以及时了解到API的状态例如弃用并可以提前使用新的API。此处需要注意一点:如果使用最新的support library,compileSdkVersion的版本至少要大于support的版本以保证编译通过。

    minSdkVersion应用可以运行的最低版本主要可以用于在应用使用高于该版本的api时进行提示,避免运行过程出现崩溃。当依赖多个Module都制定了minSdkVersion时,自身推荐使用依赖库中最大的minSdkVersion避免出现问题。如果使用了比依赖库小的版本号,可以使用tools:overrideLibrary标志避免提示。

    targetSdkVersion应用向前兼容的主要依据。为了保证老Apk在新版本系统上的兼容性,只要老Apk的targetSdkVersion版本不发生变化,在新系统依然会保持老系统上的行为。
    Android系统通过获取apk配置的targetSdkVersion在调用系统对应Api时进行版本判断去执行不同的逻辑。

这三者的大小顺序应该为 minSdkVersion <= targetSdkVersion <= compileSdkVersion这个是比较合理的顺序。

  1. ==equals()的区别

    • ==:所以基本类型比较的是值是否相等,所以引用类型比较的是两者在内存中存放的地址(堆内存地址)
    • equals():默认比较的是对象的内存地址值,如果重写了equals(),则按照重写规则比较(例如String 重写了equals()这样就变成值比较)。
      • hashcode():在比较equals()之前会先比较hashcode,如果不同则表示两个值不相等,若相同再进行equals()比较
  2. 自动装箱/拆箱相关知识

    以下拿intInteger举例:int就是原始类型,Integer就是包装类型。

    自动装箱将原始类型转换为包装类型 ── Interger x = 1000=>Integer x = Integer.valueOf(1000);转换成如下代码

    自动拆箱将包装类型转换为原始类型──Integer x = 1000;System.out.println(x)=>System.out.println(x.intValue())转换成如下代码

    拓展知识:

    Integer的缓存策略:Integer中存在一个缓存池(IntegetCache)将使用频繁的值进行缓存避免创建过多对象提高性能。默认缓存池范围为-128~127,如果在该范围内返回的就是 IntegerCache中的对象,否则返回new Integer(XX)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Integer a = 127;
    Integer a1 = 127;

    Integer b = 128;
    Integer b1 = 128;

    System.out.println(a==a1); //true 返回的实际上是缓存池的对象
    System.out.println(a.equals(a1)); //true
    System.out.println(b==b1); //false 重新new Integer
    System.out.println(b.equals(b1)); //true
  1. Linux中的epoll、select、inotify机制简析

    都是IO多路复用机制,一个进程可以监视多个描述符,一旦某个描述符就位,就会通知系统回写。

    IO多路复用:内核一旦发现进程指定的一个或者多个IO条件准备读取,就会通知该进程。

    优势在于系统开销小,系统不需要创建进程/线程,也无需维护。

    • inotify:允许监控程序打开一个独立文件描述符,并针对事件集监控一个或多个文件,例如:打开、关闭、重命名、创建、删除等功能。(用于FileObserver`监听对应文件的打开、修改、创建等事件)

      • inotify_init()创建一个监听文件变动的inotify实例,并返回指向该实例的文件描述符(fd)
      • inotify_add_watch增加对文件或目录的监控,并指定监控事件
      • inotify_rm_watch移除对文件或目录的监控
    • select:允许进程指示内核等待多个事件的任何一个发送,并只有在一个或多个事件发生或经历一段指定的时间后才唤醒。

      select需要遍历所有句柄才可以获取到哪个句柄有事件通知,并且最多支持1024个句柄,超过则可能导致溢出异常。

    • epoll:epoll使用一个文件描述符管理多个文件描述符,将用户关系的文件描述符的事件存放到一个内核的事件表中。

      • epoll_create(int size):告诉内核需要监听的文件描述符个数

      • epoll_ctl(int epfd,int op,int fd,struct epoll_event *event):对指定文件描述符进行op操作

        • epfd:epoll_create 返回的文件描述符

        • op:1)EPOLL_CTL_ADD 增加监听事件

          ​ 2)EPOLL_CTL_DEL 删除监听事件

          ​ 3)EPOLL_CTL_MOD 修改监听事件

        • fd:需要监听的文件描述符

        • event:告诉内核需要监听的事件

      • epoll_wait():等待epfd的监听回调

        epoll对于句柄事件的选择不是遍历的,当事件响应时会通知到epoll。

        epoll有两种工作模式:

        • LT模式-水平触发(默认模式):epoll_wait检测到描述符事件时会通知到应用程序,应用程序可以不立即处理该事件,等待下次epoll_wait时会继续发出通知。效率较低但是不用担心数据丢失。
        • ET模式-边缘触发:epoll_wait检测到描述符事件时会通知到应用程序,应用程序必须立即处理该事件,等待下次epoll_wait时不会继续发出通知。效率最高但是需要对每个请求进行处理,避免丢失事件造成影响。必须使用非阻塞套接口,避免堵塞造成任务堵死。

Linux IO模式及 select、poll、epoll详解

  1. MarkDown在嵌套<html></html>时会产生多余的<br>,需要使用 {% raw %} {% endraw %} 包html table

  2. i++++i的区别?

    1、 i++ 返回原来的值,++i 返回加1后的值。
    2、 i++ 不能作为左值,而++i 可以。

首先解释下什么是左值(以下两段引用自中文维基百科『右值引用』词条)。

左值是对应内存中有确定存储地址的对象的表达式的值,而右值是所有不是左值的表达式的值。

一般来说,左值是可以放到赋值符号左边的变量。但

能否被赋值不是区分左值与右值的依据。比如,C++的const左值是不可赋值的;而作为临时对象的右值可能允许被赋值。左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址。

i++的字节码表示为:

1
2
3
4
5
37: lload         9
39: dup2 //复制栈顶数据并压入栈顶,此时压入为i
40: lconst_1
41: ladd
42: lstore 9

++i的字节码表示为:

1
2
3
4
5
37: lload         9
39: lconst_1
40: ladd
41: dup2 //复制栈顶数据并压入栈顶,此时压入为i+1
42: lstore 9
  1. Boolean在数组中占到了一个字节,在单独变量中等价于int占用了4个字节。

  2. 快速失败(fast-fail)安全失败(safe-fail)的概念

    快速失败:迭代器遍历集合时,在过程中对集合的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException

    因为遍历过程中会使用一个modCount变量,遍历期间如果内容发生了变化,modCount会发生改变,迭代器在执行hasNext()/next()时,都会检测该值是否发生变化,发生变化则终止遍历并抛出异常。

    java.util下的类都是快速失败的!

    安全失败:迭代器遍历的不是原有集合,而是原有集合的复制集合

    因为遍历的是复制集合,所以遍历期间原有集合发生变化不会影响到遍历过程,就不会触发异常抛出。由于遍历的是复制集合,导致遍历时无法获取最新的修改。

    java.util.concurrent下的类都是安全失败的!

  3. Class.forName()ClassLoader.class的区别

    Class.forName()

    1
    2
    3
    public static Class<?> forName(String name, /*要加载的Class名字*/
    2222222222 boolean initialize,/*是否需要初始化-调用静态代码快及静态变量初始化*/
    ClassLoader loader/*指定ClassLoader进行加载*/)

    执行时默认会调用静态代码块static{...},以及分配静态变量存储空间

    ClassLoader.loadClass()

    1
    protected Class<?> loadClass(String name, boolean resolve)

    执行时不会对类进行初始化,只是将类加载到了虚拟机中。

  4. 编译期Debug

    配置编译命令终端输入 ./gradlew :app:clean :app:assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true,此时进入等待状态

    配置Run/Debug Configurations,新增一个Remote配置,使用默认配置即可

    切换到Remote配置,点击Debug按钮,然后debug attach成功

    此时运行Make Project等待断点执行到

  5. JNI抛出Java异常

    1
    2
    3
    4
    5
    6
    7
    void throwException(JNIEnv *env, char *msg) {
    jclass exClass;
    char *className = "java/lang/NullPointerException";
    exClass = env->FindClass(className);
    //调用ThrowNew 抛出异常
    env->ThrowNew(exClass, msg);
    }

    ThrowNew的实现方法在jni_interal.cc

    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    int ThrowNewException(JNIEnv* env, jclass exception_class, const char* msg, jobject cause)
    REQUIRES(!Locks::mutator_lock_)
    {
    // Turn the const char* into a java.lang.String.
    ScopedLocalRef<jstring> s(env, env->NewStringUTF(msg));
    if (msg != nullptr && s.get() == nullptr) {
    return JNI_ERR;
    }

    // Choose an appropriate constructor and set up the arguments.
    jvalue args[2];
    const char* signature;
    if (msg == nullptr && cause == nullptr) {
    signature = "()V";
    } else if (msg != nullptr && cause == nullptr) {
    signature = "(Ljava/lang/String;)V";
    args[0].l = s.get();
    } else if (msg == nullptr && cause != nullptr) {
    signature = "(Ljava/lang/Throwable;)V";
    args[0].l = cause;
    } else {
    signature = "(Ljava/lang/String;Ljava/lang/Throwable;)V";
    args[0].l = s.get();
    args[1].l = cause;
    }
    jmethodID mid = env->GetMethodID(exception_class, "<init>", signature);
    if (mid == nullptr) {
    ScopedObjectAccess soa(env);
    LOG(ERROR) << "No <init>" << signature << " in "
    << mirror::Class::PrettyClass(soa.Decode<mirror::Class>(exception_class));
    return JNI_ERR;
    }

    ScopedLocalRef<jthrowable> exception(
    env, reinterpret_cast<jthrowable>(env->NewObjectA(exception_class, mid, args)))
    ;
    if (exception.get() == nullptr) {
    return JNI_ERR;
    }
    ScopedObjectAccess soa(env);
    soa.Self()->SetException(soa.Decode<mirror::Throwable>(exception.get()));
    return JNI_OK;
    }

    //thread.cc
    void Thread::SetException(ObjPtr<mirror::Throwable> new_exception) {
    CHECK(new_exception != nullptr);
    // TODO: DCHECK(!IsExceptionPending());
    //此处设置了 jni插入的异常信息 ,会触发ART的checkPoint的检测,检测到该信息时抛出对应异常
    tlsPtr_.exception = new_exception.Ptr();
    }

    插入位置在方法调用循环处。

  6. 使用Gradle命令更新 dependenices

    1
    ./gradlew --configure-on-demand
  1. Markdown常用操作

    1
    2
    3
    [显示内容](#标题) 锚点跳转

    {%post_link 文章标题%} 文章跳转
  1. 设置hexo的博客置顶规则,按照tops进行配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var posts = locals.posts.data.sort(function (a, b) {
    //两个post都定义了top
    if (a.top && b.top) {
    //按日期将降序
    if (a.top == b.top) return b.date - a.date;
    //按top排序
    else return b.top - a.top;
    }
    //定义了top的排前面
    else if (a.top && !b.top) {
    return -1;
    }
    else if (!a.top && b.top) {
    return 1;
    }
    //没有定义top就按照日期降序
    else return b.date - a.date;
    });

    需要配置在node-modules/hexo-generator-index2/lib/generator.js里面

  2. kotlin中的inlinenoinlinecrossinline的作用、

    inline:函数进行内联,将inline fun直接插入到调用函数的代码内,优化代码结构,从而减少函数类型对象的创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fun main(args:Array<String>){
    testInline()
    print("world")
    }

    inline fun testInline(){
    print("Hello")
    }

    输出结果:
    HelloWorld

    实际编译结果:
    fun main(args:Array<String>){
    print("Hello")
    print("world")
    }

    noinline:局部关掉函数内联优化,摆脱inline不能使用函数类型的参数当对象用的限制。作用于函数的参数且参数必须为函数类型

    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
    36
    inline fun test(noinline a : Int) {
    //Modifier 'noinline' is allowed only for function parameters of an inline function
    //错误使用方法 `noinline`只能使用在函数参数上
    }

    inline fun test(a: Int, b: (Int) -> Unit): (Int) -> Unit {
    return b
    //Illegal usage of inline-parameter 'b'
    //错误使用方法 不能直接返回 函数类型,因为经过内联后,函数类型无法被调用,失去了存在意义
    //这种错误写法,编译器可以直接检测出来
    }

    inline fun test(a:Int , noinline b :(String)->Unit) : (String) -> Unit {
    println(a)
    b("World")
    return b
    }

    fun main(args:Array<String>){
    println("Hello")
    test(3){ it->
    println(it)
    }
    }

    输出结果:
    Hello
    3
    World

    实际编辑结果:
    fun main(args:Array<String>){
    println("Hello")
    println(3)
    b.invoke("World")
    }
`crossinline`:局部加强函数内联优化,将内联函数里的函数类型参数可以当作对象使用。

首先声明两个概念:

- Lambda表达式不允许使用`return`,可以使用`return@XX`来指定返回位置

  
1
2
3
4
5
6
7
8
9
10
11
fun test(a:()->Unit){
...
}

fun main(args:Array<String>){
test {
...
return //这个是不被允许使用的
//return@test 这个是可以的
}
}
- 只有被`inline`修饰的内联函数的`Lambda表达式`可以使用return。在`间接调用`是被禁止的操作
1
2
3
4
5
6
7
8
9
10
11
inline fun test(action:()->Unit){
println("Hello")
action()
}

fun main(args:Array<String>){
test{
println("World")
return //是被允许这么做的
}
}
`crossinline`实质为了**声明函数参数的`lambda`不能写`return`,避免lambda中的return影响外部的执行流程**。 使用`inline`修饰函数时需要注意以下几点: - `inline`修饰函数,最好函数参数也是`函数类型`,否则无法获得性能提升 - **避免内联大型函数**,因为`inline`会增加代码的生成量 - `inline`修饰的函数不持有函数的对象引用,也不能将函数参数传递给另一个函数
1
2
3
4
5
6
7
fun test123(a:()->Unit){

}

inline fun test12(a:()->Unit){
test123(a) //无法编译
}

  1. 匿名内存(Ashmem)

    以Android Q为目标平台(targetVersion 29)的应用无法直接使用ashmem,必须通过NDK的AsharedMemory来访问共享内存,也无法直接使用ioctl,必须改为AShredMemory来创建共享内存区域。

  2. 当出现您的连接不是私密连接时,点击高级后,并直接输入thisisunsafe关键字并回车。

  3. 1
    2
    3
    4
    5
    class A{
    void test(){
    B b = new B;
    }
    }

    此时调用A类,是否触发B的加载?

  1. 如何获取当前Activity展示的Dialog?

    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
    36
    37
    38
    39
    40
    41
    42
    43
        @SuppressWarnings("unchecked")
    public static List<View> getViewRoots() {

    List<View> viewRoots = new ArrayList<>();

    try {
    Object windowManager;
    windowManager = Class.forName("android.view.WindowManagerGlobal")
    .getMethod("getInstance").invoke(null);
    //WindowManagerGlobal 内部持有 mRoots(ViewRootImpl列表) mViews(DecorView列表)
    Field rootsField = windowManager.getClass().getDeclaredField("mRoots");
    rootsField.setAccessible(true);

    Field stoppedField = Class.forName("android.view.ViewRootImpl")
    .getDeclaredField("mStopped");
    stoppedField.setAccessible(true);
    //ViewRootImpl mView对应了 DecorView
    Field rootViewField = Class.forName("android.view.ViewRootImpl")
    .getDeclaredField("mView");
    rootViewField.setAccessible(true);
    //DecorView mWindow对应了 PhoneWindow
    Field windowField = Class.forName("com.android.internal.policy.DecorView")
    .getDeclaredField("mWindow");
    windowField.setAccessible(true);

    List<ViewParent> viewParents = (List<ViewParent>) rootsField.get(windowManager);
    // Filter out inactive view roots
    for (ViewParent viewParent : viewParents) {
    boolean stopped = (boolean) stoppedField.get(viewParent);
    if (!stopped) {
    View view = (View) rootViewField.get(viewParent);
    Window w = (Window) windowField.get(view);
    //Window setCallback 一般对应Window的创建者
    Log.w("sss",w.getCallback().toString());
    viewRoots.add(view);
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    }

    return viewRoots;
    }

    当有dialog弹出时,此时页面就会有两个window对象。通过反射也可以获取两个对象。

    一般Activity和Dialog的布局都比较复杂,可能会存在标题栏等信息,需要封装一层DecorView。

    然后通过windowManager管理。

    ToastPopupWindow布局比较简单,直接addView即可。

  2. PrecomputedText

  1. Mac 下 MAT启动失败

    原因:mat暂不支持高版本的jdk,需要手动指定jdk版本

    解决方法:

    • 打开 /Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini

    • 添加如下配置

      1
      2
      -vm
      /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/bin # 替换自己的jdk目录
  1. Android 6.0以下的机型可能存在SVG图片加载异常导致崩溃,使用app:srcCompat替代android:src使用,可以兼容后续更新的SVG语法。

  2. dependencySubstitution

    dependencySubstitution接收一系列替换规则,允许你通过substitute函数为项目中的依赖替换为你希望的依赖项,例如:

    1
    2
    3
    4
    5
    6
    configurations.all {
    resolutionStrategy.dependencySubstitution {
    //可以 在远程依赖 与 本地依赖进行转换,满足源码依赖要求
    substitute module("io.reactivex.rxjava2:rxjava") with module("io.reactivex.rxjava3:rxjava:3.0.0-RC1")
    }
    }
  3. 扩大View的点击区域

    view#setOnTouchDelegate

  4. ll;

  5. dff


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!