01 NSTimer造成的內(nèi)存泄漏問題?
1.1 什么是內(nèi)存泄漏?
一個對象在引用計數(shù)變?yōu)?時,系統(tǒng)會回收內(nèi)存。如果一個本應(yīng)該被回收的內(nèi)存,沒有被回收(引用計數(shù)>0),那么就會造成內(nèi)存泄漏。
以下代碼將造成內(nèi)存泄漏:
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
// 該ViewController將不會釋放
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
1.2 分析如下:
NSTimer的scheduledTimerWith
TimeInterval方法會傳進(jìn)去一個target,NSTimer內(nèi)部實現(xiàn)會有一個對象強引用傳入的對象例如(偽代碼如下,示意圖如下):
// 偽代碼@interface NSTimer ()@property (strong, nonatomic) id target;@end // 強引用該對象self.target = target
ViewController和NSTimer互相引用,此刻ViewController的引用計數(shù)為2
當(dāng)一個對象的引用計數(shù)變?yōu)?時,系統(tǒng)將回收這塊內(nèi)存。
假設(shè)對象A在某一時刻需要從內(nèi)存中釋放,那么理應(yīng)他引用的ViewController也應(yīng)該釋放,但是由于ViewController內(nèi)部的NSTimer對其有個強引用,最終導(dǎo)致ViewController不能釋放,從而導(dǎo)致內(nèi)存泄漏。如圖所示:
對象A釋放,ViewController的引用計數(shù)變?yōu)?,原本應(yīng)該引用計數(shù)變?yōu)?,從而ViewController內(nèi)存泄漏
1.3 如何解決?
按照分析,那應(yīng)該打破ViewController和NSTimer雙方的強引用。使用弱引用(弱引用不增加對象的引用計數(shù))。
方案1
使用系統(tǒng)代碼Block塊的方法破除循環(huán)引用
- (void)viewDidLoad { [super viewDidLoad]; __weak typedef(self) weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { [weakSelf timerTest]; }];}
NSTimer弱引用ViewController,在ViewController釋放時,NSTimer也獲得釋放,循環(huán)鏈條斷開
方案2
使用中間代理層來解決循環(huán)引用
// 代理類@interface Proxy : NSObject+ (instancetype)proxyWithTarget:(id)target;// 弱引用target@property (weak, nonatomic) id target;@end@implementation Proxy+ (instancetype)proxyWithTarget:(id)target { Proxy *proxy = [[MJProxy1 alloc] init]; proxy.target = target; return proxy;}- (id)forwardingTargetForSelector:(SEL)aSelector { return self.target;}@end@interface ViewController ()@property (nonatomic, strong) NSTimer *timer;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[Proxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];}- (void)timerTest{ NSLog(@"%s", __func__);}// 該ViewController將不會釋放- (void)dealloc{ NSLog(@"%s", __func__); [self.timer invalidate];}
如下圖所示,ViewController需要強引用NSTimer,NSTimer內(nèi)部需要強引用一個target對象,所以可以創(chuàng)建一個代理類來處理這個問題,所以proxy內(nèi)部有一個弱引用的target對象,ViewController調(diào)用proxyWithTarget把self傳入時不會強持有self。
三方之間沒有循環(huán)引用,最終可以釋放對象
02 淺析Android的焦點機制
焦點是一個很寬泛的概念,中文釋義是比喻問題的關(guān)鍵所在或爭論的集中點,在物理學(xué)、數(shù)學(xué)、生活中都有廣泛的使用。那么Android中的焦點是什么呢?
2.1 Android焦點概念
焦點在Android中也就是Focus,稱為Focus機制。focus在英文中的釋義是:
"the main or central point of something, especially of attention or interest",和中文語義相同。
回到我們Android開發(fā)中,我們手機屏幕可以同時顯示多種多樣的內(nèi)容,那么你的焦點或者說你的注意力在哪個內(nèi)容上?系統(tǒng)又該如何判斷呢?舉個例子,當(dāng)屏幕界面中同時存在多個EditText(輸入框)時,你的鍵盤輸入會顯示在哪個輸入框內(nèi)呢?亦或是同時顯示在所有輸入框中?這顯然是不合理的,而這時焦點機制就體現(xiàn)了它的意義。對于EditText控件來說,獲取到焦點,則意味著激活了和用戶的交互,鍵盤輸入的內(nèi)容會輸入到這個EditText上面。
2.2 焦點處理
焦點的處理包含獲取焦點、分發(fā)焦點、清除焦點等。
2.2.1 獲取焦點
讓一個View獲取焦點直接調(diào)用View#requestFocus方法,最終會調(diào)用到View#requestFocusNoSearch方法,其通過多個條件判斷該View是否允許獲取焦點,包括是否可見、是否可獲取焦點、是否可用,以及在觸屏設(shè)備中是否允許獲取焦點等。
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if (!canTakeFocus()) {
return false;
}
// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
if (!isLayoutValid()) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
} else {
clearParentsWantFocus();
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
2.2.2 獲取焦點的模式
獲取焦點有兩種模式,分別是:
普通模式(focusable):允許有普通獲取焦點的能力(比如物理鍵、電視、手表等非觸摸的輸入方式)
觸摸模式(focusableInTouchMode):允許有觸摸獲取焦點的能力。
需要注意的是,在設(shè)置允許觸摸模式時會默認(rèn)開啟普通模式,注意同時設(shè)置這兩個屬性時不要沖突。
并且由此我們可以得到一條關(guān)于焦點的特性:
并不是所有View都可以獲取焦點。獲取焦點的前提是視圖必需要有獲取焦點的資格。
2.2.3 分發(fā)焦點
上述View在獲取焦點時,需要逐級通知它的父View進(jìn)行焦點處理,清除舊焦點信息并保存新焦點信息,參見ViewGroup#requestChildFocus。
通過ViewGroup中mFocused(View類型)這個成員來保存具有焦點的子View,并且一直遞歸下去,為父View判斷是否包含焦點(hasFocus)和查找焦點(findFocus)提供了便利。
舉例:某個根View A包含B、C兩個子View,C下又包含C1、C2兩個子View,且C2具有焦點,則C中mFocused保存的是C2,根View A中mFocused保存的則是C。
另外ViewGroup也可以獲取焦點,參見ViewGroup#requestFocus,與View獲取焦點邏輯不同,ViewGroup獲取焦點受策略控制,如下:
FOCUS_BLOCK_DESCENDANTS:This view will block any of its descendants from getting focus, even if they are focusable.
FOCUS_BEFORE_DESCENDANTS:This view will get focus before any of its descendants.
FOCUS_AFTER_DESCENDANTS:This view will get focus only if none of its descendants want it.
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
// ...省略
int descendantFocusability = getDescendantFocusability();
boolean result;
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
result = super.requestFocus(direction, previouslyFocusedRect);
break;
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
result = took ? took : onRequestFocusInDescendants(direction,
previouslyFocusedRect);
break;
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
break;
}
default:
// ...省略
}
if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
}
return result;
}
由此我們也能得到另一些關(guān)于焦點的特性:
一個窗口內(nèi)最多只有一個View具有焦點,或者無焦點。上述在遞歸分發(fā)焦點時,當(dāng)有View獲取焦點后則會退出遞歸。
根View沒有焦點不能說明子View一定沒有焦點。子View具有焦點,根View能夠感知。
2.2.4 清除焦點
需要我們主動清除焦點的場景其實較少,我們可以通過clearFocus來清除焦點,View和ViewGroup的清除邏輯有細(xì)微差異,ViewGroup會同時清除上訴分發(fā)焦點過程中所記錄的狀態(tài)(需區(qū)分當(dāng)前焦點是自己還是子View),最終都會調(diào)用View#clearFocusInternal進(jìn)行真正的清除操作,后面會繼續(xù)提到焦點清除的問題。
/**
* Clears focus from the view, optionally propagating the change up through
* the parent hierarchy and requesting that the root view place new focus.
*
* @param propagate whether to propagate the change up through the parent
* hierarchy
* @param refocus when propagate is true, specifies whether to request the
* root view place new focus
*/
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
clearParentsWantFocus();
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
}
onFocusChanged(false, 0, null);
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
notifyGlobalFocusCleared(this);
}
}
}
問題1:錯誤啟用獲取焦點能力導(dǎo)致點擊失效
以EditText為例,我們在點擊時即會獲取焦點,輸入框中會顯示光標(biāo),彈出輸入法等。但像Button、TextView等控件,默認(rèn)觸摸不會獲取焦點,如果對此類控件設(shè)置了focusableInTouchMode=true,就會發(fā)現(xiàn)第一次觸摸無法響應(yīng)點擊事件,第二次點擊才會響應(yīng),這是為什么呢?從事件分發(fā)機制中尋找線索,看View#onTouchEvent中對MotionEvent.ACTION_UP的處理,可以清晰看到UP事件的處理會優(yōu)先處理焦點獲取,只有在無焦點變化時才會如我們所想的開始分發(fā)點擊事件。所以我們在第一次點擊時收到的是onFocusChange事件,第二次點擊收到的才是onClick事件。
public boolean onTouchEvent(MotionEvent event) {
// ...省略
switch (action) {
case MotionEvent.ACTION_UP:
// ...省略
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
// ...省略
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
}
問題2:clearFocus“無效”?
在之前我們了解了清除焦點的機制,但為什么有時候會碰到調(diào)用clearFocus時"無效"呢?我們對比一下我們可以主動調(diào)用的clearFocus方法和系統(tǒng)內(nèi)部調(diào)用的unFocus方法。
void unFocus(View focused) {
clearFocusInternal(focused, false, false);
}
發(fā)現(xiàn)一處可疑點,propagate和refocus的值決定了rootViewRequestFocus是否被調(diào)用,由于&&和||的短路作用,當(dāng)propagate和refocus均為true時,才會執(zhí)行rootViewRequestFocus,而在rootViewRequestFocus中會觸發(fā)root的獲取焦點邏輯。
boolean rootViewRequestFocus() {
final View root = getRootView();
return root != null && root.requestFocus();
}
因此clearFocus看似“無效”,其實是焦點被清除后又立馬被設(shè)置上了。那該如何解決呢?回顧之前提到的焦點分發(fā)邏輯,當(dāng)父View搶先獲取了焦點就能夠解決,因此,讓父view自動獲取焦點是很好的解決方法。這里我們可以回憶上面分發(fā)焦點中所提及的三種焦點分發(fā)策略,我們希望父View先于子View獲取焦點,很明顯這符合FOCUS_BEFORE_DESCENDANTS策略,但我們好像并沒有手動配置過這個策略,那FOCUS_BEFORE_DESCENDANTS策略是否是ViewGroup的默認(rèn)策略呢?我們查看ViewGroup源碼發(fā)現(xiàn)在initViewGroup中確實有默認(rèn)的設(shè)置,如下:
private void initViewGroup() {
// ...省略
setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
// ...省略
}
舉一反三,如果我們想讓子View先于父View獲取焦點或者禁止子View獲取焦點,即可通過setDescendantFocusability方法來設(shè)置。
另外感興趣的同學(xué)可以繼續(xù)探究refocus的取值邏輯。
問題3:焦點搶占
在問題2中,我們通過焦點搶占解決了一些問題,但有時候View錯誤的獲取焦點會帶來一些意料外的問題。比如EditText自動獲取了焦點導(dǎo)致自動彈起輸入法。又比如RecyclerView在嵌套時子View搶占了焦點導(dǎo)致列表發(fā)生預(yù)期外的移動等等,這是個有趣的問題,感興趣的同學(xué)可以查看RecyclerView#requestChildFocus方法,其中執(zhí)行的requestChildRectangleOnScreen方法會為你解決這個疑惑。碰到這些問題時,我們可以考慮禁止不需要獲取焦點的View的焦點獲取能力,或者讓其父View先獲取焦點來解決問題。
2.3 總結(jié)
Android中的焦點機制是一個很有趣的內(nèi)容,很多疑難問題的答案都藏在源碼中,理解了焦點的機制后,相關(guān)問題都將變得有跡可循。
03 Android中Cookie
3.1 首先什么是Cookie:
Cookie是服務(wù)器保存在瀏覽器的一小段文本信息,每個 Cookie 的大小一般不能超過4KB。瀏覽器每次向服務(wù)器發(fā)出請求,就會自動附上這段信息。
3.2 Webview的Cookie存儲:
WebView是基于 webkit 內(nèi)核的UI控件,相當(dāng)于一個瀏覽器客戶端。
它會在本地維護(hù)每次會話的cookie( 保存在 data/data/package_name/app_WebView/Cookies.db )
導(dǎo)出后可見:
3.3 Cookie屬性:
Set-Cookie:name=value [ ;expires=date][ ;max-age=time][ ;domain=domain][ ;path=path][ ;secure][ ;httponly]
例:
Set-Cookie: TEST=1234567890; Expires=Wed, 21 Oct 2022 07:28:00 GMT; Domain=baidu.com; Path=/test;Secure; HttpOnly
2.4 Cookie的設(shè)置
Android中的WebKit為我們提供了CookieManager,它是一個單例,我們可以利用它進(jìn)行Cookie的讀取和存儲,例如
CookieManager.getInstance().setCookie(url, cookie); CookieManager.getInstance().getCookie(url);
2.5 Cookie在請求中攜帶:
2.5.1 Request的Header:
WebView中H5的請求:
在WebView的H5中發(fā)送請求時,同瀏覽器一樣,每次向服務(wù)器發(fā)出請求(domain&path與cookie中設(shè)置一致),就會自動附上這段信息。
客戶端Native發(fā)請求:
由客戶端發(fā)送,包含在HTTP請求的頭部中。注意,Native發(fā)送請求時,需要網(wǎng)絡(luò)庫主動addHeader,所以建議封裝網(wǎng)絡(luò)庫時,Native仿照瀏覽器自動攜帶Cookie的機制。如:
// 簡單寫了個意思,具體實現(xiàn)需要遍歷拼接等判斷,大家明白就好CookieManager cookieManager = CookieManager.getInstance();String webviewCookies = cookieManager.getCookie(url);httpURLConnection.setRequestProperty("Cookie", webviewCookies);
2.5.2 Response的Set-Header:
WebView中H5的請求響應(yīng):
在WebView的H5中接收到服務(wù)端響應(yīng)時,同瀏覽器一樣,會響應(yīng)response的set-header自動為內(nèi)核種上cookie。
客戶端Native請求響應(yīng):
由客戶端接收到response后,需要注意的是系統(tǒng)并不會自動為內(nèi)核種上cookie,建議封裝網(wǎng)絡(luò)庫時,Native仿照瀏覽器響應(yīng)response的set-header自動為內(nèi)核種上Cookie。如:
// 簡單寫了個意思,具體實現(xiàn)需要添加安全性的判斷,大家明白就好
Map> responseHeaderMap = httpURLConnection.getHeaderFields();
List cookieList = responseHeaderMap.get("Set-Cookie");
CookieSyncManager.createInstance(context);
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
for (String cookie : cookieList) {
List httpCookieList = HttpCookie.parse(cookie);
HttpCookie httpCookie = httpCookieList.get(0);
String relCookie = buildCookie(httpCookie.getDomain(), httpCookie.getName(),
httpCookie.getValue(), System.currentTimeMillis() + httpCookie.getMaxAge() * 1000,
httpCookie.getSecure());
cookieManager.setCookie(domain, relCookie);
}
其他額外知識:
Cookie多進(jìn)程使用及同步:https://iluhcm.com/2018/04/27/android-cookie-sync-between-multiprocess/
本文轉(zhuǎn)自 百度Geek說