Git Product home page Git Product logo

interviewmemoirs's People

Contributors

soulrelay avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

interviewmemoirs's Issues

13、Android 加载大图方法

加载大图,一般为了尽可能避免OOM,有如下几种方法(包括但不局限于)

  • 对于图片显示:根据需要显示图片控件的大小对图片进行压缩显示。
    具体操作:
    1、将inJustDecodeBounds的值设置成true,我们就可以从BitmapFactory.Options中获取到原始图片的尺寸和类型。
    2、根据使用场景计算采样比inSampleSize,加载缩小比例的图片到内存中
    3、inJustDecodeBounds设置为false,传递inSampleSize给解码器创建缩小比例的图片
  • 如果图片数量非常多:则会使用LruCache等缓存机制,将所有图片占据的内容维持在一个范围内
  • 如果单张图片非常巨大,不允许压缩而且要按照原图尺寸加载,考虑到屏幕大小和内存占用,可以通过BitmapRegionDecoder完成局部加载

14、【NDK Lab】CMAKE 创建新的C或C++原生库(基础篇)

1、创建新项目(Create New Project)

点击File — New — New Project,把Include C++ Support前面的CheckBook勾上,然后依次进行接下来的操作

create new project

2、配置C++支持功能(Customize C++ Support)

在Customize C++ Support界面默认即可

customize C++ Support

  • C++ Standard
    指定编译库的环境,其中Toolchain Default使用的是默认的CMake环境;C++ 11也就是C++环境。
  • Exceptions Support
    如果选中复选框,则表示当前项目支持C++异常处理,如果支持,在项目Module级别的build.gradle文件中会增加一个标识 -fexceptions到cppFlags属性中,并且在so库构建时,gradle会把该属性值传递给CMake进行构建。
  • Runtime Type Information Support
    同理,选中复选框,项目支持RTTI,属性cppFlags增加标识-frtti

JniCmake项目主体结构

3、CMakeLists.txt构建

CMakeLists.txt文件用于配置JNI项目属性,主要用于声明CMake使用版本、so库名称、C/CPP文件路径等信息,下面是该文件内容:

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
  • cmake_minimum_required(VERSION 3.4.1)
    CMake最小版本使用的是3.4.1。

  • add_library()
    配置so库信息(为当前脚本文件添加库)

  • native-lib这个是声明引用so库的名称,在项目中,如果需要使用这个so文件,引用的名称就是这个。值得注意的是,实际上生成的so文件名称是libnative-lib。当Run项目或者build项目是,在Module级别的build文件下的intermediates\transforms\mergeJniLibs\debug\folders\2000\1f\main
    下会生成相应的so库文件。
  • SHARED这个参数表示共享so库文件,也就是在Run项目或者build项目时会在目录intermediates\transforms\mergeJniLibs\debug\folders\2000\1f\main
    下生成so库文。此外,so库文件都会在打包到*.apk里面,可以通过选择菜单栏的**Build->Analyze Apk...**查看apk中是否存在so库文件,一般它会存放在lib目录下。
  • src/main/cpp/native-lib.cpp 构建so库的源文件。
  • STATIC:静态库,是目标文件的归档文件,在链接其它目标的时候使用。
  • SHARED:动态库,会被动态链接,在运行时被加载。
  • MODULE:模块库,是不会被链接到其它目标中的插件,但是可能会在运行时使用dlopen-系列的函数动态链接
  • include_directories
    配置头文件路径目录

  • find_library()
    这个方法与我们要创建的so库无关而是使用NDK的Apis或者库,默认情况下Android平台集成了很多NDK库文件,所以这些文件是没有必要打包到apk里面去的。直接声明想要使用的库名称即可(猜测:貌似是在Sytem/libs目录下)。在这里不需要指定库的路径,因为这个路径已经是CMake路径搜索的一部分。如示例中使用的是log相关的so库。

  • log-lib这个指定的是在NDK库中每个类型的库会存放一个特定的位置,而log库存放在log-lib中
  • log指定使用log库
  • target_link_libraries()
    如果你本地的库(native-lib)想要调用log库的方法,那么就需要配置这个属性,意思是把NDK库关联到本地库。
  • native-lib要被关联的库名称
  • ${log-lib}要关联的库名称,要用大括号包裹,前面还要有$符号去引用。

实际上,我们可以自己创建CMakeLists.txt文件,而且路径不受限制,只要在build.gradle中配置externalNativeBuild.cmake.path来指定该文件路径即可

4、MainActivity调用和运行结果

MainActivity

JniCmake运行效果图

2、Android线程池相关之队列与池大小的关系

JAVA线程池中队列与池大小的关系

JAVA线程中对于线程池(ThreadPoolExecutor)中队列,池大小,核心线程的关系

  • 1:核心线程:简单来讲就是线程池中能否允许同时并发运行的线程的数量
  • 2:线程池大小:线程池中最多能够容纳的线程的数量。
  • 3:队列:对提交过来的任务的处理模式。

对于线程池与队列的交互有个原则:

如果队列发过来的任务,发现线程池中正在运行的线程的数量小于核心线程,则立即创建新的线程,无需进入队列等待。如果正在运行的线程等于或者大于核心线程,则必须参考提交的任务能否加入队列中去。

1:提交的任务加入队列中

  • 如果提交的任务能加入队列,考虑下队列的值是否有设定,如果没有设定,那么也就是不能创建新的线程,只能在队列中等待,因为理论上队列里面可以容纳无穷大的任务等待。换句话说,此时的线程池中的核心线程数就是池中能否允许的最大线程数。那么池的最大线程数就没有任何意义了。
  • 如果提交的任务能加入队列,队列的值是有限定的,那么首先任务进入队列中去等待,一旦队列中满了,则新增加的任务就进入线程池中创建新的线程。一旦线程池中的最大线程数超过了,那么就会拒绝后面的任务。

2:如果提交的任务不能加入队列

  • 提交的任务不能加入队列,此时就会创建新的线程加入线程池中,一旦超过线程池中最大的数量,则任务被拒绝。

3:队列的三种策略:

  • SynchronousQueue 直接提交,也就是上面讲到的所有任务不进入队列去等待。此时小于核心线程就增加,多于或等于核心线程数时,还是增加线程,最大为线程池中的最大允许。超出就拒绝。
  • LinkedBlockingQueue 无界队列 此时超过核心线程后的任务全部加入队列等待,系统最多只能运行核心线程数量的线程。这种方法相当于控制了并发的线程数量。
  • ArrayBlockingQueue 有界队列 此时超过核心线程后的任务先加入队列等待,超出队列范围后的任务就生成线程,但创建的线程最多不超过线程池的最大允许值。

4:如下源代码:

  • 固定数量的线程池
  public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }

可以看出,这个线程池的核心线程与最大线程为一个值,不等待,超出核心线程一定时间后的线程就被回收掉了。最多同时运行nThreads数量的线程。

  • 单线程池(其实个人认为可以理解为就是核心线程为1的固定线程池)
     public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
    }
  • 动态线程池(无界线程池)
 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }

可以看出,核心线程池为0,线程池的最大是无限,等待时间为60秒,队列为直接提交。
也就是说每次一个任务都直接生成一个线程,线程的无上限,但是一旦线程池中出现了空闲超过60秒的线程则被回收掉。

总的说来,在使用中我们一般使用Executors类的静态方法来创建线程池,除非我们对于线程池非常理解才能自己去灵活的规划线程池类(可以用来继承ThreadPoolExecutor)

26、快手、作业盒子、头条、摩拜、作业盒子面试题(精简篇)

快手

  • App的稳定性你是如何保证的?
  • 看过或者参与过什么开源项目?
  • 算法:两个链表删除元素相同的节点
  • MVC和MVP的区别?
  • 内存泄漏是如何发现的?怎么处理的?
  • 现场设计一个下载库
  • Activity的启动模式?
  • 讲一下Service?

头条

  • equals和hashcode的区别
  • 讲一下线程同步?举一个线程同步的例子
  • 线程池的了解
  • http状态码都有那些?
  • http1和http2的区别?
  • 如何停止一个线程
  • 性能优化都做了哪些?
  • 项目中最能体现技术深度的地方?
  • Object类中的方法,finalize()?
  • https和http的区别?
  • 堆和栈的区别?
  • 发生死循环时内存是什么状态?

作业盒子

  • 讲一下AsyncTask的原理?
  • 动画有哪几类?做过动画么?
  • Java泛型、同步锁的问题
  • Handler的原理?
  • 手写一个多线程的例子?
  • 自定义View,如何实现一个类似音乐播放器中的音轨View?

摩拜单车

  • 工厂模式?
  • 了解RxJava么?
  • 面向对象原则 SLOID讲一下?
  • 算法:求一个数组的真子集
  • 动画?
  • 讲一下下拉刷新&加载更多的原理?

Kika输入法

  • 自定义View,父容器中linearlayout内部有很多子view,子view是如何自动换行的?
  • AsyncTask原理?
  • 手写线程同步:两个线程,一个输出奇数,一个输出偶数
  • 讲一下Fragment中的transaction
  • Service和IntentService的区别?
  • 事件分发讲一下?
  • Handler的原理?
  • ListView性能优化?
  • volatile的原理?
  • merge标签的原理?
  • Activity、Window、View三者的区别和联系?

