[daily practice of Android spring moves] LeetCode Hot 5 questions + Android framework

Posted by chrys on Sat, 12 Feb 2022 16:24:39 +0100

overview

LeetCode Hot: middle order traversal of binary tree, different binary search trees, verification binary search tree, symmetric binary tree, sequence traversal of binary tree
Android framework: establish connection and connection pool

LeetCode Hot

2.36 middle order traversal of binary tree

Given the root node of a binary tree, root, returns its middle order traversal.

//recursion
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inorder(root,res);
        return res;
    }

    void inorder(TreeNode root,List<Integer> res){
        if(root == null) return;
        inorder(root.left,res);
        res.add(root.val);
        inorder(root.right,res);
    }
}
//iteration
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        Deque<TreeNode> stk = new LinkedList<TreeNode>();
        while (root != null || !stk.isEmpty()) {
            while (root != null) {
                stk.push(root);
                root = root.left;
            }
            root = stk.pop();
            res.add(root.val);
            root = root.right;	//Next cycle stack
        }
        return res;
    }
}

2.37 different binary search trees

Give you an integer n and find how many kinds of binary search trees are composed of exactly n nodes with different node values from 1 to n? Returns the number of binary search trees that meet the meaning of the question.

/*
Suppose n nodes exist, the number of binary sorting trees is G (n), and let f(i) be the number of binary search trees with i as the root, then
G(n) = f(1) + f(2) + f(3) + f(4) + ... + f(n)
When I is the root node, the number of left subtree nodes is i-1 and the number of right subtree nodes is n-i, then
f(i) = G(i-1)*G(n-i)
Combining the two formulas, the Cartland number formula can be obtained
G(n) = G(0)*G(n-1)+G(1)*(n-2)+...+G(n-1)*G(0)
*/
//DP
class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;
        
        for(int i = 2; i < n + 1; i++)
            for(int j = 1; j < i + 1; j++) 
                dp[i] += dp[j-1] * dp[i-j];
        
        return dp[n];
    }
}

2.38 validate binary search tree

Give you the root node of a binary tree, root, and judge whether it is an effective binary search tree.

//Middle order traversal
class Solution {
    long pre = Long.MIN_VALUE;
    public boolean isValidBST(TreeNode root) {
        if (root == null) return true;
        
        // Access left subtree
        if (!isValidBST(root.left))  return false;

        // Accessing the current node: if the current node is less than or equal to the previous node traversed in the middle order, it indicates that it does not meet the BST and returns false; Otherwise, continue to traverse.
        if (root.val <= pre) return false;

        pre = root.val;
        // Access right subtree
        return isValidBST(root.right);
    }
}

2.39 symmetric binary tree

Give you the root node of a binary tree, root, and check whether it is axisymmetric.

Input: root = [1,2,2,3,4,4,3]
Output: true
class Solution {
	public boolean isSymmetric(TreeNode root) {
		if(root==null) {
			return true;
		}
		return dfs(root.left,root.right);
	}
	
	boolean dfs(TreeNode left, TreeNode right) {
		//The termination condition of recursion is that both nodes are empty
		//Or one of the two nodes is empty
		//Or the values of the two nodes are not equal
		if(left==null && right==null) {
			return true;
		}
		if(left==null || right==null) {
			return false;
		}
		if(left.val != right.val) {
			return false;
		}
		//Then recursively compare the left child of the left node with the right child of the right node
		//And compare the right child of the left node with the left child of the right node
		return dfs(left.left,right.right) && dfs(left.right,right.left);
	}
}

2.40 sequence traversal of binary tree

Give you the root node of the binary tree, root, and return the sequence traversal of its node value. (that is, access all nodes from left to right layer by layer).

//BFS
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();
        TreeNode cur;
        int len;
        if(root == null) return res;
        
        queue.add(root);
        while(!queue.isEmpty()){
            List<Integer> level = new ArrayList<>();
            len = queue.size();			//The queue size is changing and needs to be saved
            for(int i=0;i<len;i++){		//Each layer requires cyclic operation
                cur = queue.poll();
                level.add(cur.val);
                if(cur.left != null) queue.add(cur.left);
                if(cur.right != null) queue.add(cur.right);
            }
            res.add(level);
        }
        return res;
    }
}

