在Android端如果OkHttp作為網(wǎng)絡(luò)請求框架泻拦,由于其提供了自定義DNS服務(wù)接口,可以很優(yōu)雅地結(jié)合HttpDns忽媒,相關(guān)實(shí)現(xiàn)可參考:HttpDns+OkHttp最佳實(shí)踐争拐。
如果您使用HttpClient
或HttpURLConnection
發(fā)起網(wǎng)絡(luò)請求,盡管無法直接自定義Dns服務(wù)晦雨,但是由于HttpClient
和HttpURLConnection
也通過InetAddress
進(jìn)行域名解析架曹,通過修改InetAddress
的DNS緩存隘冲,同樣可以比通用方案更為優(yōu)雅地使用HttpDns。
InetAddress在虛擬機(jī)層面提供了域名解析能力绑雄,通過調(diào)用InetAddress.getByName(String host)
即可獲取域名對應(yīng)的IP展辞。調(diào)用InetAddress.getByName(String host)
時,InetAddress
會首先檢查本地是否保存有對應(yīng)域名的ip緩存万牺,如果有且未過期則直接返回罗珍;如果沒有則調(diào)用系統(tǒng)DNS服務(wù)(Android的DNS也是采用NetBSD-derived resolver library來實(shí)現(xiàn))獲取相應(yīng)域名的IP,并在寫入本地緩存后返回該IP脚粟。
核心代碼位于java.net.InetAddress.lookupHostByName(String host, int netId)
public class InetAddress implements Serializable {
...
/**
* Resolves a hostname to its IP addresses using a cache.
*
* @param host the hostname to resolve.
* @param netId the network to perform resolution upon.
* @return the IP addresses of the host.
*/
private static InetAddress[] lookupHostByName(String host, int netId)
throws UnknownHostException {
BlockGuard.getThreadPolicy().onNetwork();
// Do we have a result cached?
Object cachedResult = addressCache.get(host, netId);
if (cachedResult != null) {
if (cachedResult instanceof InetAddress[]) {
// A cached positive result.
return (InetAddress[]) cachedResult;
} else {
// A cached negative result.
throw new UnknownHostException((String) cachedResult);
}
}
try {
StructAddrinfo hints = new StructAddrinfo();
hints.ai_flags = AI_ADDRCONFIG;
hints.ai_family = AF_UNSPEC;
// If we don't specify a socket type, every address will appear twice, once
// for SOCK_STREAM and one for SOCK_DGRAM. Since we do not return the family
// anyway, just pick one.
hints.ai_socktype = SOCK_STREAM;
InetAddress[] addresses = Libcore.os.android_getaddrinfo(host, hints, netId);
// TODO: should getaddrinfo set the hostname of the InetAddresses it returns?
for (InetAddress address : addresses) {
address.hostName = host;
}
addressCache.put(host, netId, addresses);
return addresses;
} catch (GaiException gaiException) {
...
}
}
}
其中addressCache
為InetAddress
的本地緩存:
private static final AddressCache addressCache = new AddressCache();
結(jié)合InetAddress
的解析策略覆旱,我們可以通過如下方法實(shí)現(xiàn)自定義DNS服務(wù):
- 通過HttpDns SDK獲取目標(biāo)域名的ip
- 利用反射的方式獲取到
InetAddress.addressCache
對象 - 利用反射方式調(diào)用
addressCache.put()
方法,域名和ip的對應(yīng)關(guān)系寫入InetAddress
緩存
具體實(shí)現(xiàn)可參考以下代碼:
public class CustomDns {
public static void writeSystemDnsCache(String hostName, String ip) {
try {
Class inetAddressClass = InetAddress.class;
Field field = inetAddressClass.getDeclaredField("addressCache");
field.setAccessible(true);
Object object = field.get(inetAddressClass);
Class cacheClass = object.getClass();
Method putMethod;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//put方法在api21及以上為put(String host, int netId, InetAddress[] address)
putMethod = cacheClass.getDeclaredMethod("put", String.class, int.class, InetAddress[].class);
} else {
//put方法在api20及以下為put(String host, InetAddress[] address)
putMethod = cacheClass.getDeclaredMethod("put", String.class, InetAddress[].class);
}
putMethod.setAccessible(true);
String[] ipStr = ip.split("\\.");
byte[] ipBuf = new byte[4];
for(int i = 0; i < 4; i++) {
ipBuf[i] = (byte) (Integer.parseInt(ipStr[i]) & 0xff);
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
putMethod.invoke(object, hostName, 0, new InetAddress[] {InetAddress.getByAddress(ipBuf)});
} else {
putMethod.invoke(object, hostName, new InetAddress[] {InetAddress.getByAddress(ipBuf)});
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
和通用方案相比核无,使用該方法具有下列優(yōu)勢:
- 實(shí)現(xiàn)簡單
- 通用性強(qiáng)扣唱,該方案在HTTPS,SNI以及設(shè)置Cookie等場景均適用。規(guī)避了證書校驗(yàn)厕宗,域名檢查等環(huán)節(jié)
- 全局生效画舌,
InetAddress.addressCache
為全局單例,該方案對所有使用InetAddress
作為域名解析服務(wù)的請求全部生效
<font color="red">另外使用該方案請務(wù)必注意以下幾點(diǎn):</font>
-
AddressCache
的默認(rèn)TTL為2S已慢,且默認(rèn)最多可以保存16條緩存記錄:class AddressCache { ... /** * When the cache contains more entries than this, we start dropping the oldest ones. * This should be a power of two to avoid wasted space in our custom map. */ private static final int MAX_ENTRIES = 16; // The TTL for the Java-level cache is short, just 2s. private static final long TTL_NANOS = 2 * 1000000000L; } }
Android虛擬機(jī)下反射規(guī)則與JVM存在差異曲聂,無法直接修改final變量的值。所以使用該方法請務(wù)必注意IP過期時間及緩存數(shù)量佑惠。另外針對該問題可嘗試另一種解決方案:重寫AddressCache類朋腋,并通過ClassLoader優(yōu)先加載,覆蓋系統(tǒng)類膜楷。
AddressCache.put
方法在 API 21進(jìn)行了改動旭咽,增加了netId
參數(shù),為保證兼容性需要針對不同版本區(qū)別處理赌厅。具體方案參考上文代碼該方式可以解決HTTPS穷绵,SNI以及設(shè)置cookie等場景,但不適用于WebView場景特愿。Android Webview使用
Chromium
或Webkit
作為內(nèi)核(Android 4.4開始仲墨,Webview內(nèi)核由Chromium替代Webkit)。上述兩者均繞開InetAddress而直接使用系統(tǒng)DNS服務(wù)揍障,所以該方案對此場景無效目养。