Zenjoy

  • Service和IntentService的区别?
  • JVM的原理?内存是如何分配的?
  • 广播和Service讲一下
  • App默认分配的内存是多少?
  • https的原理?
  • 加密算法有哪些?
  • App如何实现多进程?

链家

  • 对插件化的了解?
  • Gradle对App的优化?
  • 下拉刷新的实现原理?
  • 算法:连续子数组的最大和

LeanCloud

  • 线程和进程的区别?
  • 数组和链表的区别?
  • 算法:获取一个单链表的第K个元素
  • Activity、Window、View的区别和联系
  • 线程之间是如何通信的?

逻辑思维

  • 事件分发?
  • 算法
    • 一次循环,对0-99的数进行排序
    • 一次循环,对0 1 2这三个数进行排序,不能引入额外的空间
  • view中执行invalidate()方法后,发生了什么?

百度

  • Activity的启动流程

一点资讯

  • Activity的启动流程
  • OnClick事件在事件分发流程中处于什么位置?
  • 算法:树的遍历

7、OkHttp3线程池相关之Dispatcher中的ExecutorService

先从Okhttp3的请求实现说起(包括同步请求和异步请求)

基本使用

同步请求:

private static final String DESTINATION_ADDRESS = "https://github.com/soulrelay";
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder()
        .url(DESTINATION_ADDRESS)
        .build();
Response response = client.newCall(request).execute();

异步请求:

private static final String DESTINATION_ADDRESS = "https://github.com/soulrelay";
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder()
        .url(DESTINATION_ADDRESS)
        .build();
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
    }
});

小结:

  • 不管是同步请求还是异步请求,都需要初始化OkHttpClient以及创建一个Request,然后再调用OkHttpClinet的newCall方法,创建出一个RealCall对象
  • 对于同步请求,是调用RealCall的execute方法;而异步请求,则是调用RealCall的enqueue方法实现

简介

  • OkHttpCLient:在Okhttp库中,OkHttpClient处于一个中心者的地位,很多功能都需要通过它来转发或者实现的。在创建的时候,初始化了很多功能类,比如缓存,拦截器,网络连接池,分发器等类。 由于初始化比较复杂,OkHttpClient内部提供了Builder模式来创建,一般情况下,OkHttpClient是唯一的
  • Request:是用来构建一个请求对象的,符合Http请求的标准,包含了请求头,方法等等属性,较为复杂,因此同样提供Builder模式构建。
  • Response:是用来构建一个响应对象的,包含了响应头,响应码,数据等等属性,同样也提供Builder模式构建

RealCall

// 同步
client.newCall(request).execute()
// 异步
client.newCall(request).enqueue(Callback)

同步和异步请求,都是调用OkHttpClient的newCall方法创建一个RealCall对象,然后通过这个对象,执行请求的

@Override 
public Call newCall(Request request) {
    return new RealCall(this, request);
}

execute 同步请求

@Override 
public Response execute() throws IOException {
     // 方法检测,execute只能调用1次
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    try {
       // 将RealCall添加到Dispatcher中的同步请求队列中
      client.dispatcher().executed(this);
       // 获取响应的数据
       // Okhttp会对数据(请求流和响应流)进行拦截进行一些额外的处理,
       // 比如失败重连,添加请求头,没网络优先返回缓存数据等等。
       // 这一拦截过程,采用责任链模式来实现的,和Android中的事件传递机制差不多
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
       // 请求完成或者取消,都会将RealCall从同步请求队列中移除
      client.dispatcher().finished(this);
    }
}

enqueue-异步请求

@Override 
public void enqueue(Callback responseCallback) {
    // 和execute一样,enqueue也只能调用1次
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    // 执行请求
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

AsyncCall

AsyncCall–实际上是一个Runnable

public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

NamedRunnable,继承Runnable的抽象类,根据传入的名称来修改线程名称,抽象出执行任务的execute方法

final class AsyncCall extends NamedRunnable {
    // 其他省略,execute方法式真正执行请求
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
}

AsyncCall中的execute方法,是请求任务执行的方法,获取相应数据最终也是调用了getResponseWithInterceptorChain方法。

private Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!retryAndFollowUpInterceptor.isForWebSocket()) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(
        retryAndFollowUpInterceptor.isForWebSocket()));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

Dispatcher

原理:

  • 对于异步请求来说,Dispatcher类内部持有一个正在执行(运行中)异步任务的队列和一个等待执行(就绪)异步请求的队列,以及一个线程池
  • 这个线程池,没有常存的核心线程,最多线程数为Integer.MAX_VALUE,线程空闲时存活时间为60秒,而SynchronousQueue #2 是不保存任务的,所以只要把任务添加进去就会执行
  • OkHttp不是在线程池中维护线程的个数,线程是一直在Dispatcher中直接控制。线程池中的请求都是运行中的请求。这也就是说线程的重用不是线程池控制的,通过源码我们发现线程重用的地方是请求结束的地方finished(AsyncCall call) ,而真正的控制是通过promoteCalls方法, 根据maxRequests和maxRequestsPerHost来调整runningAsyncCalls和readyAsyncCalls,使运行中的异步请求不超过两种最大值,并且如果队列有空闲,将就绪状态的请求归类为运行中
