遇到的情况: 当app变的特别大的时候,尤其是大公司,我们会引入很多其他部门的aar包。即使我们自身app的主工程使用了httpdns,但是 依旧无法避免其他aar包也会使用我们提供的httpdns服务,因为可以预见的是,不一定其他部门也会使用你使用的网络框架, 虽然大家现在都是用的ohhttp,但是特殊情况下比如有的部门还是使用httpurlconnection,甚至有的部门使用的协议都不是 http协议,比如多媒体部门提供的rtmp协议等。这些业务部门如果强制让他们使用我们的httpdns服务,代码要修改的地方 就比较多了,而且跨部门沟通问题也很大,那么有没有一种方法可以在全局层面修改dns服务呢?

答案是有的!(以下分析全部基于android-27源码,不同源码版本下面思路有别,运用到生产上需要做机型适配。)

我们在review了okhttp以及httpurlconnection以及rtmp等代码以后发现 只要是java层上面的dns查询服务都是 用的InetAddress这个类的这2个方法

public static InetAddress[] getAllByName(String host)
        throws UnknownHostException {
        return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
    }
    
     public static InetAddress getByName(String host)
        throws UnknownHostException {
        return impl.lookupAllHostAddr(host, NETID_UNSET)[0];
    }
复制代码

显然问题的关键在于这个impl是什么?那么点进去看以后 发现

原来是Inet6AddressImpl 提供的服务。再接着跟:

我们看下这个AddressCache的源码:

package java.net;

import libcore.util.BasicLruCache;


class AddressCache {
   
    private static final int MAX_ENTRIES = 16;

   
    private final BasicLruCache<AddressCacheKey, AddressCacheEntry> cache
            = new BasicLruCache<AddressCacheKey, AddressCacheEntry>(MAX_ENTRIES);

    static class AddressCacheKey {
        private final String mHostname;
        private final int mNetId;

        AddressCacheKey(String hostname, int netId) {
            mHostname = hostname;
            mNetId = netId;
        }

        @Override public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof AddressCacheKey)) {
                return false;
            }
            AddressCacheKey lhs = (AddressCacheKey) o;
            return mHostname.equals(lhs.mHostname) && mNetId == lhs.mNetId;
        }

        @Override public int hashCode() {
            int result = 17;
            result = 31 * result + mNetId;
            result = 31 * result + mHostname.hashCode();
            return result;
        }
    }

    static class AddressCacheEntry {
        
        final long expiryNanos;

        AddressCacheEntry(Object value) {
            this.value = value;
            this.expiryNanos = System.nanoTime() + TTL_NANOS;
        }
    }

  
    public void clear() {
        cache.evictAll();
    }

   
    public Object get(String hostname, int netId) {
        AddressCacheEntry entry = cache.get(new AddressCacheKey(hostname, netId));
        // Do we have a valid cache entry?
        if (entry != null && entry.expiryNanos >= System.nanoTime()) {
            return entry.value;
        }
        // Either we didn't find anything, or it had expired.
        // No need to remove expired entries: the caller will provide a replacement shortly.
        return null;
    }

    
    public void put(String hostname, int netId, InetAddress[] addresses) {
        cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(addresses));
    }

  
    public void putUnknownHost(String hostname, int netId, String detailMessage) {
        cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(detailMessage));
    }
}

复制代码

整体来说,这个AddressCache其实就是map,里面存着域名和ip的关系。那么如果我们想要修改全局的dns查询结果 那么我们只要修改这个AddressCache的内容即可。因为他们都是静态final的,全局唯一。

利用反射 hook 这个AddressCache的put或者get方法即可。这里给出一个demo:

/**
     * 其实除了hook方法,我们还可以hook addressCache这个对象,我们可以派生一个AddressCache对象,
     * 让addressCache指向他即可。注意AddressCache是一个hide的类,我们需要拿出来throw new RuntimeException("Stub!");
     * 这样在我们app代码里面才引用的到这个类
     * 
     * 这里只提供hook方法的思路。hook对象的大家自己完成吧。
     * @param host 域名
     * @param ip 你httpdns查询到的最佳ip地址
     */
    void hookInetAddress(String host, String ip) {
        //获取类的字节码文件对象
        Class c;
        try {
            //一定要注意这里 不同的android版本,hook的思路是一样的,但是细节有所差别,比如低版本
            //addressCache就是直接存在InetAddress里面作为一个静态变量的。和现在还有一层Inet6AddressImpl
            //代理是不同的 这里只是简单hook了 put方法,按需要你们还可以hook其他方法完成你们的需求
            c = Class.forName("java.net.Inet6AddressImpl");
            Field field = c.getDeclaredField("addressCache");
            field.setAccessible(true);
            Method putMethod = field.get(c).getClass().getDeclaredMethod("put", String.class, int.class, InetAddress[].class);

            String[] ipStr = ip.split("\\.");
            byte[] ipBuf = new byte[4];
            for (int i = 0; i < 4; i++) {
                ipBuf[i] = (byte) (Integer.parseInt(ipStr[i]) & 0xff);
            }
            putMethod.invoke(field.get(c), host, 0, new InetAddress[]{InetAddress.getByAddress(ipBuf)});

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

复制代码

最后实验一下 我们hook成功了没有:

//随便写一个ip吧,用来测试结果用
        hookInetAddress("www.baidu.com", "110.110.110.110");
        new Thread() {
            @Override
            public void run() {
                try {
                    InetAddress[] inetArray = InetAddress.getAllByName("www.baidu.com");
                    Log.e("wuyue", "inetArray size=" + inetArray[0].getHostAddress());
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }

            }
        }.start();
复制代码

Ok,成功。自此我们就可以在java层面上统一我们的dns服务啦。