Nico随笔


  • 首页

  • 归档

  • 标签

Android 性能检测

发表于 2017-06-01 | 分类于 Android

手机端:

开发者选项

  1. 绘图》 显示布局边界

  2. 硬件加速渲染 》调试GPU过渡绘制 要打开

  1. 监控》启用严格模式

  2. GPU呈现模式分析 可以对照颜色表找出耗时出在那一部分
    image

电脑端:

  1. 查看MemoryMonitor ,查看页面内存波动(对于listview )

  2. tools》android》android device monistor
    (其实它就是把android sdk中tools下面的很多功能聚合起来; 如ddms,uiautomatorviewer,monitor等功能聚合起来,但是好像没有集成hierarchyviewer的功能)

  3. 查看录制页面变化时候的cpu耗时
    开始录制》选中进程,点击红色方框左边的按钮,然后点击红色方框里右侧的按钮录制正式开始,需要结束时,再点击右侧按钮结束,就会有结果自动生成
    image

    分析结果》录制实际上是一个采样的过程,可以看图中红色方框里面最耗时的几个方法,基本上可以定位到程序的问题
    image

  4. 查看布局层次,以及每一层的绘制时间,目的是减小层次
    入口是在:tools 下面 hierarchyviewer 查看每个层次的绘制时间(这个好像必须在模拟器上看)

分析卡的原因

参考资料

http://www.cnblogs.com/krislight1105/p/5352500.html
http://blog.csdn.net/wangbaochu/article/details/50396512

数据库踩坑和调试

发表于 2017-04-01 | 分类于 Android

数据库

Android数据库设计

  1. 冗余字段设计,表设计要空出2到3个冗余字段
  2. 不要过多的关联查询,外键约束不要多了
  3. 设计要优先考虑易用性 ,容易升级,性能方面主要要通过应用层来掌控
  4. 适当的分库可以有效避免数据库被锁住。(sqlite的文件锁)

database is locked的原因

  • 同时有两个写操作的时候,后执行的只能先等待,如果等待时间超过5秒,就会产生这种错误
  • 一个文件正在写入,重复打开数据库操作也会报错

Android DB操作技巧

  1. 对于经常要使用的DB不需要关闭,一般全局保存一个静态的SQLiteDatabase句柄(Writable的),而且是在Application oncreate 就初始化好.
  2. 由于Application 没有destroy的回调,这里我们一般会做一个ActivityStack,然后在Activity初始化和destory 时候入栈和出栈来控制界面的显示逻辑,当检测到栈为空的时候就可以认为要退出应用了。
  3. 能一次取完数据,不要分次取(虽然可能取得数据量有一定优化,但是分次太耗时,尽量交给上层做)
  4. DataBase is locked,导致数据取不出来。 解决方向有以下几点:
    第1点:在获取databse时候加一个锁
    第2点:分库,对于相互关联若的数据进行分库处理
    第3点:数据库不要关闭,并且尝试分开存writeable 和 readable两个静态的DB

参考文章:
https://segmentfault.com/q/1010000005140824

Android 数据库多线程读写

SQLiteDatabase源码解析

可以知道,insert , update , execSQL 都会调用lock();
query 没有调用lock(),but SQLiteCursor保存了查询条件,但是并没有立即执行查询,而是使用了lazy的策略,在需要时加载部分数据时候依然会SQLiteDatabase.lock()

多线程读写

多线程写

多线程写必须公用一个SqliteHelper,不然会抛出 database is locked的错误

多线程读

多线程读的话可以使用多个SqliteHelper

单写多读

方案: 一个线程写,多个线程同时读,每个线程都用各自SQLiteOpenHelper

问题1:有线程读的时候写数据库会抛出异常 database is locked

原因:
SQLiteOpenHelper.getReadableDatabase() 不见得获得的就是只读SQLiteDatabase

解决方案:
重写getOnlyReadDatabase 来强制只获取只读的Database

问题2:在FIX问题1后在有线程读的时候写数据库仍然会出现database is locked

原因:SQLiteDataBase有个属性 ENABLE_WRITE_AHEAD_LOGGING的属性(默认是关闭的),在关闭时,不允许读,写同时进行(通过锁来保证的);
当打开时,它允许一个写线程与多个读线程同时在一个SQLiteDatabase上起作用。实现原理是写操作其实是在一个单独的文件,不是原数据库文件。所以写在执行时,不会影响读操作,读操作读的是原数据文件,是写操作开始之前的内容。

解决方案:
SQLiteDataBase enableWriteAheadLogging()打开 ENABLE_WRITE_AHEAD_LOGGING属性。

参考文章:
http://www.cnblogs.com/javawebsoa/p/3237018.html
https://blog.csdn.net/qq_25412055/article/details/52414420