public final class Dispatcher {
  //最大请求数
  private int maxRequests = 64;
  //相同host最大请求数
  private int maxRequestsPerHost = 5;
  //请求执行,懒加载
  private ExecutorService executorService;
  //就绪状态的异步请求队列
  private final Deque<AsyncCall> readyCalls = new ArrayDeque<>();
  //运行中的异步请求队列
  private final Deque<AsyncCall> runningCalls = new ArrayDeque<>();
  //进行中的同步请求,包括那些尚未完成被取消的请求
  private final Deque<Call> executedCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }

  public Dispatcher() {
  }

  public synchronized ExecutorService getExecutorService() {
    if (executorService == null) {
       //corePoolSize 为 0表示,没有核心线程,所有执行请求的线程,使用完了如果过期了(keepAliveTime)就回收了,
      //maximumPoolSize 无限大的线程池空间
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

  public synchronized void setMaxRequests(int maxRequests) {
    if (maxRequests < 1) {
      throw new IllegalArgumentException("max < 1: " + maxRequests);
    }
    this.maxRequests = maxRequests;
    promoteCalls();
  }

  public synchronized int getMaxRequests() {
    return maxRequests;
  }

  public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
    if (maxRequestsPerHost < 1) {
      throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
    }
    this.maxRequestsPerHost = maxRequestsPerHost;
    promoteCalls();
  }

  public synchronized int getMaxRequestsPerHost() {
    return maxRequestsPerHost;
  }

  synchronized void enqueue(AsyncCall call) {
    if (runningCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningCalls.add(call);
      getExecutorService().execute(call);
    } else {
      readyCalls.add(call);
    }
  }

  //取消带有tag的所有请求
  public synchronized void cancel(Object tag) {
    for (AsyncCall call : readyCalls) {
      if (Util.equal(tag, call.tag())) {
        call.cancel();
      }
    }

    for (AsyncCall call : runningCalls) {
      if (Util.equal(tag, call.tag())) {
        call.get().canceled = true;
        HttpEngine engine = call.get().engine;
        if (engine != null) engine.cancel();
      }
    }

    for (Call call : executedCalls) {
      if (Util.equal(tag, call.tag())) {
        call.cancel();
      }
    }
  }

  //异步请求结束
  //当该异步请求结束的时候,会调用此方法,
  //用于将运行中的异步请求队列中的该请求移除并调整请求队列
  //此时就绪队列中的请求就可以进入运行中的队列
  synchronized void finished(AsyncCall call) {
    if (!runningCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
    promoteCalls();
  }

  //根据maxRequests和maxRequestsPerHost来调整
  //runningAsyncCalls和readyAsyncCalls
  //使运行中的异步请求不超过两种最大值
  //并且如果队列有空闲,将就绪状态的请求归类为运行中。
  private void promoteCalls() {
    //运行中的异步请求队列的请求数大于最大请求数,那么就没必要去将就绪状态的请求移动到运行中。
    //其实就是说,如果有超过最大请求数的请求正在运行,是不需要将其移出队列的,继续运行完即可
    if (runningCalls.size() >= maxRequests) return;
    //如果就绪的队列为空,那就更没有必要移动了,因为都没有。
    if (readyCalls.isEmpty()) return; 
    //遍历就绪队列
    for (Iterator<AsyncCall> i = readyCalls.iterator(); i.hasNext(); ) {
      //取出一个请求
      AsyncCall call = i.next();
      //如果当前请求对应的host下,没有超过maxRequestsPerHost
      //那么将其从就绪队列中移除,并加入在运行队列。
      if (runningCallsForHost(call) < maxRequestsPerHost) { 
        //移除        
        i.remove();
        //加入运行队列
        runningCalls.add(call);
        //立即执行该请求
        getExecutorService().execute(call);
      }
        //如果运行队列已经到达了最大请求数上限
        //此时如果还有就绪中的请求,也不管了
      if (runningCalls.size() >= maxRequests) return; 
    }
  }

  //对比已有的运行中的请求和当前请求的host
  private int runningCallsForHost(AsyncCall call) {
    int result = 0;
    for (AsyncCall c : runningCalls) {
      if (c.host().equals(call.host())) result++;
    }
    return result;
  }

  synchronized void executed(Call call) {
    executedCalls.add(call);
  }

  //同步请求结束
  //当该同步请求结束的时候,会调用此方法,
  //用于将运行中的同步请求队列中的该请求移除
  synchronized void finished(Call call) {
    if (!executedCalls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
  }

  public synchronized int getRunningCallCount() {
    return runningCalls.size();
  }

  public synchronized int getQueuedCallCount() {
    return readyCalls.size();
  }
}

4、volatile用法

  • 1、在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
  • 2、保证此变量对所有的线程的可见性,这里的“可见性”,即:当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
  • 3、禁止指令重排序优化
  • 4、volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

5、final用法

  • 1、当用final修饰一个类时,表明这个类不能被继承
  • 2、 final修饰的方法不能被重写
  • 3、 final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
  • 4、引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的
  • 5、若某个参数被final修饰了,则代表了该参数是不可改变的

1、Android关于ThreadLocal的思考和总结

前言

Handler机制引出ThreadLocal

  • 关于ThreadLocal的分析,首先得从Android的消息机制谈起,可能我们最先想到的就是Android消息机制的上层接口Handler
  • 为了避免ANR,我们会通常把耗时操作放在子线程里面去执行,因为子线程不能更新UI,所以当子线程需要更新UI的时候就需要借助到Android的消息机制,也就是Handler机制了

关于Handler的原理,不是本文剖析的重点,这里仅给出一些相关结论,同时引出今天的主角ThreadLocal

  • Handler的处理过程运行在创建Handler的线程里
  • 一个Looper对应一个MessageQueue
  • 一个线程对应一个Looper
  • 一个Looper可以对应多个Handler
  • 线程是默认没有Looper的,线程需要通过Looper.prepare()、绑定Handler到Looper对象、Looper.loop()来建立消息循环
  • 主线程(UI线程),也就是ActivityThread,在被创建的时候就会初始化Looper,所以主线程中可以默认使用Handler
  • 可以通过Looper的quitSafely()或者quit()方法终结消息循环,quitSafely相比于quit方法安全之处在于清空消息之前会派发所有的非延迟消息。
  • 不确定当前线程时,更新UI时尽量调用post方法

如何保证一个线程对应一个Looper,同时各个线程之间的Looper互不干扰就引出了接下来要讨论的ThreadLocal

 public final class Looper {
    private static final String TAG = "Looper";
    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
    ....//省略
    }

分析

案例展示及运行结果

这里先给出ThreadLocal和InheritableThreadLocal的简单实用demo

public class ThreadLocalTest {
    static final String CONSTANT_01 = "CONSTANT_01";
    static final String CONSTANT_02 = "CONSTANT_02";

    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<String> threadLocal = new ThreadLocal<String>();
        threadLocal.set(CONSTANT_01);

        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>();
        inheritableThreadLocal.set(CONSTANT_01);

        Thread thread_1 = new TestThread(threadLocal, inheritableThreadLocal);
        thread_1.setName("thread_01");
        thread_1.start();

        thread_1.join();

        System.out.println("   " + Thread.currentThread().getName() + "  ******************************************");
        System.out.println("   " + Thread.currentThread().getName() + "   \tThreadLocal: " + threadLocal.get());
        System.out.println("   " + Thread.currentThread().getName() + "   \tInheritableThreadLocal: " + inheritableThreadLocal.get());
        System.out.println("   " + Thread.currentThread().getName() + "  ******************************************");
    }
}

class TestThread extends Thread {
    ThreadLocal<String> threadLocal;
    InheritableThreadLocal<String> inheritableThreadLocal;

    public TestThread(ThreadLocal<String> threadLocal, InheritableThreadLocal<String> inheritableThreadLocal) {
        super();
        this.threadLocal = threadLocal;
        this.inheritableThreadLocal = inheritableThreadLocal;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "******************************************");
        System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get());
        System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get());
        System.out.println(Thread.currentThread().getName() + "******************************************\n");

        threadLocal.set(ThreadLocalTest.CONSTANT_02);
        inheritableThreadLocal.set(ThreadLocalTest.CONSTANT_02);

        System.out.println(Thread.currentThread().getName() + "*************(Reset Value)****************");
        System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get());
        System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get());
        System.out.println(Thread.currentThread().getName() + "*************(Reset Value)****************\n");
    }
}

运行结果:

thread_01******************************************
thread_01	ThreadLocal: null
thread_01	InheritableThreadLocal: CONSTANT_01
thread_01******************************************

thread_01*************(Reset Value)****************
thread_01	ThreadLocal: CONSTANT_02
thread_01	InheritableThreadLocal: CONSTANT_02
thread_01*************(Reset Value)****************

   main  ******************************************
   main   	ThreadLocal: CONSTANT_01
   main   	InheritableThreadLocal: CONSTANT_01
   main  ******************************************

如果这个时候你对运行结果有疑问 或者说 「我擦」怎么又突然冒出来一个InheritableThreadLocal,那么请继续往下看

ThreadLocal类结构预览

当然,我们肯定要先从ThreadLocal开始说起:

先从大体上看一下,可以发现,Java和Android中ThreadLocal的类结构(包括部分细节)还是有一些区别的,不过Android中的实现方式越来越贴近Java版

第一张图为jdk1.8.0_131中ThreadLocal的类结构:

第二张图为android-25中ThreadLocal的类结构:

这里写图片描述

ThreadLocal探秘

这里主要以Android-25(Android7.1.1)的源码为基础进行分析,其实几乎和Java版本的源码一致


首先澄清一下对ThreadLocal的错误认知:

  • ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
  • ThreadLocal的目的是为了解决多线程访问资源时的共享问题

为什么这么说那?
我们看看Android源码中是如何介绍ThreadLocal的:


This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).


描述的大致意思是这样:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。

可以这么总结:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

有时候大家会拿同步机制(如synchronized)和ThreadLocal做对比,怎么说才能不引起误解那?
可以这么理解:
对于多线程资源共享的问题,前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。但是ThreadLocal却并不是为了解决并发或者多线程资源共享而设计的

所以ThreadLocal既不是为了解决共享多线程的访问问题,更不是为了解决线程同步问题,ThreadLocal的设计初衷就是为了提供线程内部的局部变量,方便在本线程内随时随地的读取,并且与其他线程隔离。

ThreadLocal的应用场景:

  • 当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候
    如:属性动画为每个线程设置AnimationHandler、Android的Handler消息机制中通过ThreadLocal实现Looper在线程中的存取、EventBus获取当前线程的PostingThreadState对象或者即将被分发的事件队列或者当前线程是否正在进行事件分发的布尔值
  • 复杂逻辑下的对象传递
    使用参数传递的话:当函数调用栈更深时,设计会很糟糕,为每一个线程定义一个静态变量监听器,如果是多线程的话,一个线程就需要定义一个静态变量,无法扩展,这时候使用ThreadLocal就可以解决问题。

ThreadLocal源码解读

构造函数:

    public ThreadLocal() {
    }

创建一个线程的本地变量

initialValue函数:

    protected T initialValue() {
        return null;
    }

该函数在调用get函数的时候会第一次调用,但是如果一开始就调用了set函数,则该函数不会被调用。通常该函数只会被调用一次,除非手动调用了remove函数之后又调用get函数,这种情况下,get函数中还是会调用initialValue函数。该函数是protected类型的,很显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重载,以指定初始值,比如

public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return Integer.valueOf(1);
        }
    };
}

get函数:

该函数用来获取与当前线程关联的ThreadLocal的值,如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回

   public T get() {
	    //1、首先获取当前线程
        Thread t = Thread.currentThread();
        //2、根据当前线程获取一个map
        ThreadLocalMap map = getMap(t);
        //3、如果获取的map不为空,则在map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到5
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            //4、如果e不为null,则返回e.value,否则转到5
            if (e != null)
                return (T)e.value;
        }
        //5、map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的map
        return setInitialValue();
    }

   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

   private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

值得注意的是,上面getMap方法中获取的threadLocals即是Thread中的一个成员变量

 public class Thread implements Runnable {
    ...//省略
    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class.*/
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ...//省略
}

这里的inheritableThreadLocals会在下文分析InheritableThreadLocal涉及到

set函数:

set函数用来设置当前线程的该ThreadLocal的值,设置当前线程的ThreadLocal的值为value

    public void set(T value) {
        //1、首先获取当前线程
        Thread t = Thread.currentThread();
        //2、根据当前线程获取一个map
        ThreadLocalMap map = getMap(t);
        if (map != null)
        //3、map不为空,则把键值对保存到map中
            map.set(this, value);
        //4、如果map为空(第一次调用的时候map值为null),则去创建一个ThreadLocalMap对象并赋值给map,并把键值对保存到map中。
        else
            createMap(t, value);
    }

remove函数:

remove函数用来将当前线程的ThreadLocal绑定的值删除,在某些情况下需要手动调用该函数,防止内存泄露。

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap

可以看成一个HashMap,但是它本身具体的实现却与java.util.Map沾不上一点关系。只是内部的实现跟HashMap类似(通过哈希表的方式存储)。

static class ThreadLocalMap {
    static class Entry extend WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        ...//省略
 }

大致类结构如下图所示:

这里写图片描述

ThreadLocalMap中定义了Entry数组实例table,用于存储Entry。相当于使用一个数组维护一张哈希表,负载因子是最大容量的2/3

 private Entry[] table;

关于ThreadLocalMap重要函数的分析会结合下一节ThreadLocal内存泄漏的问题一并讨论


PS:Android早期版本,这部分的数据结构是通过Values实现的,Values中也有一个table的成员变量,table是一个Object数组,也是以类似map的方式来存储的。偶数单元存储的是key,key的下一个单元存储的是对应的value,所以每存储一个元素,需要两个单元,所以容量一定是2的倍数。这里的key存储的也是ThreadLocal实例的弱引用


ThreadLocal内存泄漏的问题

ThreadLocal里面使用了一个存在弱引用的map,当释放掉ThreadLocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用ThreadLocal的remove方法.

在ThreadLocal的生命周期中,都存在这些引用.

看下图(来源参考): 实线代表强引用,虚线代表弱引用.

这里写图片描述

  • 每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个ThreadLocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向ThreadLocal. 当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
  • 所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在ThreadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。  
  • 为了最小化减少内存泄露的可能性和影响,(设计中加上了一些防护措施)在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

getEntry函数:

首先从ThreadLocal的直接索引位置获取Entry e,如果e不为null并且key相同则返回e;如果e为null或者key不一致则通过getEntryAfterMiss向下一个位置查询

     private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

getEntryAfterMiss函数:

这个过程中遇到的key为null的Entry都会被擦除(Entry内的value也就没有强引用链,自然会被回收)

    private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal k = e.get();
                if (k == key)
                //命中
                    return e;
                if (k == null)
                //如果key值为null,则擦除该位置的Entry
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                //继续向下一个位置查询
                e = tab[i];
            }
            return null;
        }