Android framework

Establish connection

Interceptors such as RetryAndFollowUpInterceptor, BridgeInterceptor, CacheInterceptor, ConnectInterceptor and CallServerInterceptor will be added in OkHttp by default. This article mainly takes a look at the RetryAndFollupInterceptor and leads to the analysis of connection establishment.

RetryAndFollowUpInterceptor:

The most important codes of Interceptor are in intercept. Here are some codes in RetryAndFollowUpInterceptor#intercept:

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

    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);//Create a streamalallocation object

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();  //Call the release method
        throw new IOException("Canceled");
      }
    ...
    //Pass the streamalallocation object to the next Interceptor
    response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null); 
    ...
}

StreamAlloction:

Streamalallocation is a stream allocator by name. In fact, it manages several things as a whole.

Simply put, streamalallocation coordinates three things:

  • Connections: physical socket connections
  • Streams: logical HTTP request/response pair. Each Connection has a variable allocationLimit, which is used to define the number of concurrent streams that can be hosted. HTTP/1. The Connection of X can only have one stream at a time, and HTTP/2 can generally have multiple streams.
  • Calls: sequence of streams. An initial request may also have subsequent requests (such as redirection). OkHttp tends to make all streams run on the same connection in a call.

Streamalallocation provides API s to release the above resource objects. The next place to use the streamalallocation object created in RetryAndFollowUpInterceptor is ConnectInterceptor, and its intercept code is as follows:

@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");
    //Streams, the logical HTTP request/response pair.
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();	//Physical socket connection

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

newStream:

The newStream method in streamalallocation is used to find new RealConnection and HttpCodec. The code is as follows:

public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      //Find the available Connection through findHealthyConnection, and use this Connection to generate an HttpCodec object
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

findHealthyConnection is to find a healthy connection. The code is as follows:

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
      // successCount == 0 means it has not been used, so it can be used
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
 }

public boolean isHealthy(boolean doExtensiveChecks) {
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
      return false;
    }
    ... // Omit Http2 code
    return true;
  }

In an infinite loop, find a connection through findConnection and judge whether it is available. First, if it has not been used, it must be healthy and can be returned directly. Otherwise, call isHealth to judge whether the socket is closed. The socket here is assigned in findConnection. Look at the code of findConnection:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection.
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

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

      selectedRoute = route;
    }

    // If we need a route, make one. This is a blocking operation.
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();
    }

    RealConnection result;
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      // Now that we have an IP address, make another attempt at getting a connection from the pool.
      // 2. Get the ip address from the connectionpool again
      // This could match due to connection coalescing.
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) return connection;

      // Create a connection and assign it to this allocation immediately. This makes it possible
      // for an asynchronous cancel() to interrupt the handshake we're about to do.
      route = selectedRoute;
      refusedStreamCount = 0;
      // 3. There is no in the ConnectionPool. Create a new one
      result = new RealConnection(connectionPool, selectedRoute);
      // 3. Add streamalallocation to a queue in 'RealConnection'
      acquire(result);
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    // 4. Establish a connection and create a socket in it
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // Pool the connection.
      // 5. Put the newly created connection into the ConnectionPool 
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }

The above steps to create a Connection are as follows:

  1. Call internal The get method obtains a Connection from the ConnectionPool, which is mainly judged according to the host of the url, and the relevant code is in the ConnectionPool.
  2. If there is no IP address and the IP address is obtained again, obtain it again.
  3. If there is no in the ConnectionPool, create a new RealConnection and call acquire to add the streamalallocation to a queue in the RealConnection.
  4. Call the RealConnection#connect method to establish a connection, and a Socket will be created internally.
  5. Add the newly created Connection to the ConnectionPool.

After obtaining the Connection, create an HttpCodec object.

public HttpCodec newCodec(
      OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(client.readTimeoutMillis());
      source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
}

The corresponding HttpCodec is created according to whether Http1 or Http2, and the socket is created in the connect method in RealConnection.