数据库调试

第一步通过AS自带的Android monitor 把数据库导出来

see

第二步通过Sqlite professional

查看数据库的结构可以具体查看到每一张表的详细信息
see
也可以通过query工具栏,执行基本的命令行操作。

Sqlite基本命令:http://www.runoob.com/sqlite/sqlite-trigger.html

Okhttp核心流程

发表于 2016-11-01 | 分类于 android

Interface:OkhttpClient(singleton) Request Response

主要由以下三部分 组成

1. 任务调度: 核心类disruptor(singleton)

a. 线程池
b. 队列

/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
 private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

(由于okhttp两种 运行模式 sync 和async)

2. 网络请求

  • 核心类RealCall HttpEngine StreamAllocation Interceptor (及其中的内部类 Chain)
  • 每个请求会生成一个Request
  • 然后根据 request 和httpclieant 生成唯一的 RealCall(在real里面会构造出一个httpengine)

okhttp一个请求的完整流程图
image

在realcall的execute方法中会调用

client.dispatcher().executed(this);//问题1  
Response result = getResponseWithInterceptorChain(false);//问题2  

获得请求结果.
现在看问题1 在这里面干了写什么
其实就是 dispatcher记录任务而已 ,没有任何执行方法
核心在问题2中
她会

Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
return chain.proceed(originalRequest);

也就是生成拦截器 ,并用这个拦截器启动这个请求
继续进入proceed 可以看到
进入了ApplicationInterceptorChain的getresponse方法
开始构造HttpEngine

engine.sendRequest();//问题21
engine.readResponse();//问题22

在问题21中其实就是 Rfc 标准的一种 实现

InternalCache responseCache = Internal.instance.internalCache(client);
Response cacheCandidate = responseCache != null
    ? responseCache.get(request)
    : null;

就是看当前是否配置了cache 如果有cache 则在responseCache.get找 当前request的cache
这个过程 就是根据 Util.md5Hex(request.url().toString())的从map中取值cacheCandidate的过程
当取到值后还要根据http cache的rfc标准 判断 是否合理能使用
new CacheStrategy.Factory(now, request, cacheCandidate).get();
在CacheStrategy中 这里会真正的判断和并处理当前的reponse cache和request(处理request是为了 当cache过期后还可以通过304 机制实现重用);

这里需要判断 当前的 cacheCOntrol Expires等 (尤其要处理一种情况就是当cache过期了 要根据 etag和Last-Modified 等等 要在request 中添加参数)

if (etag != null) {
  conditionalRequestBuilder.header("If-None-Match", etag);
} else if (lastModified != null) {
  conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
} else if (servedDate != null) {
  conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
}

Request conditionalRequest = conditionalRequestBuilder.build();

这是请求前的工作
现在开始发送请求

httpStream = connect();
httpStream.setHttpEngine(this);

这一部分的核心是StreamAllaction(辅助是ConnectionPool 和Route RouteSelector)

streamAllocation.newStream(client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis(),
client.retryOnConnectionFailure(), doExtensiveHealthChecks);

解决的问题 是socket连接重用和 route
先是从连接池中找

// Attempt to get a connection from the pool.
 RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
 if (pooledConnection != null) {
   this.connection = pooledConnection;
   return pooledConnection;
 }

如果没有的话就看route可以重用嘛

 if (selectedRoute == null) {
  selectedRoute = routeSelector.next();
  synchronized (connectionPool) {
    route = selectedRoute;
  }
}
RealConnection newConnection = new RealConnection(selectedRoute);
acquire(newConnection);

(这其实和一个dns解析 有关系也就是 一个域名对应多个ip 而且不一定所有的ip都是通的 建立连接的时候会尝试 所有的 直到通了为止

RouteDatabase里的Set<Route> failedRoutes = new LinkedHashSet<>())

弄完之后就是放如

Internal.instance.put(connectionPool, newConnection);

连接池中 方便下次继续使用
下一步 就是建立socket连接的过程

 newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),
    connectionRetryEnabled);
routeDatabase().connected(newConnection.route());

跟进去 就是

connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);

继续

 try {
  Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
  throw new ConnectException("Failed to connect to " + route.socketAddress());
}
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));

连接socket(返回连接 是以Http1xStream Http2xStream)形式返回回来的
然后才是真正发送这个请求的过程

httpStream.writeRequestHeaders(networkRequest);
requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);

继续跟进

 httpEngine.writingRequestHeaders();
  String requestLine = RequestLine.get(
    request, httpEngine.getConnection().route().proxy().type());
writeRequest(request.headers(), requestLine);

》》

** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
sink.writeUtf8(requestLine).writeUtf8("\r\n");
for (int i = 0, size = headers.size(); i < size; i++) {
  sink.writeUtf8(headers.name(i))
      .writeUtf8(": ")
      .writeUtf8(headers.value(i))
      .writeUtf8("\r\n");
}
sink.writeUtf8("\r\n");
state = STATE_OPEN_REQUEST_BODY;
}

读过程
就是通过这个stream构造出 newChunkedSink 或者newFixedLengthSink

到此 请求reponse的工作做完了
现在 如果是需要真正发送网络请求就也就是问题22

networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);

》》

engin.readResponse

》》

// Write the request body to the socket.
if (requestBodyOut != null) {
  if (bufferedRequestBody != null) {
    // This also closes the wrapped requestBodyOut.
    bufferedRequestBody.close();
  } else {
    requestBodyOut.close();
  }
  if (requestBodyOut instanceof RetryableSink) {
    httpStream.writeRequestBody((RetryableSink) requestBodyOut);
  }
}

networkResponse = readNetworkResponse();

》》

Response readNetworkResponse() throws IOException {
httpStream.finishRequest();

Response networkResponse = httpStream.readResponseHeaders()
    .request(networkRequest)
    .handshake(streamAllocation.connection().handshake())
    .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
    .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
    .build();

if (!forWebSocket) {
  networkResponse = networkResponse.newBuilder()
      .body(httpStream.openResponseBody(networkResponse))
      .build();
}

if ("close".equalsIgnoreCase(networkResponse.request().header("Connection"))
    || "close".equalsIgnoreCase(networkResponse.header("Connection"))) {
  streamAllocation.noNewStreams();
}

return networkResponse;

}

解析和返回

 Response response = engine.getResponse();


//tobe continue(已经 cache过程 请求重试过程)
 Request followUp = engine.followUpRequest();

4. Interceptor.Chain 请求链路

4.1 chain的初始化

第一个拦截器RetryAndFollowUpInterceptor 是在初始化RealCall 时候new的

RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
   final EventListener.Factory eventListenerFactory = client.eventListenerFactory();

   this.client = client;
   this.originalRequest = originalRequest;
   this.forWebSocket = forWebSocket;
   this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);

   // TODO(jwilson): this is unsafe publication and not threadsafe.
   this.eventListener = eventListenerFactory.create(this);
 }

后面再真正执行时候创建了其他拦截器

@Override public Response execute() throws IOException {
  try {
    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
  } finally {
    client.dispatcher().finished(this);
  }
}

      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 (!forWebSocket) {
           interceptors.addAll(client.networkInterceptors());
         }
         interceptors.add(new CallServerInterceptor(forWebSocket));

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

我们经常自定义的

Applicationinterceptor 》》 client.interceptors() 
NetWorkclientepter 》》client.networkInterceptors()    

责任链的实现 是需要通过 在每个层级Interceptor里面new RealInterceptorChain 实现的

每次new的时候都是在当前index+1, 而且里面传入所有的interceptors

// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(
    interceptors, streamAllocation, httpCodec, connection, index + 1, request);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);

RealInterceptorChain 中的

proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection)

最后会调用 interceptor.intercept(next)

response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);

每个interceptor 都会在intercept方法里面调用本层级的 ((RealInterceptorChain) chain).proceed 进而递归到最后一层

最后递归到 CallServerInterceptor 的intercept方法:

@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  HttpCodec httpCodec = realChain.httpStream();
  StreamAllocation streamAllocation = realChain.streamAllocation();
  RealConnection connection = (RealConnection) realChain.connection();
  Request request = realChain.request();

  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {

    if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
      httpCodec.flushRequest();
      responseBuilder = httpCodec.readResponseHeaders(true);
    }

    if (responseBuilder == null) {
      // Write the request body if the "Expect: 100-continue" expectation was met.
      Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
    } 

  Response response = responseBuilder
      .request(request)
      .handshake(streamAllocation.connection().handshake())
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
       ......
  return response;
}

最后在每一个IntercepterChain里面的

Response response = interceptor.intercept(next);

拿到里面一层的intercepter的response 然后加工最后返回给上一层的intercepter

例如BridgeInterceptor里面会拿到CacheInterpceter里面返回的response 然后处理一下返回给上一层的RetryAndFollowUpInterceptor

 Response networkResponse = chain.proceed(requestBuilder.build());
 Response response = interceptor.intercept(next);
 HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
    .request(userRequest);

if (transparentGzip
    && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
    && HttpHeaders.hasBody(networkResponse)) {
  GzipSource responseBody = new GzipSource(networkResponse.body().source());
  Headers strippedHeaders = networkResponse.headers().newBuilder()
      .removeAll("Content-Encoding")
      .removeAll("Content-Length")
      .build();
  responseBuilder.headers(strippedHeaders);
  responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}