set函数:
set操作也有类似的**,将key为null的这些Entry都删除,防止内存泄露

     private void set(ThreadLocal key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

小结:

  • 虽然源码中对内存泄漏做了很好的防护作用,但是很多情况下还是需要手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。
  • 所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

InheritableThreadLocal与ThreadLocal的区别

InheritableThreadLocal比ThreadLocal多一个特性,继承性,可以从父线程中得到初始值

首先浏览下 InheritableThreadLocal 类中有什么东西:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

其实就是重写了3个方法

  • InheritableThreadLocal的get()方法会调用getMap(t),而这时返回的是inheritableThreadLocals(Thread的一个成员变量)
  • 父线程往子线程中传递值是在Thread thread = new Thread()的时候,然后调用线程内部的init方法进行处理,最终就是不断的把当前线程的inheritableThreadLocals值复制到我们新创建的线程中的inheritableThreadLocals 中
  • 主要面对的是线程中再创建线程的场景,类似开篇举的例子,而对于子线程之间的传递或者线程池中得到父线程的值则不可行(这部分没有深入研究)

总结

现在回过头来分析开篇的例子:

  • 第一次打印:子线程中的ThreadLocal没有赋值,所以为null,而子线程中的InheritableThreadLocal却可以获取到父线程中的值CONSTANT_01
  • 第二次打印:子线程ThreadLocal和InheritableThreadLocal同时重新赋值CONSTANT_02,所以打印出的结果都为CONSTANT_02
  • 第三次打印:回到主线程,主线程和子线程都是维护自己的副本,所以子线程赋值CONSTANT_02并不会对主线程有任何影响,所以主线程打印出的结果依旧都是CONSTANT_01

参考

https://www.zhihu.com/question/23089780
https://github.com/pzxwhc/MineKnowContainer/issues/12
https://github.com/pzxwhc/MineKnowContainer/issues/20
http://blog.csdn.net/singwhatiwanna/article/details/48350919
http://www.cnblogs.com/onlywujun/p/3524675.html

其它

我的微信公众号

15、SharedPreference使用注意事项

SharedPreference是一种轻量级的存储方式,使用方便,但是也有它适用的场景。要优雅地使用它,要注意以下几点:

  • 不要存放大的key和value!会引起界面卡、频繁GC、占用内存等等
  • 毫不相关的配置项不要放在一起,文件越大读取越慢
  • 读取频繁的key和不易变动的key尽量不要放在一起,影响速度,除非整个文件很小
  • 不要频繁edit和apply,尽量批量修改一次提交!
  • 尽量不要存放JSON和HTML,这种场景请直接使用json
  • 不要指望SharedPreference进行跨进程通信

28、算法随手记

NO.1

#说到树的层次遍历,就应该提到广度优先搜索算法------广度优先搜索算法(Breadth-First-Search),又译作宽度优先搜索,或横向优先搜索,简称BFS,是一种图形搜索算法。

可以说树层次遍历是广度优先遍历的一种直接应用吧,比较广度优先搜索是图形的一种搜索算法,图形是一种比较大的概念,但这个和深度优先齐名的算法,在树的层次遍历引用中,并没有那么复杂,或许是因为用在树的遍历,而非图吧。

树的层次遍历,故名思议,在一棵树中,把节点从左往右,一层一层的,从上往下,遍历输出,这里要用到一种很重要的数据结构,队列,一提到队列,我们就要想到先进先进先,即为先进入队列元素,先接受处理,我们在日常生活中排队时,就是先到的人,先接受服务。
理解好队列,可以很容易的解决树的层此遍历,步骤如下:

   1.首先将根节点放入队列中。 
   2.当队列为非空时,循环执行步骤3到步骤5,否则执行6; 
   3.出队列取得一个结点,访问该结点; 
   4.若该结点的左子树为非空,则将该结点的左子树入队列; 
   5.若该结点的右子树为非空,则将该结点的右子树入队列; 
   6.结束。
/***************************************     
 * 内容:二叉树的层次遍历      
 ***************************************/    
import java.util.ArrayDeque;     
import java.util.Queue;   
public class BinTree {
    private char date;
    private BinTree lchild;   //左孩子
    private BinTree rchild;   //右孩子
    
    private BinTree(char c ){
        date = c;
    }
   public static void BFSOrder(BinTree t)
    {
        if(t==null) return ;
        Queue<BinTree> queue = new ArrayDeque<BinTree>();    
        //队列小知识:使用offer和poll优于add和remove之处在于它们返回值可以判断成功与否,而不抛出异常
        queue.offer(t);              //进入队列
        while(!queue.isEmpty())
        {
            t=queue.poll();           //当前节点出队列
            System.out.print(t.date);
            if(t.lchild!=null)              //当前节点左孩子去排队,在前面哦
                queue.offer(t.lchild);
            if(t.rchild!=null)            //右孩子排第二
                queue.offer(t.rchild);    
        }
    }
    public static void main(String[] args) {
         BinTree b1 = new BinTree('a');
         BinTree b2 = new BinTree('b');
         BinTree b3 = new BinTree('c');
         BinTree b4 = new BinTree('d');
         BinTree b5 = new BinTree('e');
         BinTree b6 = new BinTree('f');
         BinTree b7 = new BinTree('g');
    
        /**
         *      a 
         *    /   \
         *   b     c
         *  / \   / \
         * d   e f   g
         */
        b1.lchild = b2;
        b1.rchild = b3;
        b2.lchild = b4;
        b2.rchild = b5;
        b3.lchild = b6;
        b3.rchild = b7;

        BinTree.BFSOrder(b1);
        System.out.println();    
        }
}

11、HTTPS

互联网的通信安全,建立在SSL/TLS协议之上。
本文简要介绍SSL/TLS协议的运行机制。文章的重点是设计**和运行过程,不涉及具体的实现细节

1、作用

不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。

  • 窃听风险(eavesdropping):第三方可以获知通信内容。
  • 篡改风险(tampering):第三方可以修改通信内容。
  • 冒充风险(pretending):第三方可以冒充他人身份参与通信。

SSL/TLS协议是为了解决这三大风险而设计的,希望达到:

  • 所有信息都是加密传播,第三方无法窃听。
  • 具有校验机制,一旦被篡改,通信双方会立刻发现。
  • 配备身份证书,防止身份被冒充。

互联网是开放环境,通信双方都是未知身份,这为协议的设计带来了很大的难度。而且,协议还必须能够经受所有匪夷所思的攻击,这使得SSL/TLS协议变得异常复杂。

2、历史

互联网加密通信协议的历史,几乎与互联网一样长。

  • 1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。
  • 1995年,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。
  • 1996年,SSL 3.0版问世,得到大规模应用。
  • 1999年,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。
  • 2006年和2008年,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的修订版。

目前,应用最广泛的是TLS 1.0,接下来是SSL 3.0。但是,主流浏览器都已经实现了TLS 1.2的支持。
TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。

3、基本的运行过程

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
但是,这里有两个问题。

  • 如何保证公钥不被篡改?
    • 解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
  • 公钥加密计算量太大,如何减少耗用的时间?
    • 解决方法:每一次对话(session),客户端和服务器端都生成一个"对话密钥"(session key),用它来加密信息。由于"对话密钥"是对称加密,所以运算速度非常快,而服务器公钥只用于加密"对话密钥"本身,这样就减少了加密运算的消耗时间。

因此,SSL/TLS协议的基本过程是这样的:

  • 客户端向服务器端索要并验证公钥。
  • 双方协商生成"对话密钥"。
  • 双方采用"对话密钥"进行加密通信。

上面过程的前两步,又称为"握手阶段"(handshake)。

4、握手阶段的详细过程

"握手阶段"涉及四次通信,我们一个个来看。需要注意的是,"握手阶段"的所有通信都是明文的。
bg2014020503

客户端发出请求(ClientHello)

首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。
在这一步,客户端主要向服务器提供以下信息。

  • 支持的协议版本,比如TLS 1.0版。
  • 一个客户端生成的随机数,稍后用于生成"对话密钥"。
  • 支持的加密方法,比如RSA公钥加密。
  • 支持的压缩方法。

这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应该向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。
对于虚拟主机的用户来说,这当然很不方便。2006年,TLS协议加入了一个Server Name Indication扩展,允许客户端向服务器提供它所请求的域名。

服务器回应(SeverHello)

服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。

  • 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
  • 一个服务器生成的随机数,稍后用于生成"对话密钥"。
  • 确认使用的加密方法,比如RSA公钥加密。
  • 服务器证书。

除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供"客户端证书"。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。

  • 一个随机数。该随机数用服务器公钥加密,防止被窃听。
  • 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
  • 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把"会话密钥"。

为什么一定要用三个随机数,来生成"会话密钥"

"不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。pre master的存在在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。"此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。

服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息。

  • 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
  • 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容。

23、卡顿

卡顿类型
需要从底层公共库 、第三方库、业务代码、架构层面分析

  • 文件读写
  • SharedPreferences是线程安全的但并非进程安全的
  • SharedPreferences写入卡顿 写入多线程同步导致卡顿 #15 可以考虑使用apply代替commit
  • 在主页面读取本地数据 初始化过程文件操作

PS:这两个方法的区别在于:

  1. apply没有返回值而commit返回boolean表明修改是否提交成功
  2. apply是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘, 而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内容,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
  3. apply方法不会提示任何失败的提示。
    由于在一个进程中,sharedPreference是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,建议使用apply,当然需要确保提交成功且有后续操作的话,还是需要用commit的。
  • 业务逻辑&布局复杂
  • WebView卡顿问题
  • 生命周期触(如onResume)发jsbridge
  • 列表实例化item卡顿
  • 包或者设备信息读取
  • 初始化获取信息 可考虑使用缓存
  • 第三方库 如短时间内大量调用Glide导致Glide内部频繁获取包信息,还有网络库等 可以使用信息缓存
  • 序列化
    序列化卡顿,使用流配合序列化可提升性能

  • 反射
    反射卡顿多数集中在4.4.2和4.4.4系统上

  • 插件系统
    hook了系统的AMS 造成Activity pause生命周期卡顿 或者启动service时候卡顿等问题

  • 多线程
    多线程等待带来的卡顿,集中在6.0、6.1、7.0等系统

12、【NDK Lab】环境搭建以及so打包和使用

直入正题!

环境搭建

开发环境

  • MacBook Pro(macOS Sierra10.12.6)
  • Android Studio2.3.3
  • Gradle 2.3.3

NDK install

1、这里我是采用Android Studio自行安装的,打开AndroidStudio,选择顶部工具条,Tools->Android->SDK Manager->SDK Tools->NDK 点击install

NDK安装路径

设置NDK路径

2、也可以自行下载ndk包(去AndroidDevTools或者谷歌官方网站下载),下载ndk包后解析到某个路径,打开Project Structure->设置 NDK location,同上1设置NDK路径

3、NDK环境变量配置,我们需要使用到ndk-build命令,打开终端 -> 输入 :vim ~/.bash_profile -> 加入NDK 包的路径,具体怎么使用vim进行编辑请自行百度

配置NDK环境变量

配置NDK环境变量

4、保存文件,关闭.bash_profile,输入source .bash_profile或者重新开启一个terminal ,当前配置才会生效。 命令行输入ndk-build验证配置是否成功

总之:整个配置过程同Android SDK的配置一样

NDK开发实战

1、创建Android项目JniLab
2、查看项目local.properties中加入ndk和sdk的路径是否正确

ndk.dir=/Users/didi/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/didi/Library/Android/sdk

3、配置项目下的gradle.properties文件,表示我们要使用NDK进行开发(缺少这步会导致后续无法通过alt+enter快捷键生成jni文件夹)

android.useDeprecatedNdk=true

4、在moudle根目录下的的build.gradle中的defaultConfig标签内部里加入如下代码

ndk{    
 // 生成的so文件名字,调用C程序的代码中会用到该名字,需要保持一致    
moduleName "algorithm"     
// 输出指定三种平台下的so库
// 还可以添加 'x86_64', 'mips', 'mips64'
abiFilters "armeabi", "armeabi-v7a", "x86" 
}

5、编写jni代码
通过System.loadLibrary加载的库名要和上述4的moduleName一致,否则会出现java.lang.UnsatisfiedLinkError问题,找不到so库

JniManager

6、执行第5步的时候,如上图所示,对应native方法(getInfo)会提示找不到对应方法,快捷键 alt+enter 会生成对应jni文件夹,包含algorithm.c文件,此处的native方法还是会显示红色,但是不影响编译

jni文件夹生成

7、编译项目后会发现app/build中已经生成so文件,并且已经对应的cpu包就是我们在gradle中已经配置的,并且已经调用成功

build中生成对应so文件

PS:编译时可能碰到NDK_PROJECT_PATH = null问题

Messages Gradle Build

暂时的解决方法:将app module的compileSdkVersion与targetSdkVersion由之前的25改成24(可能也跟最新的NDK版本有关系)

成功调用native方法

打包出动态so文件,在其它项目中使用

有时候我们的需求是这样的,我们把一些比较重要的业务逻辑封装到ndk内部,对java层只暴露接口。我们就需要打包出so文件,并且可能需要在其他项目中使用,下面将介绍so(符合JNI标准)文件的打包,以及在其他项目中如何正确的调用

  • 编写Android.mk文件,放到jni文件夹根目录,与.c文件同级
    PS:注意中文注释或者中文空格带来的意外麻烦
LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
LOCAL_MODULE := algorithm
LOCAL_SRC_FILES := /Users/sus/example/JniLab/app/src/main/jni/algorithm.c
 include $(BUILD_SHARED_LIBRARY)
  • 使用ndk-build命令(需要配置NDK环境变量,参照上文NDK环境变量配置),生成so文件
  • 编写Application.mk文件,放到jni文件夹根目录,与.c文件同级
APP_PLATFORM := android-14
APP_ABI :=all //打包出所有cpu平台so文件

进入到main目录后在terminal中输入命令,ndk-build工具便会帮我们打包出所有cpu平台so文件

ndk-build

libs目录

其它项目使用该so文件

  • 拷贝so文件到项目的main/jniLibs目录
  • 新建package,包名与类名以及方法名必须与生成so文件的类保持一致!
  • 使用loadLibrary加载动态库,声明native方法

PS:这里如果你不想新建项目测试,你可以在main下新建jniLibs文件夹,把libs里的so放到jniLibs中,删除libs文件夹,然后删除jni文件夹运行也会起到类似在新项目中使用so文件的作用

  • 对于上面说的【包名与类名以及方法名必须与生成so文件的类保持一致!】这个规范,读者可能有疑惑,这样的约束太死板不够灵活,我们在使用一些包含so库的第三方SDK的时候并不记得有这么多限制
  • 的确如此,我们看下第三方SDK是怎么搞的,以Umeng Push SDK为参考来看一下,我们发现第三方库都会带有jar包,然后通过包里面去调用so文件,我们只需要使用jar包中暴露的接口方法即可,而上述的规范可能更适合内部人员之间开发和使用so

Umeng Push SDK

下载地址:JniLab

8、AsyncTask

对比一下AsyncTask在子线程和在主线程初始化的区别

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getCanonicalName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        android.util.Log.e(TAG, "#onCreate, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());

        new Thread(new Runnable() {
            @Override
            public void run() {
                android.util.Log.e(TAG, "创建AsyncTask的子线程, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());
                new AsyncTask<Void, Void, String>() {
                    @Override
                    protected void onPreExecute() {
                        android.util.Log.e(TAG, "子线程内初始化AsyncTask #onPreExecute, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());
                        super.onPreExecute();
                    }

                    @Override
                    protected String doInBackground(Void... params) {
                        android.util.Log.e(TAG, "子线程内初始化AsyncTask #doInBackground, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());
                        return null;
                    }

                    @Override
                    protected void onPostExecute(String result) {
                        android.util.Log.e(TAG, "子线程内初始化AsyncTask #onPostExecute, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());
                        super.onPostExecute(result);
                    }
                }.execute((Void) null);
            }
        }).start();

// Caused by: java.util.concurrent.RejectedExecutionException: 
// Task android.os.AsyncTask$3@7c4920 rejected from  
// java.util.concurrent.ThreadPoolExecutor@1f312d9
// [Running, pool size = 17, active threads = 17, queued tasks = 128, completed tasks = 0]

//        for(int i = 0;i<200;i++) {
//            new AsyncTask<Void, Void, Void>() {
//                @Override
//                protected Void doInBackground(Void... params) {
//                    // do something
//                    try {
//                        Thread.sleep(3000);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                    return null;
//                }
//            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
//        }

        new AsyncTask<Void, Void, String>() {
            @Override
            protected void onPreExecute() {
                android.util.Log.e(TAG, "主线程内初始化AsyncTask #onPreExecute, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());
                super.onPreExecute();
            }

            @Override
            protected String doInBackground(Void... params) {
                android.util.Log.e(TAG, "主线程内初始化AsyncTask #doInBackground, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());
                return null;
            }

            @Override
            protected void onPostExecute(String result) {
                android.util.Log.e(TAG, "主线程内初始化AsyncTask #onPostExecute, " + Thread.currentThread().getName() + "--->" + Thread.currentThread().getId());
                super.onPostExecute(result);
            }
        }.execute((Void) null);
    }
}

在子线程初始化的打印日志:

a2913717-1192-4efe-bb02-0e8788d8da5f

在主线程初始化的打印日志:

9751f010-fab9-46dc-b345-4de502510abf

总结:

  • AsyncTask里的Handler为InternalHandler,这里直接使用的主线程的Looper( super(Looper.getMainLooper());),如果去看API 22以下的代码,会发现它没有这个构造函数,而是使用默认的;默认情况下,Handler会使用当前线程的Looper,如果你的AsyncTask是在子线程创建的,那么很不幸,你的onPreExecute和onPostExecute并非在UI线程执行,而是被Handler post到创建它的那个线程执行;如果你在这两个地方更新了UI,那么直接导致崩溃,API 25的代码其实稍微更近一步(通过上面案例日志分析),即使你在子线程中创建AsyncTask,onPostExecute也会在UI线程执行,但是onPreExecute还是在创建AsyncTask的线程中执行
  • 所以 归结为一点:为了避免不必要的麻烦,AsyncTask必须在主线程初始化,源码注释也是这么提示我们的
  • 在Android 1.5刚引入的时候,AsyncTask的execute是串行执行的;到了Android 1.6直到Android 2.3.2,又被修改为并行执行了。Android 3.0以上,AsyncTask默认串行执行的
  • 默认串行的设计是为了避免相同资源的同步访问问题
  • 如果希望AsyncTask可以并行处理的话,可以使用executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
  • 如果任务过多,超过了工作队列以及线程数目的限制导致这个线程池发生阻塞,默认的处理方式会直接抛出一个异常导致进程挂掉 如上述案例模拟(当然可以重写AsyncTask,在executeOnExecutor中对exec.execute(mFuture)做保护,捕获RejectedExecutionException异常,这里可以同时做一个统计,如果出现问题,说明AsyncTask不适合这种场景,需要考虑重构,既然知道有此问题,最好主动避免涉及)

AsyncTask源码

这里是以API 25为例进行分析的:

public abstract class AsyncTask<Params, Progress, Result> {
    private static final String LOG_TAG = "AsyncTask";

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // 至少需要2个线程,最多需要4个线程在核心池中
    // 可以比CPU数量少一个,避免让所有的CPU都在处理后台工作
    // 这个计算方式不同版本表现不一样,早期是CPU+1
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;
    
    // 线程创建工厂
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    // 工作队列长度为128
    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);
    // 可以用于并行执行任务的 Executor
    public static final Executor THREAD_POOL_EXECUTOR;
    // THREAD_POOL_EXECUTOR初始化
    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        // 核心线程空闲允许超时处理 待遇同非核心线程
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }

    // 串行执行任务的Executor(按顺序每次同时只能执行一个任务)
    // 指定进程全局有效
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;
    //默认串行执行 而非并行执行
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    private static InternalHandler sHandler;

    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;

    private volatile Status mStatus = Status.PENDING;
    
    private final AtomicBoolean mCancelled = new AtomicBoolean();
    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

    // SERIAL_EXECUTOR也使用THREAD_POOL_EXECUTOR实现的
    // 只不过通过队列mTasks调度实现串行的控制
    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

    public enum Status {
        PENDING,
        RUNNING,
        FINISHED,
    }

    private static Handler getHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler();
            }
            return sHandler;
        }
    }

    /** @hide */
    public static void setDefaultExecutor(Executor exec) {
        sDefaultExecutor = exec;
    }

    // 创建一个异步任务,构造函数必须在UI线程调用
    public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //noinspection unchecked
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    postResult(result);
                }
                return result;
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

    private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }

    public final Status getStatus() {
        return mStatus;
    }

    @WorkerThread
    protected abstract Result doInBackground(Params... params);

    @MainThread
    protected void onPreExecute() {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onPostExecute(Result result) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onProgressUpdate(Progress... values) {
    }

    @SuppressWarnings({"UnusedParameters"})
    @MainThread
    protected void onCancelled(Result result) {
        onCancelled();
    }    
    
    @MainThread
    protected void onCancelled() {
    }

    public final boolean isCancelled() {
        return mCancelled.get();
    }

    public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);
        return mFuture.cancel(mayInterruptIfRunning);
    }

    public final Result get() throws InterruptedException, ExecutionException {
        return mFuture.get();
    }

    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
            ExecutionException, TimeoutException {
        return mFuture.get(timeout, unit);
    }
    
     // 基于指定参数执行任务 默认串行执行
    @MainThread
    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }
    
    //基于指定参数执行任务,可以自定义Executor,也可以直接使用THREAD_POOL_EXECUTOR
    @MainThread
    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }
    
    //简易版 可以直接执行一个Runnable对象
    @MainThread
    public static void execute(Runnable runnable) {
        sDefaultExecutor.execute(runnable);
    }

    @WorkerThread
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

    private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

    @SuppressWarnings({"RawUseOfParameterizedType"})
    private static class AsyncTaskResult<Data> {
        final AsyncTask mTask;
        final Data[] mData;

        AsyncTaskResult(AsyncTask task, Data... data) {
            mTask = task;
            mData = data;
        }
    }
}