RealConnection:

RealConnection encapsulates the underlying Socket connection. There must be a Socket object inside. The following are the variables inside RealConnection:

public final class RealConnection extends Http2Connection.Listener implements Connection {
  private static final String NPE_THROW_WITH_NULL = "throw with null exception";
  private final ConnectionPool connectionPool;
  //Route represents the path established with the server. In fact, Address is encapsulated internally, and Address is the URL of the request.
  private final Route route;

  //The rawSocket object represents the underlying connection, and another socket is used for Https. For ordinary Http requests, the two objects are the same. 
  private Socket rawSocket;

  private Socket socket;
  private Handshake handshake;
  private Protocol protocol;
  private Http2Connection http2Connection;
  //source and sink are the input and output streams obtained by encapsulating socket with Okio.
  private BufferedSource source;
  private BufferedSink sink;

  //The noNewStream object is used to identify that this Connection can no longer be used for Http requests. Once set to true, it will not change again.
  public boolean noNewStreams;

  public int successCount;

  //allocationLimit means that this Connection can carry at most several Http streams at the same time. For Http/1, there can only be one.
  public int allocationLimit = 1;

  //Allocations is a List object that holds the weak reference of the streamalallocation that is using the Connection. When streamalallocation calls acquire, its weak reference will be added to the List, and calling release will remove the reference. Empty allocations indicates that the Connection is idle. ConnectionPool uses this information to decide whether to close the Connection.
  public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

  /** Nanotime timestamp when {@code allocations.size()} reached zero. */
  public long idleAtNanos = Long.MAX_VALUE;
  ...
}

connect

RealConnection is used to establish a connection, which has the corresponding connect method:

public void connect(
      int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
    ...
    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout);
        } else {
          // Create socket and establish connection
          connectSocket(connectTimeout, readTimeout);
        }
        // establish
        establishProtocol(connectionSpecSelector);
        break;
      }
    ...
}

private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();
    // Create socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    try {
      // Establishing a connection is equivalent to calling the connect method of socket
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
      ce.initCause(e);
      throw ce;
    }
    
    try {
      // Get input / output stream
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw new IOException(npe);
      }
    }
}

If it is not Https, call connectSocket, create a rawSocket object internally, and set the timeout. Followed by platform get(). connectSocket calls the corresponding connect method according to different platforms, so that rawSocket can connect to the server. Then, Okio is used to encapsulate the input and output stream of rawSocket. The input and output stream here is finally handed over to HttpCodec for writing and reading of Http messages. Through the above steps, the connection of Http request is realized.

Summary:

This paper analyzes the basic process of OkHttp establishing Connection from creating streamalallocation object in RetryAndFollowupIntercept to creating RealConnection and HttpCodec in Connection. It can be seen that the connections in OkHttp are encapsulated by RealConnection, the input and output of Http stream are operated by HttpCodec, and streamalallocation manages these resources as a whole. In the process of finding and creating connections, a key thing is ConnectionPool, that is, Connection pool. It is responsible for managing all connections. OkHttp uses this Connection pool to reuse connections to improve the efficiency of network requests

Connection pool

Previously, we analyzed the process of OkHttp establishing connection, mainly involving several classes, including streamalallocation, RealConnection and HttpCodec, in which RealConnection encapsulates the underlying socket. Socket establishes a TCP connection, which consumes time and resources, while OkHttp uses connection pool to manage the connections here, reuse the connections and improve the efficiency of requests. The connection pool in OkHttp is implemented by ConnectionPool. This paper mainly analyzes this class.

In the findConnection method of streamalallocation, there is such a code:

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

Internal. instance. Finally, get gets a RealConnection from the ConnectionPool. If there is one, it returns directly. Here is the code in ConnectionPool:

@Nullable 
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    //connections is an ArrayDeque in the ConnectionPool
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
}

After taking a Connection from the queue, judge whether it can meet the reuse requirements:

public boolean isEligible(Address address, @Nullable Route route) {
    // If the allocated quantity of this Connection exceeds the allocation limit or is marked as unable to reallocate, false will be returned directly
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // Judge whether the variables in Address other than host are the same. If there are different variables, the connection cannot be reused
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // Judge whether the host is the same. If so, the Connection is reusable for the current Address
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }
   // Omit http2 related codes
   ...
}

boolean equalsNonHost(Address that) {
    return this.dns.equals(that.dns)
        && this.proxyAuthenticator.equals(that.proxyAuthenticator)
        && this.protocols.equals(that.protocols)
        && this.connectionSpecs.equals(that.connectionSpecs)
        && this.proxySelector.equals(that.proxySelector)
        && equal(this.proxy, that.proxy)
        && equal(this.sslSocketFactory, that.sslSocketFactory)
        && equal(this.hostnameVerifier, that.hostnameVerifier)
        && equal(this.certificatePinner, that.certificatePinner)
        && this.url().port() == that.url().port();
}

From the above code, the get logic is relatively simple and clear.

Next, take a look at put. In the findConnection method of streamalallocation, if a new Connection is created, it will be put into the Connection pool.

Internal.instance.put(connectionPool, result);

The final call is ConnectionPool#put:

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
}

First, judge whether the cleanup thread is started. If not, put the cleanupRunnable into the thread pool. Finally, put RealConnection into the queue.

cleanup:

Clean up the idle connection pool by cleaning up the idle thread:

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
};

There is an infinite loop in run. After calling cleanup, you get a time waitNano. If it is not - 1, it indicates the sleep time of the thread. Then call wait to enter sleep. If it is - 1, it means that there are no connections to clean up. Just return directly.

The main implementation of cleanup is in the cleanup method, and the following is its code:

long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // 1. Judge whether it is an idle connection
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // 2. Determine whether it is the connection with the longest idle time
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
      //  3. If the longest idle time exceeds the set maximum value, or the number of idle links exceeds the maximum number, clean up, otherwise calculate the waiting time to be cleaned up next time
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // We've found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }
     // 3. Close the connected socket
    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
}

The logic of cleaning up is roughly as follows:

  1. Traverse all connections and call pruneAndGetAllocationCount for each connection to determine whether it is an idle connection. If it is in use, directly traverse one.
  2. For idle connections, judge whether they are currently idle for the longest time.
  3. For the connection with the longest idle time, if it exceeds the set maximum idle time (5 minutes) or the maximum number of idle connections (5), clean up the connection. Otherwise, calculate the time to clean up next time, so that the cycle in cleanupRunnable will sleep for the corresponding time, and continue to clean up after waking up.

pruneAndGetAllocationCount is used to clean up potentially leaked streamalallocations and return the number of streamalallocations using this connection. The code is as follows:

private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<StreamAllocation>> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
      Reference<StreamAllocation> reference = references.get(i);

      if (reference.get() != null) {
        i++;
        continue;
      }

      // We've discovered a leaked allocation. This is an application bug.
      // If the streamalallocation reference is recycled, but the reference list of connection is thrown and held, a memory leak may occur
      StreamAllocation.StreamAllocationReference streamAllocRef =
          (StreamAllocation.StreamAllocationReference) reference;
      String message = "A connection to " + connection.route().address().url()
          + " was leaked. Did you forget to close a response body?";
      Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

      references.remove(i);
      connection.noNewStreams = true;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
}

If the streamlocation has been recycled, it indicates that the code of the application layer does not need this Connection, but the Connection still holds the reference of streamlocation, it indicates that the release(RealConnection connection) method in streamlocation has not been called, which may be caused by reading the ResponseBody without closing I/O.

Summary:

The connection pool in OkHttp is mainly used to save a queue of connections in use. For multiple connections that meet the conditions of the same host, reuse the same RealConnection to improve the request efficiency. In addition, the thread will be started to clean up realconnections that are idle timeout or exceed the idle number.

@However

Blog address: https://segmentfault.com/a/1190000012656606

summary

1. These algorithm problems about binary tree are relatively simple and are all basic operations;
2.OkHttp source code analysis is basically here, which mainly depends on understanding memory and being able to tell it in your own words;

Topics: Java Android leetcode