return responseBuilder.build();

PS:这些intercepter大多数都在okhttp3.internal.http包下面 。

4.3

虽然在ConnectInterceptor分配了Connection 但是却是在CallServerInterceptor发起的网络请求
(这里涉及到 HTTP1.1的长连接,一次建立TCP连接后,下一次请求同一域名,继续用这个通道传输数据)

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

4.2 这里的每个intercepter 都需要new 一个RealInterceptorChain 可以参考 Fresco的方案改进

RetryAndFollowUpInterceptor(BridgeInterceptor(CacheInterceptor(ConnectInterceptor(CallServerInterceptor()))))

这样一层嵌套一层,就可以直接不借助RealInterceptorChain实现责任链

4.3 日志

  • 请求结果日志

      compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'   
    
    public class HttpLogger implements HttpLoggingInterceptor.Logger {
      @Override
      public void log(String message) {
        Log.d("HttpLogInfo", message);
      }
    }  
    
    HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor(new HttpLogger());
    logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);   
    
    OkHttpClient client = new OkHttpClient.Builder()
                  .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
                  .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
        .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
        .addNetworkInterceptor(logInterceptor)
    
  • 请求时长日志

在okhttp里面打印:

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    //请求前--打印请求信息
    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    //网络请求
    Response response = chain.proceed(request);

    //网络响应后--打印响应信息
    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    return response;
  }
}   

在volley里面打印:

在GsonRequest里面 打印请求响应时间和请求日志

protected Response<T> parseNetworkResponse(NetworkResponse response) 

请求响应时长:可以在new GsonRequest时候记录开始时间,在parseNetworkResponse时候后打印最终响应时间
请求日志:直接打印response.data的结果就行了

  • 日志打印框架
    https://github.com/orhanobut/logger

参考资料:https://www.jianshu.com/p/d04b463806c8

3. cache管理

最后附上 相关的http请求cache的rfc内容
image

每个状态的详细说明如下:

1. Last-Modified