16、ScaleType

1458573-b7ce213f05816e85
图片尺寸大于控件尺寸
1458573-4e468ddb0dbcd11c
1458573-c9eafd355be4f25e
图片尺寸小于控件尺寸
1458573-f79bf962aed9c5bd

  • FIT_XY:对原图宽高进行放缩,该放缩不保持原比例来填充满ImageView。
  • MATRIX:不改变原图大小从ImageView的左上角开始绘制,超过ImageView部分不再显示。
  • CENTER:对原图居中显示,超过ImageView部分不再显示。
  • CENTER_CROP:对原图居中显示后进行等比放缩处理,使原图最小边等于ImageView的相应边。
  • CENTER_INSIDE:若原图宽高小于ImageView宽高,这原图不做处理居中显示,否则按比例放缩原图宽(高)是之等于ImageView的宽(高)。
  • FIT_START:对原图按比例放缩使之等于ImageView的宽高,若原图高大于宽则左对齐否则上对其。
  • FIT_CENTER:对原图按比例放缩使之等于ImageView的宽高使之居中显示。
  • FIT_END:对原图按比例放缩使之等于ImageView的宽高,若原图高大于宽则右对齐否则下对其。

当我们没有在布局文件中使用scaleType属性或者是没有手动调用setScaleType方法时,那么mScaleType的默认值就是FIT_CENTER。

6、static用法

  • 1、static变量 对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)
  • 2、static方法 静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
  • 3、static代码块 static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次
  • 4、static final用来修饰成员变量和成员方法,可简单理解为“全局常量”!对于变量,表示一旦给值就不可修改,并且通过类名可以访问。对于方法,表示不可覆盖,并且可以通过类名直接访问
    对于被static和final修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象,这一点在编程中用到很多。

3、synchronize用法

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

    1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
    1. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    1. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
    1. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象

10、Android运行时权限管理策略

Android M 行为变更(6.0)

运行时权限:

此版本引入了一种新的权限模式,如今,用户可直接在运行时管理应用权限。这种模式让用户能够更好地了解和控制权限,同时为应用开发者精简了安装和自动更新过程。用户可为所安装的各个应用分别授予或撤销权限。
对于以 Android 6.0(API 级别 23)或更高版本为目标平台的应用,请务必在运行时检查和请求权限。要确定您的应用是否已被授予权限,请调用新增的 checkSelfPermission() 方法。要请求权限,请调用新增的 requestPermissions() 方法。即使您的应用并不以 Android 6.0(API 级别 23)为目标平台,您也应该在新权限模式下测试您的应用。

Android O 行为变更(8.0)

权限
在 Android O 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。

对于针对 Android O 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android O,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。

关于运行时权限

在旧的权限管理系统中,权限仅仅在App安装时询问用户一次,用户同意了这些权限App才能被安装(某些深度定制系统另说),App一旦安装后就可以偷偷的做一些不为人知的事情了。

在Android6.0开始,App可以直接安装,App在运行时一个一个询问用户授予权限,系统会弹出一个对话框让用户选择是否授权某个权限给App(这个Dialog不能由开发者定制),当App需要用户授予不恰当的权限的时候,用户可以拒绝,用户也可以在设置页面对每个App的权限进行管理。

特别注意:这个对话框不是开发者调用某个权限的功能时由系统自动弹出,而是需要开发者手动调用,如果你直接调用而没有去申请权限的话,将会导致App奔溃。

也许你已经开始慌了,这对于用户来说是好事,但是对于开发者来说我们不能直接调用方法了,我们不得不在每一个需要权限的地方检查并请求用户授权,所以就引出了以下两个问题。

