参考资料:
什么是UserAgent
以下是摘自Wiki 的描述.
在计算机科学中,用户代理(英语:User Agent)指的是代表用户行为的软件代理程序所提供的对自己的一个标识符。例如,一个电子邮件阅读器就是一个电子邮件客户端,而在会话发起协议(SIP)中,用户代理指代的是一个通信会话的所有两个终端。
在HTTP中,User-Agent字符串通常被用于内容协商,而原始服务器为该响应选择适当的内容或操作参数。例如,User-Agent字符串可能被网络服务器用以基于特定版本的客户端软件的已知功能选择适当的变体。
通过使用robots.txt文件的可以设置网络抓取工具对网站的部分访问与否,而其设置标准之一就是用户代理字符串。换句话说,借由robots.txt文件的设置,可以让网站不能被特定的浏览器访问。
所以,每一个与网络进行连接的客户端,都应该有一个自己的UserAgent。在App开发当中使用UserAgent的情况一般在:
- WebView页面
- 网络库
获取WebView的UserAgent
UIWebView 和 WKWebView 与 JS 交互的方法有点区别,UIWebView 是同步的,而 WKWebView 是异步的。
UIWebView方式
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSLog(@"userAgent :%@", userAgent);
WKWebView方式
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero];
[wkWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
NSLog(@"userAgent :%@", result);
}];
默认WebView的UserAgent
以下是参考资料:iOS修改WebView的UserAgent中作者 iPhone 6s Plus,iOS 10.3.2 获取到的 UserAgent。
Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Mobile/14F89
无论使用 UIWebView 方式还是 WKWebView 方式,获取到的结果是一样的。也就是说,获取 UserAgent 不区分 webView 是哪个控件哪个内核。
修改UserAgent
修改UserAgent调用的核心代码:
NSString *newUserAgent = @"xxx";
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newUserAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
上述方式是修改全局的UserAgent,正常操作流程一般是我们先通过UIWebView
或者WKWebView
调用jsnavigator.userAgent
获取到系统的默认UserAgent,然后我们拿到这个UserAgent进行修改后在进行网页的加载或者网络请求。
修改UserAgent可能带来的问题
1. 修改UseraAgent在WebView加载网页的时候有时无效。
这是因为我们在Web页面实例之后,拿到对应的UserAgent,然后再进行修改,重新注册到全局UserAgent,此时已经实例的WebView的UserAgent已经确定,如果在全局设置之前已经进行loadRequest:
,就只能使用实例时候的UserAgent,造成后续修改就无法生效。
iOS9 WKWebView新增 API
/*! @abstract The custom user agent string or nil if no custom user agent string has been set.
*/
@property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));
可以直接修改 WKWebView 的 UserAgent,即使在WebView实例之后。
[self.wkWebView setCustomUserAgent:newUserAgent];
但是我们最终还是要适配iOS8,所以我们可以忽略这个特性,大家知道就好。
2. 不能确定当前使用的是哪一个UserAgent。
虽然我们修改过UserAgent,WebView对应的UserAgent会是我们想要的,但是如果我们再次实例WebView的时候,也会发现UserAgent也变成了我们修改过的UserAgent。倘若我们想使用原来的UserAgent就变的有点尴尬了😓。
可以得出的结论
我们知道,每次启动App,都会是系统默认的UserAgent,而调用[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
修改后再次获得的UserAgent都是修改后的UserAgent。
这样我们思考下,可以猜测,我们修改的UserAgent是存在在内存当中,每次App重启就都会获取到硬盘中存在的UserAgent。
解决方案
针对参考资料:iOS修改WebView的UserAgent中提出的解决方案,所做的是需要两个WebView,一个进行修改UserAgent,一个负责加载网页,其目的就是为了不改动加载网页的Web页的UA,能够确保得到想要的UA。
但是经过测试我发现,其实我们只要在WebView页面实例之前修改过UserAgent,就一定能够得到我们想要的UserAgent,所以,其实原理很简单,只要我们梳理清楚了,一切迎刃而解。
接下来我们需要做的就是:
- App启动时获取系统默认UserAgen进行存储。
- 使用WebView之前,对存储的默认UserAgent进行修改,通过
registerDefaults:
方法注册到内存中。 - WebView实例的时候从内存中拿到我们修改的userAgent。
网络库的UserAgent
ASIHTTPRequest
Takes the form "My Application 1.0 (Macintosh; Mac OS X 10.5.7; en_GB)"
// Build and set the user agent string if the request does not already have a custom user agent specified
if (![[self requestHeaders] objectForKey:@"User-Agent"]) {
NSString *tempUserAgentString = [self userAgentString];
if (!tempUserAgentString) {
tempUserAgentString = [ASIHTTPRequest defaultUserAgentString];
}
if (tempUserAgentString) {
[self addRequestHeader:@"User-Agent" value:tempUserAgentString];
}
}
+ (NSString *)defaultUserAgentString
{
@synchronized (self) {
if (!defaultUserAgent) {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
// Attempt to find a name for this application
NSString *appName = [bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
if (!appName) {
appName = [bundle objectForInfoDictionaryKey:@"CFBundleName"];
}
NSData *latin1Data = [appName dataUsingEncoding:NSUTF8StringEncoding];
appName = [[[NSString alloc] initWithData:latin1Data encoding:NSISOLatin1StringEncoding] autorelease];
// If we couldn't find one, we'll give up (and ASIHTTPRequest will use the standard CFNetwork user agent)
if (!appName) {
return nil;
}
NSString *appVersion = nil;
NSString *marketingVersionNumber = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
NSString *developmentVersionNumber = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
if (marketingVersionNumber && developmentVersionNumber) {
if ([marketingVersionNumber isEqualToString:developmentVersionNumber]) {
appVersion = marketingVersionNumber;
} else {
appVersion = [NSString stringWithFormat:@"%@ rv:%@",marketingVersionNumber,developmentVersionNumber];
}
} else {
appVersion = (marketingVersionNumber ? marketingVersionNumber : developmentVersionNumber);
}
NSString *deviceName;
NSString *OSName;
NSString *OSVersion;
NSString *locale = [[NSLocale currentLocale] localeIdentifier];
#if TARGET_OS_IPHONE
UIDevice *device = [UIDevice currentDevice];
deviceName = [device model];
OSName = [device systemName];
OSVersion = [device systemVersion];
#else
deviceName = @"Macintosh";
OSName = @"Mac OS X";
// From http://www.cocoadev.com/index.pl?DeterminingOSVersion
// We won't bother to check for systems prior to 10.4, since ASIHTTPRequest only works on 10.5+
OSErr err;
SInt32 versionMajor, versionMinor, versionBugFix;
err = Gestalt(gestaltSystemVersionMajor, &versionMajor);
if (err != noErr) return nil;
err = Gestalt(gestaltSystemVersionMinor, &versionMinor);
if (err != noErr) return nil;
err = Gestalt(gestaltSystemVersionBugFix, &versionBugFix);
if (err != noErr) return nil;
OSVersion = [NSString stringWithFormat:@"%u.%u.%u", versionMajor, versionMinor, versionBugFix];
#endif
// Takes the form "My Application 1.0 (Macintosh; Mac OS X 10.5.7; en_GB)"
[self setDefaultUserAgentString:[NSString stringWithFormat:@"%@ %@ (%@; %@ %@; %@)", appName, appVersion, deviceName, OSName, OSVersion, locale]];
}
return [[defaultUserAgent retain] autorelease];
}
return nil;
}
Alamofire
Example: iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0
public static let defaultHTTPHeaders: HTTPHeaders = {
// Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3
let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5"
// Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5
let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { index, languageCode in
let quality = 1.0 - (Double(index) * 0.1)
return "\(languageCode);q=\(quality)"
}.joined(separator: ", ")
// User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3
// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0`
let userAgent: String = {
if let info = Bundle.main.infoDictionary {
let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown"
let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown"
let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown"
let osNameVersion: String = {
let version = ProcessInfo.processInfo.operatingSystemVersion
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
let osName: String = {
#if os(iOS)
return "iOS"
#elseif os(watchOS)
return "watchOS"
#elseif os(tvOS)
return "tvOS"
#elseif os(macOS)
return "OS X"
#elseif os(Linux)
return "Linux"
#else
return "Unknown"
#endif
}()
return "\(osName) \(versionString)"
}()
let alamofireVersion: String = {
guard
let afInfo = Bundle(for: SessionManager.self).infoDictionary,
let build = afInfo["CFBundleShortVersionString"]
else { return "Unknown" }
return "Alamofire/\(build)"
}()
return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
}
return "Alamofire"
}()
return [
"Accept-Encoding": acceptEncoding,
"Accept-Language": acceptLanguage,
"User-Agent": userAgent
]
}()
Alamofire和AFNetWorking基本类似。项目中直接使用Alamofire,所以就不做展示。
总结
- 我们可以看到WebView和网络库都有自己的UserAgent。
- 如果我们要修改UserAgent的时候,最好保存一个自己默认的UserAgent。
- 修改WebView的UserAgent请在WebView实例之前就行修改,并注册到系统内存当中。