在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记(HttpReponse Header)此文件在服务期端最后被修改的时间,格式类似这样:
Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT
客户端第二次请求此URL时,根据HTTP协议的规定,浏览器会向服务器传送If-Modified-Since报头(HttpRequest Header),询问该时间之后文件是否有被修改过:
If-Modified-Since:Tue, 24 Feb 2009 08:01:04 GMT
如果服务器端的资源没有变化,则自动返回HTTP304(NotChanged.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。
注:如果If-Modified-Since的时间比服务器当前时间(当前的请求时间request_time)还晚,会认为是个非法请求

2. Etag工作原理

HTTP协议规格说明定义ETag为“被请求变量的实体标记”(参见14.19)。简单点即服务器响应时给请求URL标记,并在HTTP响应头中将其传送到客户端,类似服务器端返回的格式:
Etag:“5d8c72a5edda8d6a:3239″
客户端的查询更新格式是这样的:
If-None-Match:“5d8c72a5edda8d6a:3239″
如果ETag没改变,则返回状态304。
即:在客户端发出请求后,HttpReponse Header中包含Etag:“5d8c72a5edda8d6a:3239″
标识,等于告诉Client端,你拿到的这个的资源有表示ID:5d8c72a5edda8d6a:3239。当下次需要发Request索要同一个URI的时候,浏览器同时发出一个If-None-Match报头(Http RequestHeader)此时包头中信息包含上次访问得到的Etag:“5d8c72a5edda8d6a:3239″标识。
If-None-Match:“5d8c72a5edda8d6a:3239“
,这样,Client端等于Cache了两份,服务器端就会比对2者的etag。如果If-None-Match为False,不返回200,返回304(Not Modified) Response。

3. Expires

给出的日期/时间后,被响应认为是过时。如Expires:Thu, 02 Apr 2009 05:14:08 GMT
需和Last-Modified结合使用。用于控制请求文件的有效时间,当请求数据在有效期内时客户端浏览器从缓存请求数据而不是服务器端.当缓存中数据失效或过期,才决定从服务器更新数据。

4. Last-Modified和Expires

Last-Modified标识能够节省一点带宽,但是还是逃不掉发一个HTTP请求出去,而且要和Expires一起用。而Expires标识却使得浏览器干脆连HTTP请求都不用发,比如当用户F5或者点击Refresh按钮的时候就算对于有Expires的URI,一样也会发一个HTTP请求出去,所以,Last-Modified还是要用的,而且要和Expires一起用。

5. Etag和Expires

如果服务器端同时设置了Etag和Expires时,Etag原理同样,即与Last-Modified/Etag对应的HttpRequestHeader:If-Modified-Since和If-None-Match。我们可以看到这两个Header的值和WebServer发出的Last-Modified,Etag值完全一样;在完全匹配If-Modified-Since和If-None-Match即检查完修改时间和Etag之后,服务器才能返回304.

6. Last-Modified和Etag

分布式系统里多台机器间文件的last-modified必须保持一致,以免负载均衡到不同机器导致比对失败
分布式系统尽量关闭掉Etag(每台机器生成的etag都会不一样)
Last-Modified和ETags请求的http报头一起使用,服务器首先产生Last-Modified/Etag标记,服务器可在稍后使用它来判断页面是否已经被修改,来决定文件是否继续缓存
过程如下:

  • 客户端请求一个页面(A)。
  • 服务器返回页面A,并在给A加上一个Last-Modified/ETag。
  • 客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。
  • 客户再次请求页面A,并将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。
  • 服务器检查该Last-Modified或ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304和一个空的响应体。

备注:

  • Last-Modified和Etag头都是由WebServer发出的HttpReponse Header,WebServer应该同时支持这两种头。
  • WebServer发送完Last-Modified/Etag头给客户端后,客户端会缓存这些头;
  • 客户端再次发起相同页面的请求时,将分别发送与Last-Modified/Etag对应的HttpRequestHeader:If-Modified-Since和If-None-Match。我们可以看到这两个Header的值和WebServer发出的Last-Modified,Etag值完全一样;
  • 通过上述值到服务器端检查,判断文件是否继续缓存;

7.关于 Cache-Control: max-age=秒 和 Expires

Expires = 时间,HTTP 1.0 版本,缓存的载止时间,允许客户端在这个时间之前不去检查(发请求)
max-age = 秒,HTTP 1.1版本,资源在本地缓存多少秒。
如果max-age和Expires同时存在,则被Cache-Control的max-age覆盖。
Expires 的一个缺点就是,返回的到期时间是服务器端的时间,这样存在一个问题,如果客户端的时间与服务器的时间相差很大,那么误差就很大,所以在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代。
Expires =max-age + “每次下载时的当前的request时间”
所以一旦重新下载的页面后,expires就重新计算一次,但last-modified不会变化

沉浸式的实现方案

发表于 2016-10-01 | 分类于 Android

Step 1:

把状态栏设置为透明

    if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP&&!Build.BRAND.equalsIgnoreCase("huawei")){/**5.0及以上且不是华为,
            因为华为5.0以上系统已经全屏但是状态栏黑色改不掉*/
        Window window = getWindow();
        window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
                | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
        window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.M){
            window.setStatusBarColor(Color.TRANSPARENT);
            if(Build.MANUFACTURER.toLowerCase().contains("xiaomi")) {
                setMiuiStatusBarDarkMode(true);
            }
        }else{
            window.setStatusBarColor(Color.argb(0x80,0x00,0x00,0x00));
        }

    }else{/**5.0以下,4.0及以上*/
        Window win = getWindow();
        WindowManager.LayoutParams winParams = win.getAttributes();
        int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
        winParams.flags |= bits;
        win.setAttributes(winParams);
    }
    /**由于MIUI 6修改过所以系统方法无法设置状态栏字体颜色无效,需要使用该方法设置
    *ps:mui上设置状态栏字体颜色为深色
    *
    */
    public void setMiuiStatusBarDarkMode(boolean isdarkmode) {
    Class<? extends Window> clazz = this.getWindow().getClass();
    try {
        int darkModeFlag = 0;
        Class<?> layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
        Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
        darkModeFlag = field.getInt(layoutParams);
        Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
        extraFlagField.invoke(this.getWindow(), isdarkmode ? darkModeFlag : 0, darkModeFlag);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Step 2:

设置绘制区域

rootView.setFitsSystemWindows(true);
rootView.setClipToPadding(true);

这样rootView就顶上头部去了,但是有个副作用就是界面显示区域也扩展下面的虚拟按键后面了,所以要对于alignparentbottom的功能栏,就不能直接放到rootView里面,必须放到与rootView外面。这样功能栏就不会被底部的虚拟按键盖住(这里不要考虑计算虚拟按键的高度,已经是否有虚拟按键,华为手机可以通过手势动态展开或者收起虚拟按键)

注意机型:

  1. nexus固定死的虚拟按键
  2. 华为和小米Mix可以使用手势控制的虚拟按键

AS工程导入

发表于 2015-11-01 | 分类于 Android

1.gradle版本对应关系

  • classpath ‘com.android.tools.build:gradle:2.3.0’

    可以支持gradle-wrapper.properties中的
    distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
    从3.3 到4.4

  • classpath ‘com.android.tools.build:gradle:2.2.3’

    可以支持gradle-wrapper.properties中的
    distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-all.zip

    从3.0到哪3.2

  • classpath ‘com.android.tools.build:gradle:1.5.0’

可以支持gradle-wrapper.properties中的
distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip

从2.4到哪2.9

2. 使用离线gradle的方法

gradle的全版本(已经上传到LYC的网盘):
https://pan.baidu.com/disk/home?#/all?vmode=list&path=%2Fgradlewrapper

2.1 导入新工程

首先校验里面的/gradle/wrapper/gradle-wrapper.properties
如果本地(/Users/liuyc/.gradle/wrapper/dists)没有这个版本的gradle会去下载,
这时候会在dists目录生成一个”gradle-2.2-all/*/“目录

使用离线gradle的方法:

  • cancel掉本次gradle编译,并完全退出AS(不只是关闭工程)
  • 把离线的gradle zip包拷贝进那个随机名的目录,删除这个目录的其他文件
  • 再次启动AS打开这个Project,这样AS就使用这个Zip包了

    2.2 命令行打包(gradlew 命令)

    虽然导入工程时候做了一遍了,但是这个命令还是会额外再下一次gradle版本,同样会在”/Users/liuyc/.gradle/wrapper/dists”目录再生成一个”gradle-2.2-all/*/“目录

使用离线gradle的方法同上

PS:最终每个”/Users/liuyc/.gradle/wrapper/dists/gradle-..*-all”会有两个随机名的目录
(一个对应于编译时候,另外一个对应于gradlew命令行时候的)

2.3 不要忘了gradle-wrapper.property要和build.gradle里面的配置对应

对应关系参考上面的

3. AS设置里面的离线gradle

see

4.工程导入

4.1 普通eclipse 工程导入

直接import,AS会提示转成eclipse工程

4.2 github上的工程导入

用AS打开top level的settings.gradle选择进行配置导入
ps:如果这一步直接导入的话就会报 Could not find method android() for arguments

可能遇到的问题:

  1. 配置sdk位置(顶层build.gradle同级)
    local.properties(建议从已有工程拷贝一个)
    sdk.dir=/Users/lyc/codeTools/android-sdk

  2. 配置模块的build.gradle的gradletoolVersion版本
    buildToolsVersion ‘23.0.2’

建议从已有工程的里面找一个可用的版本填上去

查看自己已经有哪些版本的buildTools的方法,

see

  1. modle配置模块的工程compileSdkVersionversion
    为一个已有版本的
  1. 配置
    dependencies中的com.android.support的版本,要求是于compileSdkVersionversion的版本一直
    ,但是这个不好处理

compile ‘com.android.support:design:25.1.1’
compile ‘com.android.support:appcompat-v7:25.1.1’
compile ‘com.android.support:cardview-v7:25.1.1’

英文有子序列号
建议按照以下方法写
compile ‘com.android.support:cardview-v7:25.+’
这样就会取本版本号下面最大的一个

see

5.配置本地Gradel版本
这个常见于直接从别人那里拷贝的工程(也就是带有gradle/wrapper/**)的工程

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 

gradle会去找这个版本的gradle,如果找不到就会重新下载
gradle的默认下载目录在用户目录的.gradle/wrapper/dists文件夹中(隐藏的) 里面会有所有已经下载的gradle 版本,可以把它改成一个已经有的版本就行

1.3 本地工程导入

直接import

iOS入门

发表于 2015-11-01 | 分类于 iOS

第一课:熟悉OC语法

第二课:storyboard和 手写View

storyboard;

直接往上面拖放控件,然后按住ctr 把控件拖动到 @implementation,就相当于自动findview了,可以直接使用是这个控件了

@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;

还有一种就是直接关联上点击事件
把控件直接拖动到这个申明方法上就可以了

- (IBAction)showAlert:(id)sender;

这个的原理可以看看storyboard的文件内容就可以明白了,大致和android的类似

图片资源文件两种放法

  1. 在Supporting Files 同级添加一个Resources的包,里面直接放图片进去(可以只放三倍图)

    UIImageView *imageView = [[UIImageView alloc] init];
       imageView.image = [UIImage imageNamed:@"home_banner.png"];    // 正常显示的图片
    
  2. 在Assets.xcaseset里面右键新建New image set,然后手动把图片拖动到右边的一倍两倍三倍的框框中去

storyboard 改成手写UI类型

  1. 第一步:
  • 删除storyboard文件
  • 编辑Supporting Files目录下的 .plist文件,
    删除Main storyboard file name这一项
  1. 第二步:
    编辑AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [self.window makeKeyAndVisible];

    ViewController *__rootController = [[ViewController alloc] init];
    UINavigationController *__navCtrler = [[UINavigationController alloc] initWithRootViewController:__rootController];
    __navCtrler.navigationBarHidden = YES;
    self.window.rootViewController = __navCtrler;
  // Override point for customization after application launch.
    return YES;
}

这个相当于Application的oncreate,在这里加载Entry activity

在ViewController里面手动往根View里面添加控件

- (void)viewDidLoad {
    [super viewDidLoad];

    UILabel *firstLable=[[UILabel alloc]init];
    firstLable.frame=CGRectMake(0, 0, 100, 100);
    firstLable.backgroundColor = [UIColor whiteColor]; //设置lable背景颜色为黑色

    firstLable.text=@"第一级";
    firstLable.userInteractionEnabled = YES;

    [firstLable setTextColor:[UIColor greenColor]]; //设置文本字体颜色为白色

    [self.view addSubview:firstLable];
    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapLable:)];
    [firstLable addGestureRecognizer:singleTap];

    // Do any additional setup after loading the view, typically from a nib.
}

viewDidLoad 相当于Activity的oncreate,这里我们就是手写View,然后加到根View里面去 self.view
[self.view addSubview:firstLable];

第三课:Viewcontroller 之间的跳转

在ViewController从一个Viewcontroller跳到另外一个SecondViewController

-(void) tapLable:(UILabel *)sender{
    NSLog(@"进入第二级Controller");

    SecondViewController *controller = [[SecondViewController alloc] init];
    [self.navigationController pushViewController:controller animated:YES];
}

在SecondViewController也是一样的写法

返回第一个VIewCOntroller的方法

[self.navigationController popViewControllerAnimated:YES];

AS 踩坑日记

发表于 2015-11-01 | 分类于 工具

升级instant run时候的报错
Error:Access to the dex task is now impossible, starting with 1.4.0
1.4.0 introduces a new Transform API allowing manipulation of the .class files.
See more information: http://tools.android.com/tech-docs/new-build-system/transform-api

解决方案:

buildscript {
repositories {
jcenter()
}
dependencies {
classpath ‘com.android.tools.build:gradle:2.0.0’

}

}

改成1.3.0

项目根目录的gradle的
grade.property

distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip

混淆写法

发表于 2015-11-01 | 分类于 打包

-keepclassmembers class {
public void onEvent*(
);
}

只保护 public void onEvent(*)的方法不被重命名

-keepattributes Signature

-keep class okio.* {;}

-keep class android.support.v4.app.NotificationCompat*{
public
;
}

-keep class com.veda.lyc.Utils{ static *;}
保持里面的静态方法不被混淆

keep的几个选项

Keep From being removed or renamed From being renamed
Classes and class members -keep -keepnames
Class members only -keepclassmembers -keepclassmembernames
-keepclassmembers -keepclassmembernames

Classes and class members, if class members present | -keepclasseswithmembers | -keepclasseswithmembernames |

参考资料
http://blog.csdn.net/sudic_niu/article/details/7921548

AS和Xcode快捷键

发表于 2015-11-01 | 分类于 工具

Xcode

XCode 不能直接修改文件名,需要找到本文件对应的申明或者实现的名称,右键refactor>rename 修改
(记得勾选 Rename related files)

在 Xcode里面所有的窗口控制显示与否都可以通过右上角的来控制

定位类的位置 shift commoand j

commond 6

option 然后点击方法位置

commond T

shift commoand O

shift commoand F

commoand F

esc 查看方法参数

快捷键位置方法 XCode>preferences>Key Bindings

显示主题颜色设置 XCode>preferences>Font & Colors 从Default 改成 Dusk
窗口显示和隐藏的button(其中代码下面的debug和log窗口在他们的右下角又有单独的控制显示和隐藏的button)

image

image

参考资料:http://www.cocoachina.com/ios/20141224/10752.html

AndroidStudio

快捷键位置方法 AndroidStudio>preferences>Keymap (把Mac OS X 复制一份,然后修改)

显示主题颜色设置 AndroidStudio>preferences>Editor>Colors & Font 从Default 改成 Darcula (然后点击 save as ,再把字体调大一点,不能直接修改)

这里主要看名字方便以后找快捷键
显示类里面的方法:
image
查找类:
image
自动生成(构造函数,get,setter…):
image
删除一行:
image
找到方法的所有调用:
image
格式化代码:
image
复制一行:
image
区块加注释:
image

Android模块化方案

发表于 2015-11-01 | 分类于 Android

AAR模块化方案

1、首先 按照应用分层(common view),注意每个module最好配置一下resourcePrefix “mc_”
2、上层再按照功能模块比如 信用卡和个人中心

这样每个都会打出一个aar,府工程引入所有的aar就可以引入这个模块

问题

但是这个距离一个可以发布出去的aar还有距离,有以下几个问题需要解决:

  1. 本地jar依赖(主工程很有可能也公用这个jar);
  2. 混淆的问题;
  3. 多个aar合并;
  4. 兄弟模块互相调用,基模块调用父模块的方法;

问题1:

Android dependency的几种方法:
eg:

    testCompile 'junit:junit:4.12'
//compile fileTree(dir: 'libs', include: ['*.jar'])  
provided fileTree(dir: 'libs',include: ['*.jar'])  
compile 'com.android.support:support-v4:23.0.1'  
compile project(':Module_common')   
  • testCompile : debug 会编译 正式打包不会编译
  • compile : 除去”compile jar”会编译进arr,其余的都不会编译进去
  • provided:编译时候不会把jar编译进去

传递依赖的问题
(例如 modle A 依赖 Module B,Module B 又依赖Module C ) jar 都放在C里面,Module A不引入jar 也能引用到jar。

但是正式打包时候不需要这些jar,本地的jar必须以provided方式引入,这样aar里面就不会打进去这些jar了。

这会引入另外一个问题:C改成provide 这些jar后,A和B找不到这些依赖的jar 会编译失败

解决方案:
把jar单独成一个module D,写成provided
(也可在module A 和 B里面 把这些jar 都拷贝进去,然后统统写成provided)

问题2

首先要明白,我们要去混淆的是A和主工程的接口,而A和B以及C直接的调用都需要混淆,但是不能在B和C里面混淆,因为这样一混淆的话,A与B、C调用的接口也混淆了

所以混淆只能在A里面打开,A和B都不能打开混淆

问题3

默认会打出三个aar,但是我们只能发布一个aar出去,所以必须使用到 https://github.com/adwiv/android-fat-aar 写的合并aar的gradle

方法:
1、拷贝 fat-aar.gradle到build.gradle 同级目录
2、module的build.gradle新增
apply from: ‘fat-aar.gradle’ ,

compile project(‘:Module_common’)
改成
embedded project(‘:Module_common’)

这样在打B的aar时候,会把基moduel的aar合并进来

如果这个模块工程就一个工程,就只需要解决问题1,同时配置一下混淆就行

问题4

通过hook调用
基模块:

public class CommonModuleDataEngine {

    private static CommonModuleDataEngine INSTANCE;

    private AppInfo appInfo;//这个必须要父模块或者兄弟模块
    public static CommonModuleDataEngine getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new CommonModuleDataEngine();
        }
        return INSTANCE;
    }

    private CommonModuleDataEngine() {
    }

    public String getAppInfo() {
        return appInfo.getAppInfo();
    }

    public static class Builder {
        private AppInfo appInfo;

        public Builder setAppInfo(AppInfo appInfo) {
            this.appInfo = appInfo;
            return this;
        }
        public void build() {
            CommonModuleDataEngine engin = CommonModuleDataEngine.getInstance();
            engin.appInfo = this.appInfo;

        }

    }
    public interface AppInfo {

        String getAppInfo();

    }

}

主工程

在Application初始化的时候初始几个基类CommonModuleDataEngine,

ps:内部类的去混淆

    -keepclasseswithmembers  class CommonModuleDataEngine {*;}
-keepclasseswithmembers  class CommonModuleDataEngine$* {*;}

Android工程拆解

可以这么抽象:
程序入库 壳工程 Project

程序界面 entry moudle

(它依赖于兄弟moudle_*,core_moudle)

问题1:大家都依赖相同的core_moudle ,account_moudle

必须做成gradle 依赖就可以了,注意保持上层moudle的版本号大于下层的

compile(“cn.lyc:android-account:${optimus_version}”) {
force true
exclude module: ‘lib-mcbd’
}

问题2:兄弟moudle的相互跳转

方案是统一在Application里面注册这个moudle,然后通过scheme跳转

问题3:兄弟moudle的数据获取

不要直接获取尽量都是通过在APPlication 里面启动收注册要公布的信息

其它注意事项

1、主程序和Module里面的Manifest里面的一些配置同名。必须在manifest 中的相应的配置里面 添加

tools:replace="android:icon,theme,label"

例如基module和主程序都包含了高德定位,manifest必然也有相同的配置信息

<meta-data
    tools:replace="android:value"
    android:name="com.amap.api.v2.apikey"
    android:value="*******" />

App组件化与业务拆分: http://www.jianshu.com/p/60c1b9ddd8ab
安卓组件化相关开源方案最全总结:
https://mp.weixin.qq.com/s/SbIWWj2kYC5kF7GEoRiWww
https://juejin.im/post/5a7ab8846fb9a0634514a2f5
https://github.com/JessYanCoding/MVPArms/

1234
Liuycheng

Liuycheng

34 日志
8 分类
34 标签
© 2018 Liuycheng
由 Hexo 强力驱动
主题 - NexT.Muse