哪些权限需要动态申请

新的权限策略讲权限分为两类,第一类是不涉及用户隐私的,只需要在Manifest中声明即可,比如网络、蓝牙、NFC等;第二类是涉及到用户隐私信息的,需要用户授权后才可使用,比如SD卡读写、联系人、短信读写等。

Normal Permissions

此类权限都是正常保护的权限,只需要在AndroidManifest.xml中简单声明这些权限即可,安装即授权,不需要每次使用时都检查权限,而且用户不能取消以上授权,除非用户卸载App。

  • ACCESS_LOCATION_EXTRA_COMMANDS
  • ACCESS_NETWORK_STATE
  • ACCESS_NOTIFICATION_POLICY
  • ACCESS_WIFI_STATE
  • BLUETOOTH
  • BLUETOOTH_ADMIN
  • BROADCAST_STICKY
  • CHANGE_NETWORK_STATE
  • CHANGE_WIFI_MULTICAST_STATE
  • CHANGE_WIFI_STATE
  • DISABLE_KEYGUARD
  • EXPAND_STATUS_BAR
  • GET_PACKAGE_SIZE
  • INSTALL_SHORTCUT
  • INTERNET
  • KILL_BACKGROUND_PROCESSES
  • MODIFY_AUDIO_SETTINGS
  • NFC
  • READ_SYNC_SETTINGS
  • READ_SYNC_STATS
  • RECEIVE_BOOT_COMPLETED
  • REORDER_TASKS
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
  • REQUEST_INSTALL_PACKAGES
  • SET_ALARM
  • SET_TIME_ZONE
  • SET_WALLPAPER
  • SET_WALLPAPER_HINTS
  • TRANSMIT_IR
  • UNINSTALL_SHORTCUT
  • USE_FINGERPRINT
  • VIBRATE
  • WAKE_LOCK
  • WRITE_SYNC_SETTINGS

Dangerous Permissions

所有危险的Android系统权限属于权限组,如果APP运行在Android 6.0 (API level 23)或者更高级别的设备中,而且targetSdkVersion>=23时,系统将会自动采用动态权限管理策略,如果你在涉及到特殊权限操作时没有做动态权限的申请将会导致App崩溃,因此你需要注意:

此类权限也必须在Manifest中申明,否则申请时不提使用用户,直接回调开发者权限被拒绝。同一个权限组的任何一个权限被授权了,这个权限组的其他权限也自动被授权。例如,一旦WRITE_CONTACTS被授权了,App也有READ_CONTACTS和GET_ACCOUNTS了。申请某一个权限的时候系统弹出的Dialog是对整个权限组的说明,而不是单个权限。例如我申请READ_EXTERNAL_STORAGE,系统会提示"允许xxx访问设备上的照片、媒体内容和文件吗?"。如果App运行在Android 5.1 (API level 22)或者更迭级别的设备中,或者targetSdkVersion<=22时(此时设备可以是Android 6.0 (API level 23)或者更高),在所有系统中仍将采用旧的权限管理策略,系统会要求用户在安装的时候授予权限。其次,系统就告诉用户App需要什么权限组,而不是个别的某个权限。

  • CALENDAR(日历)
  • READ_CALENDAR
  • WRITE_CALENDAR
  • CAMERA(相机)
  • CAMERA
  • CONTACTS(联系人)
  • READ_CONTACTS
  • WRITE_CONTACTS
  • GET_ACCOUNTS
  • LOCATION(位置)
  • ACCESS_FINE_LOCATION
  • ACCESS_COARSE_LOCATION
  • MICROPHONE(麦克风)
  • RECORD_AUDIO
  • PHONE(手机)
  • READ_PHONE_STATE
  • CALL_PHONE
  • READ_CALL_LOG
  • WRITE_CALL_LOG
  • ADD_VOICEMAIL
  • USE_SIP
  • PROCESS_OUTGOING_CALLS
  • SENSORS(传感器)
  • BODY_SENSORS
  • SMS(短信)
  • SEND_SMS
  • RECEIVE_SMS
  • READ_SMS
  • RECEIVE_WAP_PUSH
  • RECEIVE_MMS
  • STORAGE(存储卡)
  • READ_EXTERNAL_STORAGE
  • WRITE_EXTERNAL_STORAGE

使用adb命令可以查看这些需要授权的权限组:

  • adb shell pm list permissions -d -g

使用adb命令同样可以授权/撤销某个权限:

  • adb shell pm [grant|revoke] ...
    关于运行时权限的一些建议

只请求你需要的权限,减少请求的次数,或用Intent来代替,让其他的应用来处理。

如果你使用Intent,你不需要设计界面,由第三方的应用来完成所有操作。比如打电话、选择图片等。如果你请求权限,你可以完全控制用户体验,自己定义UI。但是用户也可以拒绝权限,就意味着你的应用不能执行这个特殊操作。防止一次请求太多的权限或请求次数太多,用户可能对你的应用感到厌烦,在应用启动的时候,最好先请求应用必须的一些权限,非必须权限在使用的时候才请求,建议整理并按照上述分类管理自己的权限:

  • 普通权限(Normal PNermissions):只需要在Androidmanifest.xml中声明相应的权限,安装即许可。
    需要运行时申请的权限(Dangerous Permissions):
  • 必要权限:最好在应用启动的时候,进行请求许可的一些权限(主要是应用中主要功能需要的权限)。
  • 附带权限:不是应用主要功能需要的权限(如:选择图片时,需要读取SD卡权限)。
    解释你的应用为什么需要这些权限:在你调用requestPermissions()之前,你为什么需要这个权限。

例如,一个摄影的App可能需要使用定位服务,因为它需要用位置标记照片。一般的用户可能会不理解,他们会困惑为什么他们的App想要知道他的位置。所以在这种情况下,所以你需要在requestpermissions()之前告诉用户你为什么需要这个权限。
使用兼容库support-v4中的方法

  • ContextCompat.checkSelfPermission()
  • ActivityCompat.requestPermissions()
  • ActivityCompat.shouldShowRequestPermissionRationale()

几个重要的方法与常量解释

PackageManager中的两个常量:

  • PackageManager.PERMISSION_DENIED:该权限是被拒绝的。
  • PackageManager.PERMISSION_GRANTED:该权限是被授权的。

Activity中或者Fragment都会有以下几个方法:

  • int checkSelfPermission(String)
  • void requestPermissions(int, String...)
  • boolean shouldShowRequestPermissionRationale(String)
  • void onRequestPermissionsResult()

上述四个方法中,前三个方法在support-v4的ActivityCompat中都有,建议使用兼容库中的方法。最后一个方法是用户授权或者拒绝某个权限组时系统会回调Activity或者Fragment中的方法。

checkSelfPermission() 检查权限

检查某一个权限的当前状态,你应该在请求某个权限时检查这个权限是否已经被用户授权,已经授权的权限重复申请可能会让用户产生厌烦。
该方法有一个参数是权限名称,有一个int的返回值,用这个值与上面提到的两个常量做比较可判断检查的权限当前的状态。

if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
     != PackageManager.PERMISSION_GRANTED) {
 // 没有权限,申请权限。
}else{
 // 有权限了,去放肆吧。
}

requestPermissions() 申请权限

请求用户授权几个权限,调用后系统会显示一个请求用户授权的提示对话框,App不能配置和修改这个对话框,如果需要提示用户这个权限相关的信息或说明,需要在调用 requestPermissions() 之前处理,该方法有两个参数:

  • int requestCode,会在回调onRequestPermissionsResult()时返回,用来判断是哪个授权申请的回调。
  • String[] permissions,权限数组,你需要申请的的权限的数组。

由于该方法是异步的,所以无返回值,当用户处理完授权操作时,会回调Activity或者Fragment的onRequestPermissionsResult()方法处理权限结果回调

该方法在Activity/Fragment中应该被重写,当用户处理完授权操作时,系统会自动回调该方法,该方法有三个参数:

*int requestCode,在调用requestPermissions()时的第一个参数。
*String[] permissions,权限数组,在调用requestPermissions()时的第二个参数。
*int[] grantResults,授权结果数组,对应permissions,具体值和上方提到的PackageManager中的两个常量做比较。

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
  switch (requestCode) {
      case MMM: {
          if (grantResults.length > 0
              && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
              // 权限被用户同意,可以去放肆了。
          } else {
              // 权限被用户拒绝了,洗洗睡吧。
          }
          return;
      }
  }
}

shouldShowRequestPermissionRationale()

望文生义,是否应该显示请求权限的说明。
第一次请求权限时,用户拒绝了,调用shouldShowRequestPermissionRationale()后返回true,应该显示一些为什么需要这个权限的说明。用户在第一次拒绝某个权限后,下次再次申请时,授权的dialog中将会出现“不再提醒”选项,一旦选中勾选了,那么下次申请将不会提示用户。
第二次请求权限时,用户拒绝了,并选择了“不在提醒”的选项,调用shouldShowRequestPermissionRationale()后返回false。设备的策略禁止当前应用获取这个权限的授权:shouldShowRequestPermissionRationale()返回false 。加这个提醒的好处在于,用户拒绝过一次权限后我们再次申请时可以提醒该权限的重要性,面得再次申请时用户勾选“不再提醒”并决绝,导致下次申请权限直接失败。
综上所述,整合代码后:

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {// 没有权限。
    if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_CONTACTS)) {
            // 用户拒绝过这个权限了,应该提示用户,为什么需要这个权限。
    } else {
        // 申请授权。
        ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MMM);
    }
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MMM: {
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 权限被用户同意,可以去放肆了。
            } else {
                // 权限被用户拒绝了,洗洗睡吧。
            }
            return;
        }
    }
}

22、包体积优化

  • 图片压缩
  • 对aar中大于20K的图片进行压缩,最好在源码中压缩提交重打aar包
  • 对主业务中的功能代码进行图片扫描,对aar中大于20K的图片进行压缩
  • 可选压缩工具有:1.imageoptim (开源,免费试用) 2.tinypng(月数量限制,效果比imageoptim好)
  • 删除未用的多语言资源 resConfigs "zh-rCN" , "en" 打包过程
    只保留support包中的简体中文和英文 string资源

  • 选择分辨率支持 打包过程

  • 1080x1920(41.46%) 1080x1812(10.34%)1080X1800(6.53%)1080x1776(5.50%)
  • 720x1280(13.7%)

结论:xxhdpi占比约为:60%,考虑去掉hdpi下的资源

splits {
    density {
 enable true
 exclude "hdpi"
 compatibleScreens 'small', 
'normal', 
'large', 
'xlarge',
'xxlarge',
'xxxlarge'
    }
}
  • 删除aar 去掉已下线业务线
  • 动态下发,对部分业务采用插件的方式动态下发

9、Android ListView 图片重复错位闪烁问题原理及解决方案

原理

ListView item缓存机制:为了使得性能更优,ListView会缓存行item(某行对应的View)。ListView通过adapter的getView函数获得每行的item。滑动过程中,

  • a. 如果某行item已经滑出屏幕,若该item不在缓存内,则put进缓存,否则更新缓存;
  • b. 获取滑入屏幕的行item之前会先判断缓存中是否有可用的item,如果有,做为convertView参数传递给adapter的getView。

这样,getView就可以充分利用缓存大大提升ListView的性能。即便上万个行item,最多inflate的次数为n,n为一屏最多显示ListView行item的个数

这样提升了性能,但同时也会造成另外一些问题:

假设一屏最多显示7个item,当第1个item滑出,第8个item滑入的时候

  • 图片显示重复
    这个显示重复是指当前行item显示了之前某行item的图片。 比如ListView滑动到第1行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第8行,且滑动过程中该图片加载结束,第1行已不在屏幕内,根据上面介绍的缓存原理,第1行的view可能被第8行复用,这样我们看到的就是第8行显示了本该属于第1行的图片,造成显示重复。
  • b. 图片显示错乱
    这个显示错乱是指某行item显示了不属于该行item的图片。 比如ListView滑动到第1行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第8行,第1行已不在屏幕内,根据上面介绍的缓存原理,第1行的view可能被第8行复用,第8行显示了第1行的View,这时之前的图片加载结束,就会显示在第8行,造成错乱。
  • c. 图片显示闪烁
    上面b的情况,第8行图片又很快加载结束,所以我们看到第8行先显示了第1行的图片,立马又显示了自己的图片进行覆盖造成闪烁错乱。

解决方案

解决方法:

  • 对imageview设置tag,并预设一张图片。
  • 向下滑动后,item8显示,item1隐藏。但由于item1是第一次进来就显示,所以一般情况下,item1都会比item8先下载完,但由于此时可见的item8的tag,和隐藏了的item1的url不匹配,所以就算item1的图片下载完也不会显示到item8中,因为tag标识的永远是可见图片中的url。

关键代码:

// 给 ImageView 设置一个 tag
holder.img.setTag(imgUrl);
// 通过 tag 来防止图片错位
if (imageView.getTag() != null && imageView.getTag().equals(imageUrl)) {
    imageView.setImageBitmap(result);
}

27、移动开发者快速上手php接口开发

前言

Android写多了,突然想换个姿势;就鼓捣了一阵iOS,发现和Android大同小异;那就学学后台,写写接口?

有时候吧,人就得折腾折腾,否则就容易缺乏危机感,虽然大部分时间我们都是从事当前岗位的事情,但是工作之余,我们也是要逐步提升自己,起码我们要有意愿去了解新技术!毕竟同时了解多种相关技术,整体的思维肯定会有提升!

但是不可否认的是:脱离实际业务的技术都是纸上谈兵

但是我们也得从纸上谈兵做起啊!

学习一门新的技术,应该学什么,学到什么程度,可以从公司的招聘任职要求中看出门路:

这里以php为例吧,毕竟php是世界上最好的语言(手动微笑)

这里写图片描述

当然,最终达到什么程度,还得看个人目标和精力投入!

接下来,我也只是把我目前领悟的招式记录了一下,如果合你的套路那你可以参考参考,如果你觉得招式太简单,那就请不吝赐教!

开发环境配置

  • Mac下如何搭建PHP开发环境?
    • 快速上手的话,这里推荐MAMP或者MAMP PRO
    • MAMP,可以切换php版本、数据库、web服务器,配置hosts也比较方便,总体更人性化(适合懒人)
    • 开发工具可以使用Sublime Text或者PhpStorm
    • 其它相关可参考PHP开发相关配置

简易版接口项目

  • 项目地址:ishow
  • 项目Model EER图:

这里写图片描述

  • 接口演示:
    这里模拟请求使用的工具是:网页调试与发送网页HTTP请求的Chrome插件Postman
    例如:注册接口register.php

这里写图片描述

其它接口依次类推:

  • 收藏与取消收藏接口 collect.php
  • 演出详情信息接口 detail.php
  • 登录接口 login.php
  • 演出列表接口 show.php
  • 分页获取演出列表接口 showbypage.php
  • 获取演出类型的接口 showtype.php
  • 修改密码的接口 updatepassword.php
  • 上传头像接口 uploadimg.php(这里提供了一个uploadimg.html模拟头像上传的逻辑)

基于Laravel5.4框架以及Angular.js进行开发

  • 为什么使用php开发框架?

PHP开发框架有助于促进快速软件开发(RAD),节约了开发者的时间,有助于创建更为稳定的程序,并减少开发者重复编写代码的劳动。这些框架还通过确保正确的数据库操作以及只在表现层编程的方式帮助初学者创建稳定的程序

  • 这里为什么选择Laravel?

php框架谁最牛?这两年各种排名下,不敢说全部,但基本上Laravel都独占鳌头,看谷歌趋势,红色的Symphony似乎才是最热的,但是走在下降通道,它的压制下,Laravel正在直线崛起!
Laravel被称之为“为Web开发艺术家使用的框架!

  • 帅乎,基于Laravel框架进行开发,既有接口开发,也涉及部分前端开发

这里写图片描述

业务接口设计与规范

上面的两个案例 参考了很多网络资源教程 个人认为只适合上手使用,如果要真正地做起后台接口开发,还是要严格遵守符合接口开发的设计和规范

下面是我从后台开发的技术分享中学习总结的,仅供大家参考:

  • 业务接口的作用

    • 从数据库/下游系统 获取、组织加工数据
    • 提供数据给需求方
  • 接口很简单,加一个接口分分钟的事情?

  • 前置准备

    • 切忌直接动手 先确保知己知彼
    • 吃透需求
      • 需求逻辑结构
        • 需求长什么样?
        • 需求是否可以实现?
        • 需要什么样的接口?
      • 交互方式
        • 一个接口能搞定吗?
        • 长连接还是短连接?
      • 需求背后的动机
        • 需求是否合理
        • 有没有更好的方案
      • 了解数据
        • 各系统数据概貌
          • 这些数据有没有?
          • 这个数据能不能拿到?
          • 有没有更好的方案?实时没有,可否离线异步?
        • 数据存储结构
          • OFS
          • ES
          • MySQL
          • offline?
          • 多套数据类问题 数据结构不方便扩展
          • 以何种方式获取
          • RPC
          • cache
      • 理解交互
        • 用户体验为重:体验优先,同步or异步
        • 交互时机把控:合理分配接口
        • 理解交互特点:推拉结合方案
  • 业务接口设计原则

    • 最大化易用性
      • 命名:是易用性提升的主要部分 沉淀命名字典
        • 接口命名:最好是动宾结构
        • 参数命名:最好是定语➕名词
        • 注意:不要用生僻单词或者发明缩写
      • 参数:采用粗粒度参数
      • 文档:及时性和动态性
    • 职责分明
      • 各司其职
        • get就是get,不要干set的活
      • 合理搭配
        • 针对各司其职的各类接口,寻找合理的交互形式
      • 职责明确
        • 应用场景确定可控,不合理场景不要往里塞
    • 归纳抽象
      • 归纳接口
        • 对功能进行抽象,进行“内聚”,类似功能归纳抽象到一起,降低与客户端的交互复杂度。同时防止接口数量爆炸
      • 系统拆分
        • 归纳表现层逻辑(文案组织等) 、和功能层逻辑,水平拆分,减少接口/系统间的偶合
    • 避免臃肿
      • 异步化
      • 插件化
      • 优化上下游交互
  • 做到以上这些就可以了吗?

    • 面对需求,拥抱变化
    • 优化,进无止境

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.