代碼榮辱觀-以運用風格為榮,以隨意編碼為恥

編寫代碼的八榮八恥

1. 產品命名:以簡單有趣為榮,以平庸難記為恥。

2. 單個函數:以短小精悍為榮,以冗長費神為恥。

3. 代碼維護:以持續重構為榮,以停滯不前為恥。

4. 編程風格:以運用風格為榮,以隨意編碼為恥。

5. 程序設計:以開關上線為榮,以自信編碼為恥。

6. 接口定義:以用戶易用為榮,以複雜歧義為恥。

7. 斷言分支:以實時報警為榮,以忽略分支為恥。

8. 監控報警:以定時調整為榮,以放棄維護為恥。

5Why分析

(一)

Q: 誰需要學習編寫代碼的八榮八恥?

A: 項目中的開發人員、項目經理、架構師

(二)

Q: 為什麼學習編寫代碼的八榮八恥?

A: 可以作為實際代碼編寫和review(複查)的指導規範

(三)

Q: 什麼人什麼時候需要review代碼?

A: 

對開發人員來說,需要在時間允許的條件下定期的review自己和別人的代碼,加深對項目的整體理解。對自己的成長做總結。如果過了一段時間,還看到自己之前的代碼,覺得寫的很好的話,就需要質疑自己的成長,更努力的學習了。

對於項目經理和架構師來說,鼓勵所有上線的功能都每周抽出時間來做個組內review。或者定期抽取一些模塊做review。鼓勵大家重構代碼。在review過程中,作為領導者需要對大家有輸出,對代碼怎麼寫是更好的有一些理論基礎。這時候就需要使用編寫代碼的八榮八恥作為review的指導規範。

(四)

Q: 怎麼用作review的指導規範?

A: 八榮八恥中不但介紹了每個條目的意義,而且有通俗易懂的代碼實例便於和實際中的代碼在頭腦中做對比。文中明確的指出了哪些寫法是鼓勵的、哪些是不鼓勵的,是基於什麼理由不鼓勵這樣做。

(五)

Q: 編寫代碼的八榮八恥對於高可用有什麼意義?

A: 我利用美團的內部運維平台對自己參與過的項目可用性做過統計。將影響可用性的case(具體事件)分成:開發因素和設計因素。開發因素包括系統bug、開發不規範、上線不規範、監控報警不及時(影響可用性的恢復時長)等由於具體開發者在設計階段覆蓋不到的階段發生的。設計因素包括機器故障、網絡中斷、異常流量、中間件故障等可以通過設計做容災的。結果95%以上的可用性問題都是開發因素造成的。編寫代碼的八榮八恥是對避免開發因素產生可用性問題的指導規範。

 

編程風格:以運用風格為榮,以隨意編碼為恥

引子

在工作中,經常發現有些程序員用面向對象的語言寫出了面向過程的代碼而自己並沒有感覺到:

前面提到有個java軟件工程師,叫Margaret。她對工作有三個要求:錢多、有趣、離家近。HR想針對這些要求和她具體溝通,問她最低標準是什麼。每一項最低要求回復一個星級。

 

星級

錢多

有趣

離家近

1

年薪10萬

出差+旅遊占工時1%

40公里

☆☆

2

年薪20萬

出差+旅遊占工時10%

20公里

☆☆☆

3

年薪50萬

出差+旅遊占工時20%

10公里

☆☆☆☆

4

年薪100萬

出差+旅遊占工時50%

2公里

☆☆☆☆☆

5

年薪500萬

出差+旅遊占工時80%

1公里

Margaret在外地,所以用了一個常用的數據交換格式json給HR回復如下:

{"moreMoney”:4,"moreFun”:2,"closerToHome”:3}

拿到這個回復時面向過程的解析方式是這樣寫的:

Map json = (HashMap) JSONUtils.parse("{\"moreMoney\":4,\"moreFun\":2,\"closerToHome\":3}");
int moreMoney = (int)json.get("moreMoney");int moreFun = (int)json.get("moreFun");int closerToHome = (int)json.get("closerToHome");

接收方將接收到的數據轉成了json,代碼里一堆get完成了功能。為什麼說這是面向過程的呢?map是一種數據結構,沒有直接的業務意義。功能實現了,表達的意義卻不清晰。

這段代碼更好的一個實現方式是將接收的數據結構定義成一個對象,在java里可以使用jackson等工具直接將json轉成有業務含義的對象。

ObjectMapper objectMapper = new ObjectMapper();
Requirement requirement = objectMapper.readValue("{\"moreMoney\":4,\"moreFun\":2,\"closerToHome\":3}",JavaSoftwareEngineerMargaretRequirement.class);

這樣做,HR拿到的requirement不是一列列数字,需要自己對核對每一項都是什麼意思。而是一個有完整語義的對象,利於理解。而以這種思路來進行編寫的代碼我經常稱他們叫面向對象風格的代碼。

WHY

來看一段寫赤壁山旅行的文章:

今天有幸登上赤壁山,看到這山上的景物,不禁想起了當前戰場上廝殺的場面。想起當年要不是周瑜運氣好,大冬天颳起了東風,恐怕吳國就被曹操滅了。

再來看唐代詩人杜牧經過赤壁山這個著名的古戰場,有感於三國時代的英雄成敗而寫下的《赤壁》:

折戟沉沙鐵未銷,自將磨洗認前朝。

東風不與周郎便,銅雀春深鎖二喬。

前兩句意思是在沙子底下找到一隻斷戟,磨洗之後發現“made in 赤壁”。讀者讀了這兩句不禁會聯想起當前戰場上廝殺的場面吧。

后兩句意思是要不是周瑜運氣好,大冬天颳起了東風。那孫權的老婆大喬和周瑜的老婆小喬這兩位絕世美女都要被曹操這個色老頭關進銅雀台了。因為曹操久仰大小喬的美貌,提前為二人修築銅雀台,作為打敗吳國的戰利品。

杜牧隻字未提戰場和如果沒有東風的運氣,將會亡國的下場。但是讀者卻能心領神會,印象深刻。這是因為杜牧採用了以小見大的風格手法。

代碼與代碼的區別如同文章與文章的區別。能否讓讀者以更短的時間、更輕鬆的讀懂?代碼是給人整體感還是噁心感?這些都決定了代碼的可維護性。而它和系統可用性、穩定性的最直接關係在工作中非常常見:“爺爺的!這是誰寫的代碼這麼爛?忍不了了,老子不幹了。”而這個代碼的作者之所以離職也是因為忍受不了自己的爛代碼。頻繁的人員更替,新接手人員要有學習的成本。成本就包括要踩坑來加深對系統的理解。

HOW

除了開頭提到的面向對象的風格,編寫java代碼時下面三種風格也很常見。

1.fluent風格

fluent風格的代碼常以Builder結尾。比如StringBuilder就是典型的fluent風格。定義一個人的對象,這個對象使用fluent風格代碼這麼寫:

public class Person {    private String name;    private int armCount=2;//胳膊數默認為2 private int legCount=2;//腿數默認為2
    public static Person builder() {        return new Person.Builder(); }
    public Person setName(String name) {        this.name = name;        return this; }
    public Person armCount(int armCount) {        this.armCount = armCount;        return this; }
    public void legCount(int legCount) {        this.legCount = legCount;    }}

如上,就是每次給對象賦屬性的時候同時返回對象本身。這樣調用的時候:

Person.builder().name("Jane").armCount(2).legCount(2);

這樣寫的好處是比每個屬性都用一句set簡潔。在屬性多的時候,用構造函數。調用時容易表達不清楚屬性的含義。方法名起到了解釋的作用。現在流行的做法是代碼即註釋,註釋不用在每個方法都寫。這時候能表達自身意義的代碼就更加重要。注意:我們也可以保留setXXX、getXXX的命名規範,因為jackson等序列化反序列化的組件會根據set、get方法對參數賦值,上面的明明風格在序列化時會有問題。

當然,這個類也可以直接用lombok註解得到。

@Data@Buildrpublic class Person {    private String name;    private int armCount=2;//胳膊數默認為2    private int legCount=2;//腿數默認為2}

2.lambda函數式編程風格

lambda函數式編程比傳統的命令式編程更加簡潔。比如:現在有一群人。

List<Person> personList = Lists.newArrayList();
personList.add(Person.builder().name("Jane"));personList.add(Person.builder().name("Joe").armCount(1));personList.add(Person.builder().name("Stark").legCount(1));

要找出所有的殘疾人:

List<Person> disabledPersonList = Lists.newArrayList();for(Person person : personList) {    if(person.legCount()!=2 || person.armCount()!=2) {        disabledPersonList.add(person);    }}

或者使用lambda函數式編程:

List<Person> disabledPersonList = personList.stream().filter(person -> person.legCount()!=2 || person.armCount()!=2).collect(Collectors.toList());

3.設計模式風格

1995年,GoF(Gang of Four,四人幫)合作出版了《設計模式:可復用面向對象軟件的基礎》一書,共收錄了23中設計模式,人稱“GoF設計模式”。這23種設計模式的本質是面向對象設計原則的實際運用,是一種最佳實踐。

在各種java源碼中,經常看到以設計模式命名的類名和方法名。在我們日常編碼中,設計模式也非常實用。設計模式風格的例子請參考:平時代碼中用不到設計模式?Are you kidding me?

總結

寫有技術追求的代碼

 

相關閱讀

編寫代碼的「八榮八恥」- 以用戶易用為榮,以複雜歧義為恥

編寫代碼的「八榮八恥」- 以開關上線為榮,以自信編碼為恥

編寫代碼的「八榮八恥」(上篇)

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Python 爬蟲從入門到進階之路(四)

之前的文章我們做了一個簡單的例子爬取了百度首頁的 html,我們用到的是 urlopen 來打開請求,它是一個特殊的opener(也就是模塊幫我們構建好的)。但是基本的 urlopen() 方法不支持代理、cookie等其他的HTTP/HTTPS高級功能,所以我們需要用到 Python 的 opener 來自定義我們的請求內容。

具體步驟:

  1. 使用相關的 Handler處理器 來創建特定功能的處理器對象;
  2. 然後通過 build_opener()方法使用這些處理器對象,創建自定義opener對象;
  3. 使用自定義的opener對象,調用open()方法發送請求。

我們先來回顧一下使用 urlopen 獲取百度首頁的 html 代碼實例:

 1 # 導入urllib 庫
 2 import urllib.request
 3 
 4 # url 作為Request()方法的參數,構造並返回一個Request對象
 5 request = urllib.request.Request("http://www.baidu.com")
 6 # Request對象作為urlopen()方法的參數,發送給服務器並接收響應
 7 response = urllib.request.urlopen(request)
 8 # 類文件對象支持 文件對象的操作方法,如read()方法讀取文件全部內容,返回字符串
 9 html = response.read().decode("utf-8")
10 # 打印字符串
11 print(html)

接下來我們看一下使用 opener 的處理方式:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler()
 5 
 6 # 構建一個HTTPSHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler()
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

 

在上面的第一段代碼中,我們是通過直接  import urllib.request   來導入我們需要的包,這樣當我們要使用時需要   urllib.request   來使用,第二段代碼我們是通過  from urllib import request  來導入我們需要的包,這樣當我們使用時直接  request 來使用就可以了。

第一段代碼在前面的文章中我們已經說過了,這裏就不多做解釋了。

第二段代碼中,我們使用了 opener 的方法來處理我們的請求,這樣我們就可以對代理,cookie 等做進一步的操作,後續文章會講到。最終結果如下:

在  http_handler = request.HTTPHandler() 中,我們還可以添加一個  debuglevel=1 參數,會將 Debug Log 打開,這樣程序在執行的時候,會把收包和發包的報頭在屏幕上自動打印出來,方便調試,有時可以省去抓包的工作。

代碼如下:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler(debuglevel=1)
 5 
 6 # 構建一個HTTPHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler(debuglevel=1)
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

輸出結果如下:

可以看出在響應結果的時候會為我們打印輸出一些請求信息。

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

C#規範整理·異常與自定義異常

這裡會列舉在C#中處理CLR異常方面的規範,幫助大家構建和開發一個運行良好和可靠的應用系統。

前言

  迄今為止,CLR異常機制讓人關注最多的一點就是“效率”問題。其實,這裏存在認識上的誤區,因為正常控制流程下的代碼運行並不會出現問題,只有引發異常時才會帶來效率問題。基於這一點,很多開發者已經達成共識:不應將異常機制用於正常控制流中。達成的另一個共識是:CLR異常機制帶來的“效率”問題不足以“抵消”它帶來的巨大收益。
CLR異常機制至少有以下幾個優點:

  • 正常控制流會被立即中止,無效值或狀態不會在系統中繼續傳播。
  • 提供了統一處理錯誤的方法。
  • 提供了在構造函數、操作符重載及屬性中報告異常的便利機制。
  • 提供了異常堆棧,便於開發者定位異常發生的位置。

  另外,“異常”其名稱本身就說明了它的發生是一個小概率事件。所以,因異常帶來的效率問題會被限制在一個很小的範圍內。實際上,try catch所帶來的效率問題幾乎是可以忽略的。在某些特定的場合,如Int32的Parse方法中,確實存在着因為濫用而導致的效率問題。在這種情況下,我們就應該考慮提供一個TryParse方法,從設計的角度讓用戶選擇讓程序運行得更快。另一種規避因為異常而影響效率的方法是:Tester-doer模式

正文

1.用拋出異常代替返回錯誤代碼

在異常機制出現之前,應用程序普遍採用返回錯誤代碼的方式來通知調用者發生了異常。本建議首先闡述為什麼要用拋出異常的方式來代替返回錯誤代碼的方式。對於一個成員方法而言,它要麼執行成功,要麼執行失敗。成員方法執行成功的情況很容易理解,但是如果執行失敗了卻沒有那麼簡單,因為我們需要將導致執行失敗的原因通知調用者。拋出異常和返回錯誤代碼都是用來通知調用者的手段。

但是當我們想要告訴調用者更多細節的時候,就需要與調用者約定更多的錯誤代碼。於是我們很快就會發現,錯誤代碼飛速膨脹,直到看起來似乎無法維護,因為我們總在查找並確認錯誤代碼。
在沒有異常處理機制之前,我們只能返回錯誤代碼。但是,現在有了另一種選擇,即使用異常機制。如果使用異常機制,那麼最終的代碼看起來應該是下面這樣的:

static void Main(string[]args)
{    
try  
    {   
     SaveUser(user); 
    }    
catch(IOException)   
    {       
    //IO異常,通知當前用戶 
    }    
catch(UnauthorizedAccessException)
    {       
    //權限失敗,通知客戶端管理員  
    }    
catch(CommunicationException) 
    {        
   //網絡異常,通知發送E-mail給網絡管理員  
    }
}

private static void SaveUser(User user)
{   
  SaveToFile(user); 
  SaveToDataBase(user);
}

使用CLR異常機制后,我們會發現代碼變得更清晰、更易於理解了。至於效率問題,還可以重新審視“效率”的立足點:throw exception產生的那點效率損耗與等待網絡連接異常相比,簡直微不足道,而CLR異常機制帶來的好處卻是顯而易見的。

這裏需要稍加強調的是,在catch(CommunicationExcep-tion)這個代碼塊中,代碼所完成的功能是“通知發送”而不是“發送”本身,因為我們要確保在catch和finally中所執行的代碼是可以被執行的。換句話說,盡量不要在catch和finally中再讓代碼“出錯”,那會讓異常堆棧信息變得複雜和難以理解。

在本例的catch代碼塊中,不要真的編寫發送郵件的代碼,因為發送郵件這個行為可能會產生更多的異常,而“通知發送”這個行為穩定性更高(即不“出錯”)。

以上通過實際的案例闡述了拋出異常相比於返回錯誤代碼的優越性,以及在某些情況下錯誤代碼將無用武之地,如構造函數、操作符重載及屬性。語法特性決定了其不能具備任何返回值,於是異常機制被當做取代錯誤代碼的首要選擇。

2.不要在不恰當的場合下引發異常

程序員,尤其是類庫開發人員,要掌握的兩條首要原則是:
正常的業務流程不應使用異常來處理。
不要總是嘗試去捕獲異常或引發異常,而應該允許異常向調用堆棧往上傳播。
那麼,到底應該在怎樣的情況下引發異常呢?

第一類情況 如果運行代碼後會造成內存泄漏、資源不可用,或者應用程序狀態不可恢復,則應該引發異常。
在微軟提供的Console類中有很多類似這樣的代碼:

if((value<1)||(value>100))
{    
    throw new ArgumentOutOfRangeException("value",value, Environment.GetResourceString("ArgumentOutOfRange_CursorSize"));
}

或者:

if(value==null)
{    
  throw new ArgumentNullException("value");
}

在開頭首先提到的就是:對在可控範圍內的輸入和輸出不引發異常。沒錯,區別就在於“可控”這兩個字。所謂“可控”,可定義為:發生異常后,系統資源仍可用,或資源狀態可恢復。

第二類情況 在捕獲異常的時候,如果需要包裝一些更有用的信息,則引發異常。
這類異常的引發在UI層特別有用。系統引發的異常所帶的信息往往更傾向於技術性的描述;而在UI層,面對異常的很可能是最終用戶。如果需要將異常的信息呈現給最終用戶,更好的做法是先包裝異常,然後引發一個包含友好信息的新異常。

第三類情況 如果底層異常在高層操作的上下文中沒有意義,則可以考慮捕獲這些底層異常,並引發新的有意義的異常。
例如在下面的代碼中,如果拋出InvalidCastException,則沒有任何意義,甚至會造成誤解,所以更好的方式是拋出一個ArgumentException:

private void CaseSample(object o)
{  
  if(o==null)    
  {        
   throw new ArgumentNullException("o");   
  }
}   
 
User user=null;   
try  
{   
   user=(User)o; 
}    
catch(InvalidCastException)
{    
   throw new ArgumentException("輸入參數不是一個User","o"); 
}  

//do something}

需要重點介紹的正確引發異常的典型例子就是捕獲底層API錯誤代碼,並拋出。查看Console這個類,還會發現很多地方有類似的代碼:

int errorCode=Marshal.GetLastWin32Error();
if(errorCode==6)
{   
  throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ConsoleKeyAvailableOnFile"));
}

Console為我們封裝了調用Windows API返回的錯誤代碼,而讓代碼引發了一個新的異常。

很顯然,當需要調用Windows API或第三方API提供的接口時,如果對方的異常報告機制使用的是錯誤代碼,最好重新引發該接口提供的錯誤,因為你需要讓自己的團隊更好地理解這些錯誤。

3.重新引發異常時使用Inner Exception

當捕獲了某個異常,將其包裝或重新引發異常的時候,如果其中包含了Inner Exception,則有助於程序員分析內部信息,方便代碼調試。
以一個分佈式系統為例,在進行遠程通信的時候,可能會發生的情況有:
1)網卡被禁用或網線斷開,此時會拋出SocketException,消息為:“由於目標計算機積極拒絕,無法連接。”
2)網絡正常,但是要連接的目標機沒有端口沒有處在偵聽狀態,此時,會拋出SocketException,消息為:“由於連接方在一段時間后沒有正確答覆或連接的主機沒有反應,連接嘗試失敗。”
3)連接超時,此時需要通過代碼實現關閉連接,並拋出一個SocketException,消息為:“連接超過約定的時長。”
發生以上三種情況中的任何一種情況,在返回給最終用戶的時候,我們都需要將異常信息包裝成為“網絡連接失敗,請稍候再試”。

所以,一個分佈式系統的業務處理方法,看起來應該是這樣的:

try
{    
SaveUser5(user);
}
catch(SocketException err)
{  
  throw new CommucationFailureException("網絡連接失敗,請稍後再試",err);
}

但是,在提示這條消息的時候,我們可能需要將原始異常信息記錄到日誌里,以供開發者分析具體的原因(因為如果這種情況頻繁出現,這有可能是一個Bug)。那麼,在記錄日誌的時候,就非常有必要記錄導致此異常出現的內部異常或是堆棧信息。
上文代碼中的:就是將異常重新包裝成為一個CommucationFailureException,並將SocketException作為Inner Exception(即err)向上傳遞。

此外還有一個可以採用的技巧,如果不打算使用Inner Exception,但是仍然想要返回一些額外信息的話,可以使用Exception的Data屬性。如下所示:

try
{   
 SaveUser5(user);
}
catch(SocketException err)
{    
 err.Data.Add("SocketInfo","網絡連接失敗,請稍後再試");   
 throw err;
}

在上層進行捕獲的時候,可以通過鍵值來得到異常信息:

catch(SocketException err)
{   
 Console.WriteLine(err.Data["SocketInfo"].ToString());
}

4.避免在finally內撰寫無效代碼

你應該始終認為finally內的代碼會在方法return之前執行,哪怕return是在try塊中。
C#編譯器會清理那些它認為完全沒有意義的C#代碼。

private static int TestIntReturnInTry()
{   
  int i;    
  try    
  {        
    return i=1;  
  } 
  
finally   
 {        
    i=2;      
   Console.WriteLine("\t將int結果改為2,finally執行完畢");   
 }
}

5.避免嵌套異常

應該允許異常在調用堆棧中往上傳播,不要過多使用catch,然後再throw。過多使用catch會帶來兩個問題:

  • 代碼更多了。這看上去好像你根本不知道該怎麼處理異常,所以你總在不停地catch。
  • 隱藏了堆棧信息,使你不知道真正發生異常的地方。

嵌套異常會導致 調用堆棧被重置了。最糟糕的情況是:如果方法捕獲的是Exception。所以也就是說,如果這個方法中還存在另外的異常,在UI層將永遠不知道真正發生錯誤的地方。
除了第3點提到的需要包裝異常的情況外,無故地嵌套異常是我們要極力避免的。當然,如果真的需要捕獲這個異常來恢復一些狀態,然後重新拋出,代碼看起來應該是這樣的:

try{ 
  MethodTry();
}
catch(Exception)
{ 
   //工作代碼   
 throw;
}

或者:

try
{    
 MethodTry();
}
catch
{    
  //工作代碼 
   throw;
}

盡量避免像下面這樣引發異常:

catch(Exception err)
{   
 //工作代碼  
  throw err;
}

直接throw err而不是throw將會重置堆棧信息。

6.避免“吃掉”異常

嵌套異常是很危險的行為,一不小心就會將異常堆棧信息,也就是真正的Bug出處隱藏起來。但這還不是最嚴重的行為,最嚴重的就是“吃掉”異常,即捕獲,然後不向上層throw拋出。如果你不知道如何處理某個異常,那麼千萬不要“吃掉”異常,如果你一不小心“吃掉”了一個本該往上傳遞的異常,那麼,這裏可能誕生一個Bug,而且,解決它會很費周折。

避免“吃掉”異常,並不是說不應該“吃掉”異常,而是這裏面有個重要原則:該異常可被預見,並且通常情況它不能算是一個Bug。 比如有些場景存在你可以預見的但不重要的Exception,這個就不算一個bug。

7.為循環增加Tester-Doer模式而不是將try-catch置於循環內

如果需要在循環中引發異常,你需要特別注意,因為拋出異常是一個相當影響性能的過程。應該盡量在循環當中對異常發生的一些條件進行判斷,然後根據條件進行處理。

8.總是處理未捕獲的異常

處理未捕獲的異常是每個應用程序應具備的基本功能,C#在AppDomain提供了UnhandledException事件來接收未捕獲到的異常的通知。常見的應用如下:

static void Main(string[]args)
{    
  AppDomain.CurrentDomain.UnhandledException+=new  UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}

static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{    
  Exception error=(Exception)e.ExceptionObject;   
 Console.WriteLine("MyHandler caught:"+error.Message);
}

未捕獲的異常通常就是運行時期的Bug,我們可以在App-Domain.CurrentDomain.UnhandledException的註冊事件方法CurrentDomain_UnhandledException中,將未捕獲異常的信息記錄在日誌中。值得注意的是,UnhandledException提供的機制並不能阻止應用程序終止,也就是說,執行CurrentDomain_UnhandledException方法后,應用程序就會被終止。

9.正確捕獲多線程中的異常

多線程的異常處理需要採用特殊的方法。以下的處理方式會存在問題:

try{  
  Thread t=new Thread((ThreadStart)delegate    
{        
  throw new Exception("多線程異常");    
});   

 t.Start();
}

catch(Exception error)
{ 
   MessageBox.Show(error.Message+Environment.NewLine+error.StackTrace);
}

應用程序並不會在這裏捕獲線程t中的異常,而是會直接退出。從.NET 2.0開始,任何線程上未處理的異常,都會導致應用程序的退出(先會觸發AppDomain的UnhandledException)。上面代碼中的try-catch實際上捕獲的還是當前線程的異常,而t屬於新起的異常,所以,正確的做法應該是把 try-catch放在線程裏面

Thread t=new Thread((ThreadStart)delegate
{    
try   
 {      
  throw new Exception("多線程異常");   
 }    
catch(Exception error)   {  ....   });

t.Start();

10.慎用自定義異常

除非有充分的理由,否則一般不要創建自定義異常。如果要對某類程序出錯信息做特殊處理,那就自定義異常。需要自定義異常的理由如下:
1)方便調試。通過拋出一個自定義的異常類型實例,我們可以使捕獲代碼精確地知道所發生的事情,並以合適的方式進行恢復。
2)邏輯包裝。自定義異常可包裝多個其他異常,然後拋出一個業務異常。
3)方便調用者編碼。在編寫自己的類庫或者業務層代碼的時候,自定義異常可以讓調用方更方便處理業務異常邏輯。例如,保存數據失敗可以分成兩個異常“數據庫連接失敗”和“網絡異常”。
4)引入新異常類。這使程序員能夠根據異常類在代碼中採取不同的操作。

11.從System.Exception或其他常見的基本異常中派生異常

這個不說了,自定義異常一般是從System.Exception派生。。事實上,現在如果你在Visual Studio中輸入Exception,然後使用快捷鍵Tab,VS會自動創建一個自定義異常類。

12.應使用finally避免資源泄漏

前面已經提到過,除非發生讓應用程序中斷的異常,否則finally總是會先於return執行。finally的這個語言特性決定了資源釋放的最佳位置就是在finally塊中;另外,資源釋放會隨着調用堆棧由下往上執行(即由內到外釋放)。

13.避免在調用棧較低的位置記錄異常

即避免在內部深處處理記錄異常。最適合記錄異常和報告的是應用程序的最上層,這通常是UI層。
並不是所有的異常都要被記錄到日誌,一類情況是異常發生的場景需要被記錄,還有一類就是未被捕獲的異常。未被捕獲的異常通常被視為一個Bug,所以,對於它的記錄,應該被視為系統的一個重要組成部分。

如果異常在調用棧較低的位置被記錄或報告,並且又被包裝后拋出;然後在調用棧較高位置也捕獲記錄異常。這就會讓記錄重複出現。在調用棧較低的情況下,往往異常被捕獲了也不能被完整的處理。所以,綜合考慮,應用程序在設計初期,就應該為開發成員約定在何處記錄和報告異常。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

一次線上Redis類轉換異常排查引發的思考

之前同事反饋說線上遇到Redis反序列化異常問題,異常如下:

XxxClass1 cannot be cast to XxxClass2

已知信息如下:

  • 該異常不是必現的,偶爾才會出現;
  • 出現該異常后重啟應用或者過一會就好了;
  • 序列化協議使用了hessian。

因為偶爾出現,首先看了報異常那塊業務邏輯是不是有問題,看了一遍也發現什麼問題。看了下對應日誌,發現是在Redis讀超時之後才出現的該異常,因此懷疑redis client操作邏輯那塊導致的(公司架構組對redis做了一層封裝),發現獲取/釋放redis連接如下代碼:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedisPool.returnResourceObject(jedis);
10     }
11 }

初步認定原因為:發生了讀寫超時的連接,直接歸還給連接池,下次使用該連接時讀取到了上一次Redis返回的數據。因此本地驗證下,示例代碼如下:

 1 @Data
 2 @NoArgsConstructor
 3 @AllArgsConstructor
 4 static class Person implements Serializable {
 5     private String name;
 6     private int age;
 7 }
 8 @Data
 9 @NoArgsConstructor
10 @AllArgsConstructor
11 static class Dog implements Serializable {
12     private String name;
13 }
14 
15 public static void main(String[] args) throws Exception {
16     JedisPoolConfig config = new JedisPoolConfig();
17     config.setMaxTotal(1);
18     JedisPool jedisPool = new JedisPool(config, "192.168.193.133", 6379, 2000, "123456");
19 
20     Jedis jedis = jedisPool.getResource();
21     jedis.set("key1".getBytes(), serialize(new Person("luoxn28", 26)));
22     jedis.set("key2".getBytes(), serialize(new Dog("tom")));
23     jedisPool.returnResourceObject(jedis);
24 
25     try {
26         jedis = jedisPool.getResource();
27         Person person = deserialize(jedis.get("key1".getBytes()), Person.class);
28         System.out.println(person);
29     } catch (Exception e) {
30         // 發生了異常之後,未對該連接做任何處理
31         System.out.println(e.getMessage());
32     } finally {
33         if (jedis != null) {
34             jedisPool.returnResourceObject(jedis);
35         }
36     }
37 
38     try {
39         jedis = jedisPool.getResource();
40         Dog dog = deserialize(jedis.get("key2".getBytes()), Dog.class);
41         System.out.println(dog);
42     } catch (Exception e) {
43         System.out.println(e.getMessage());
44     } finally {
45         if (jedis != null) {
46             jedisPool.returnResourceObject(jedis);
47         }
48     }
49 }

連接超時時間設置2000ms,為了方便測試,可以在redis服務器上使用gdb命令斷住redis進程(如果redis部署在Linux系統上的話,還可以使用iptable命令在防火牆禁止某個回包),比如在執行 jedis.get("key1".getBytes() 代碼前,對redis進程使用gdb命令斷住,那麼就會導致讀取超時,然後就會觸發如下異常:

Person cannot be cast to Dog

既然已經知道了該問題原因並且本地復現了該問題,對應解決方案是,在發生異常時歸還給連接池時關閉該連接即可(jedis.close內部已經做了判斷),代碼如下:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedis.close();
10     }
11 }

至此,該問題解決。注意,因為使用了hessian序列化(其包含了類型信息,類似的有Java本身序列化機制),所有會報類轉換異常;如果使用了json序列化(其只包含對象屬性信息),反序列化時不會報異常,只不過因為不同類的屬性不同,會導致反序列化后的對象屬性為空或者屬性值混亂,使用時會導致問題,並且這種問題因為沒有報異常所以更不容易發現。

 

既然說到了Redis的連接,要知道的是,Redis基於RESP(Redis Serialization Protocol)協議來通信,並且通信方式是停等方式,也就說一次通信獨佔一個連接直到client讀取到返回結果之後才能釋放該連接讓其他線程使用。小夥伴們可以思考一下,Redis通信能否像dubbo那樣使用單連接+序列號(標識單次通信)通信方式呢?理論上是可以的,不過由於RESP協議中並沒有一個”序列號”的字段,所以直接靠原生的通信方法來實現是不現實的。不過我們可以通過echo命令傳遞並返回”序列號”+正常的讀寫方式來實現,這裏要保證二者執行的原子性,可以通過lua腳本或者事務來實現,事務方式如下:

MULTI
ECHO "唯一序列號"
GET key1
EXEC

然後客戶端收到的結果是一個 [ "唯一序列號", "value1" ]的列表,你可以根據前一項識別出這是你發送的哪個請求。

為什麼Redis通信方式並沒有採用類似於dubbo這種通信方式呢,個人認為有以下幾點:

  • 使用停等這種通信方式實現簡單,並且協議字段盡可能緊湊;
  • Redis都是內存操作,處理性能較強,停等協議不會造成客戶端等待時間較長;
  • 目前來看,通信方式這塊不是Redis使用上的性能瓶頸,這一點很重要。

 

推薦閱讀:

  • 別再問我ConcurrentHashMap了
  • 分佈式鎖設計與實現

  • ConcurrentHashMap竟然也有死循環問題?

  • 你的ThreadLocal線程安全么

 歡迎小夥伴掃描以下二維碼閱讀更多精彩好文。

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

高級Java工程師必備 —– 深入分析 Java IO (一)BIO

BIO編程

最原始BIO

網絡編程的基本模型是C/S模型,即兩個進程間的通信。

服務端提供IP和監聽端口,客戶端通過連接操作想服務端監聽的地址發起連接請求,通過三次握手連接,如果連接成功建立,雙方就可以通過套接字進行通信。

傳統的同步阻塞模型開發中,ServerSocket負責綁定IP地址,啟動監聽端口;Socket負責發起連接操作。連接成功后,雙方通過輸入和輸出流進行同步阻塞式通信。
最原始BIO通信模型圖:

存在的問題:

  • 同一時間,服務器只能接受來自於客戶端A的請求信息;雖然客戶端A和客戶端B的請求是同時進行的,但客戶端B發送的請求信息只能等到服務器接受完A的請求數據后,才能被接受。(acceptor只有在接受完client1的請求后才能接受client2的請求)
  • 由於服務器一次只能處理一個客戶端請求,當處理完成並返回后(或者異常時),才能進行第二次請求的處理。很顯然,這樣的處理方式在高併發的情況下,是不能採用的。

一請求一線程BIO

那有沒有方法改進呢? ,答案是有的。改進后BIO通信模型圖:

此種BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後為每個客戶端創建一個新的線程進行鏈路處理沒處理完成后,通過輸出流返回應答給客戶端,線程銷毀。即典型的一請求一應答通宵模型。

代碼演示

服務端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServer {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               SocketServerThread socketServerThread = new SocketServerThread(socket);
               new Thread(socketServerThread).start();
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServer.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThread implements Runnable {
   private Socket socket;
   public SocketServerThread (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

客戶端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.concurrent.CountDownLatch;

public class BioSocketClient{
   public static void main(String[] args) throws Exception {
       Integer clientNumber = 20;
       CountDownLatch countDownLatch = new CountDownLatch(clientNumber);

       // 分別開始啟動這20個客戶端,併發訪問
       for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) {
           ClientRequestThread client = new ClientRequestThread(countDownLatch, index);
           new Thread(client).start();
       }

       // 這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (BioSocketClient.class) {
           BioSocketClient.class.wait();
       }
   }
}



/**
* 一個ClientRequestThread線程模擬一個客戶端請求。
* @author keep_trying
*/
class ClientRequestThread implements Runnable {


   private CountDownLatch countDownLatch;

   /**
    * 這個線程的編號
    * @param countDownLatch
    */
   private Integer clientIndex;

   /**
    * countDownLatch是java提供的同步計數器。
    * 當計數器數值減為0時,所有受其影響而等待的線程將會被激活。這樣保證模擬併發請求的真實性
    * @param countDownLatch
    */
   public ClientRequestThread(CountDownLatch countDownLatch , Integer clientIndex) {
       this.countDownLatch = countDownLatch;
       this.clientIndex = clientIndex;
   }

   @Override
   public void run() {
       Socket socket = null;
       OutputStream clientRequest = null;
       InputStream clientResponse = null;

       try {
           socket = new Socket("localhost",8083);
           clientRequest = socket.getOutputStream();
           clientResponse = socket.getInputStream();

           //等待,直到SocketClientDaemon完成所有線程的啟動,然後所有線程一起發送請求
           this.countDownLatch.await();

           //發送請求信息
           clientRequest.write(("這是第" + this.clientIndex + " 個客戶端的請求。 over").getBytes());
           clientRequest.flush();

           //在這裏等待,直到服務器返回信息
          System.out.println("第" + this.clientIndex + "個客戶端的請求發送完成,等待服務器返回信息");
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           int realLen;
           String message = "";
           //程序執行到這裏,會一直等待服務器返回信息(注意,前提是in和out都不能close,如果close了就收不到服務器的反饋了)
           while((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) {
               message += new String(contextBytes , 0 , realLen);
           }
           //String messageEncode = new String(message , "UTF-8");
           message = URLDecoder.decode(message, "UTF-8");
           System.out.println("第" + this.clientIndex + "個客戶端接收到來自服務器的信息:" + message);
       } catch (Exception e) {

       } finally {
           try {
               if(clientRequest != null) {
                   clientRequest.close();
               }
               if(clientResponse != null) {
                   clientResponse.close();
               }
           } catch (IOException e) {

           }
       }
   }
}   

存在的問題:

  • 雖然在服務器端,請求的處理交給了一個獨立線程進行,但是操作系統通知accept()的方式還是單個的。也就是,實際上是服務器接收到數據報文後的“業務處理過程”可以多線程,但是數據報文的接受還是需要一個一個的來(acceptor只有在接受完client1的請求后才能接受client2的請求),下文會驗證。
  • 在linux系統中,可以創建的線程是有限的。我們可以通過cat /proc/sys/kernel/threads-max命令查看可以創建的最大線程數。當然這個值是可以更改的,但是線程越多,CPU切換所需的時間也就越長,用來處理真正業務的需求也就越少。
  • 另外,如果您的應用程序大量使用長連接的話,線程是不會關閉的。這樣系統資源的消耗更容易失控。

偽異步I/O編程

為了改進這種一連接一線程的模型,我們可以使用線程池來管理這些線程,實現1個或多個線程處理N個客戶端的模型(但是底層還是使用的同步阻塞I/O),通常被稱為“偽異步I/O模型“。

偽異步I/O模型圖:

代碼演示

只給出服務端,客戶端和上面相同

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServerThreadPool {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  
   //線程池 懶漢式的單例  
   private static ExecutorService executorService = Executors.newFixedThreadPool(60);  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               //當然業務處理過程可以交給一個線程(這裏可以使用線程池),並且線程的創建是很耗資源的。
               //最終改變不了.accept()只能一個一個接受socket的情況,並且被阻塞的情況
               SocketServerThreadPool socketServerThreadPool = new SocketServerThreadPool(socket);
               executorService.execute(socketServerThreadPool);
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServerThreadPool.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThreadPool implements Runnable {
   private Socket socket;
   public SocketServerThreadPool (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

服務器端的執行效果

在 Socket socket = serverSocket.accept(); 處打了斷點,有20個客戶端同時發出請求,可服務端還是一個一個的處理,其它線程都處於阻塞狀態

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

阻塞的問題根源

 那麼重點的問題並不是“是否使用了多線程、或是線程池”,而是為什麼accept()、read()方法會被阻塞。API文檔中對於 serverSocket.accept() 方法的使用描述:

Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.

服務器線程發起一個accept動作,詢問操作系統 是否有新的socket套接字信息從端口xx發送過來。

注意,是詢問操作系統。也就是說socket套接字的IO模式支持是基於操作系統的,那麼自然同步IO/異步IO的支持就是需要操作系統級別的了。如下圖:

 如果操作系統沒有發現有套接字從指定的端口xx來,那麼操作系統就會等待。這樣serverSocket.accept()方法就會一直等待。這就是為什麼accept()方法為什麼會阻塞:它內部的實現是使用的操作系統級別的同步IO。

  • 阻塞IO 和 非阻塞IO
    這兩個概念是程序級別的。主要描述的是程序請求操作系統IO操作后,如果IO資源沒有準備好,那麼程序該如何處理的問題:前者等待;後者繼續執行(並且使用線程一直輪詢,直到有IO資源準備好了)
  • 同步IO 和非同步IO
    這兩個概念是操作系統級別的。主要描述的是操作系統在收到程序請求IO操作后,如果IO資源沒有準備好,該如何處理相應程序的問題:前者不響應,直到IO資源準備好以後;後者返回一個標記(好讓程序和自己知道以後的數據往哪裡通知),當IO資源準備好以後,再用事件機制返回給程序。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

集成學習總結

1 基本概念

  • 集成學習的主要思路是先通過一定的規則生成多個學習器,再採用某種集成策略進行組合,最後綜合判斷輸出最終結果。一般而言,通常所說的集成學習中的多個學習器都是同質的”弱學習器”。基於該弱學習器,通過樣本集擾動、輸入特徵擾動、輸出表示擾動、算法參數擾動等方式生成多個學習器,進行集成后獲得一個精度較好的”強學習器”。
  • 目前集成學習算法大多源於bagging、boosting、stacking三種思想。

2 bagging

  • 一種提高分類模型的方法。
    • (1) 從訓練集\(S\)中有放回的隨機選取數據集\(M\)\((∣M∣ < ∣S∣)\);
    • (2) 生成一個分類模型\(C\);
    • (3) 重複以上步驟\(m\)次,得到\(m\)個分類模型\(C_1,C_2,…,C_m\);
    • (4)對於分類問題,每一個模型投票決定,少數服從多數原則;
    • (5)對於回歸問題,取平均值。
  • 注意:這種抽樣的方式會導致有的樣本取不到,大約有\(\lim_{n \to \infty}(1-\frac{1}{n})^n\) = \(36.8%\)的樣本取不到,這部分可用來做測試集。

  • 優點: 通過減少方差來提高預測結果。
  • 缺點: 失去了模型的簡單性

2.1 Random Forest

  • 是一種基於樹模型的bagging算法改進的模型。假定數據集中有\(M\)個特徵和 \(N\)個觀測值。每一個樹有放回的隨機抽出\(N\)個觀測值\(m\)(\(m=M\)或者\(m=logM\))個特徵。把每一個單一決策樹的結果綜合起來。

  • 優點:
    • (1) 減少了模型方差,提高了預測準確性。
    • (2) 不需要給樹做剪枝。
    • (3) 在大規模數據集,尤其是特徵較多的情況下,依然可以保持高效率。
    • (4) 不用做特徵選擇,並且可以給出特徵變量重要性的排序估計。
  • 缺點:
    • (1) 隨機森林已經被證明在某些噪音較大的分類或回歸問題上會過擬合
    • (2) 對於有不同取值的屬性的數據,取值劃分較多的屬性會對隨機森林產生更大的影響,所以隨機森林在這種數據上產出的屬性權值是不可信的。

3 boosting

  • 每一輪根據上一輪的分類結果動態調整每個樣本在分類器中的權重,訓練得到k個弱分類器,他們都有各自的權重,通過加權組合的方式得到最終的分類結果(綜合所有的基模型預測結果)。主要算法有AdaBoost/GBDT/Xgboost/LightGBM。

3.1 Adboost

  • 給定數據集\(S\),它包含\(n\)個元組\((X_1,y_1),(X_2,y_2),…,(X_n,y_n)(X_1,y_1), (X_2,y_2), …, (X_n,y_n)\),其中\(y_i\)是數據對象\(X_i\)的類標號。
  • (1) 開始時,Adaboost對每個訓練元組賦予相等的權重\(1/n\)。組合分類器包含\(T\)個基本分類器。
  • (2) 針對第\(t\)個分類器\(M_t\)
    • 首先,從S中的元組進行抽樣,形成大小為\(n\)的訓練集\(S_t\),此處抽樣方式為有放回的抽樣,抽樣過程中,每個元組被選中的機會由它的權重決定;
    • 然後,根據\(S_t\)導出(訓練出)分類器\(M_t\),使用\(S_t\)檢驗分類器\(M_t\)的分類誤差,並計算該分類器的“表決權”的權重;
    • 最後,訓練元組的權重根據分類器\(M_t\)的分類情況調整。
    • 如果元組被錯誤分類,則它的權重增加。
    • 如果元組被正確分類,則它的權重減少。
    • 元組的權重反映元組被分類的困難程度——權重越高,被錯誤分類的可能性越高。然後,使用這些權重,為下一輪分類器(下一個分類器)產生訓練樣本。
  • 其基本的思想是,當建立分類器時,希望它更關註上一輪分類器(上一個分類器)錯誤分類的元組。整個分類過程中,某些分類器對某些“困難”元組的分類效果可能比其他分類器好。這樣,建立了一個互補的分類器系列。
  • 用於二分類或多分類的應用場景。
  • 優點
    • (1) 很好的利用了弱分類器進行級聯。
    • (2)可以將不同的分類算法作為弱分類器。
    • (3)AdaBoost具有很高的精度。
    • (4) 相對於bagging算法和Random Forest算法,AdaBoost充分考慮的每個分類器的權重。
  • 缺點:
    • (1) AdaBoost迭代次數也就是弱分類器數目不太好設定,可以使用交叉驗證來進行確定。
    • (2) 數據不平衡導致分類精度下降。
    • (3) 訓練比較耗時,每次重新選擇當前分類器最好切分點。

3.2 GBDT

  • 採用決策樹作為弱分類器的Gradient Boosting算法被稱為GBDT,有時又被稱為MART(Multiple Additive Regression Tree)。GBDT中使用的決策樹通常為CART。
  • 用一個很簡單的例子來解釋一下GBDT訓練的過程,如圖下圖所示。模型的任務是預測一個人的年齡,訓練集只有A、B、C、D 4個人,他們的年齡分別是14、16、24、26,特徵包括了 “月購物金額”、”上網時長”、”上網歷史” 等。
  • 下面開始訓練第一棵樹:
    • 訓練的過程跟傳統決策樹相同,簡單起見,我們只進行一次分枝。訓練好第一棵樹后,求得每個樣本預測值與真實值之間的殘差。
    • 可以看到,A、B、C、D的殘差分別是−1、1、−1、1。
    • 這時我們就用每個樣本的殘差訓練下一棵樹,直到殘差收斂到某個閾值以下,或者樹的總數達到某個上限為止。
  • 由於GBDT是利用殘差訓練的,在預測的過程中,我們也需要把所有樹的預測值加起來,得到最終的預測結果。

  • 優點:
    • (1)預測階段的計算速度快,樹與樹之間可并行化計算。
    • (2)在分佈稠密的數據集上,泛化能力和表達能力都很好,這使得GBDT在Kaggle的眾多競賽中,經常名列榜首。
    • (3)採用決策樹作為弱分類器使得GBDT模型具有較好的解釋性和魯棒性,能夠自動發現特徵間的高階關係,並且也不需要對數據進行特殊的預處理如歸一化等。
  • 缺點:
    • (1)GBDT在高維稀疏的數據集上,表現不如支持向量機或者神經網絡。
    • (2)GBDT在處理文本分類特徵問題上,相對其他模型的優勢不如它在處理數值特徵時明顯。
    • (3)訓練過程需要串行訓練,只能在決策樹內部採用一些局部并行的手段提高訓練速度。

3.3 Xgboost

  • XGBoost是陳天奇等人開發的一個開源機器學習項目,高效地實現了GBDT算法並進行了算法和工程上的許多改進。
  • 目標函數:
    \[L^{(t)} = \sum_{i=1}^{n}l(y_i, \hat{y}_i^{(t)}) + \Omega(f_t) \]
  • 優點:
    • (1)計算效率高,使用了二階導。
    • (2)有正則化,減少過擬合。
    • (3)列特徵抽樣減少過擬合,同時有利於并行計算。
  • 缺點:
    • (1)每次迭代時都要遍歷整個數據集。
    • (2)內存佔用大。

3.4 GBDT與XGboost聯繫與區別

  • (1) GBDT是機器學習算法,XGBoost是該算法的工程實現。
  • (2) 在使用CART作為基分類器時,XGBoost顯式地加入了正則項來控制模型的複雜度,有利於防止過擬合,從而提高模型的泛化能力
  • (3) GBDT在模型訓練時只使用了代價函數的一階導數信息,XGBoost對代價函數進行二階泰勒展開,可以同時使用一階和二階導數。
  • (4) 傳統的GBDT採用CART作為基分類器,XGBoost支持多種類型的基分類器,比如線性分類器。
  • (5) 傳統的GBDT在每輪迭代時使用全部的數據,XGBoost則採用了與隨機森林相似的策略,支持對數據進行採樣
  • (6) 傳統的GBDT沒有設計對缺失值進行處理,XGBoost能夠自動學習出缺失值的處理策略

3.5 LightGBM

  • LightGBM也是一種基於決策樹的梯度提升算法,相比XGboost有做了許多改進。
    在樹分裂計算分裂特徵的增益時,xgboost 採用了預排序的方法來處理節點分裂,這樣計算的分裂點比較精確。但是,也造成了很大的時間開銷。為了解決這個問題,Lightgbm 選擇了基於 histogram 的決策樹算法。相比於pre-sorted算法,histogram在內存消耗和計算代價上都有不少優勢。
  • Histogram算法簡單來說,就是先對特徵值進行裝箱處理,形成一個一個的bins。在Lightgbm中默認的#bins為256(1個字節的能表示的長度,可以設置)。具體如下:
    • (1) 把連續的浮點特徵值離散化成N個整數,構造一個寬度為N的直方圖;對於分類特徵,則是每一種取值放入一個bin,且當取值的個數大於max_bin數時,會忽略那些很少出現的category值。
    • (2) 遍曆數據時,根據離散化后的值作為索引在直方圖中累積統計量。
    • (3) 一次遍歷后,直方圖累積了需要的統計量,然後根據直方圖的離散值,遍歷尋找最優的分割點。
  • Level-wise 和 Leaf-wise
    • 相對於xgboost的level—wise的生長策略,lightgbm使用了leaf-wise樹生長策略。由於level-wise在分裂時,部分增益小的樹也得到了增長,雖然容易控制誤差,但是分裂有時是不合理的,而lightgbm使用level-wise,只在增益大的樹上分裂生長,甚至對Feature f如果分裂無收益,那麼後續也將不會對f計算。體現在參數上,xgboost使用max_depth,而lightgbm使用num_leaves控制過擬合。
    • Level-wise過一次數據可以同時分裂同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,容易進行多線程優化,也好控制模型複雜度,不容易過擬合。但實際上Level-wise是一種低效的算法,因為它不加區分的對待同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,帶來了很多沒必要的開銷,因為實際上很多恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的分裂增益較低,沒必要進行搜索和分裂
    • Leaf-wise則是一種更為高效的策略,每次從當前所有恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子中,找到分裂增益最大的一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,然後分裂,如此循環。因此同Level-wise相比,在分裂次數相同的情況下,Leaf-wise可以降低更多的誤差,得到更好的精度。Leaf-wise的缺點是可能會長出比較深的決策樹,產生過擬合。因此LightGBM在Leaf-wise之上增加了一個最大深度的限制,在保證高效率的同時防止過擬合。

3.6 Xgboost與LightGBM對比

3.6.1 切分算法(切分點的選取)

  • 佔用的內存更低,只保存特徵離散化后的值,而這個值一般用8位整型存儲就足夠了,內存消耗可以降低為原來的1/8
  • 降低了計算的代價:預排序算法每遍歷一個特徵值就需要計算一次分裂的增益,而直方圖算法只需要計算k次(k可以認為是常數),時間複雜度從O(#data#feature)優化到O(k#features)。(相當於LightGBM犧牲了一部分切分的精確性來提高切分的效率,實際應用中效果還不錯)
  • 空間消耗大,需要保存數據的特徵值以及特徵排序的結果(比如排序后的索引,為了後續快速計算分割點),需要消耗兩倍於訓練數據的內存
  • 時間上也有較大開銷,遍歷每個分割點時都需要進行分裂增益的計算,消耗代價大
  • 對cache優化不友好,在預排序后,特徵對梯度的訪問是一種隨機訪問,並且不同的特徵訪問的順序不一樣,無法對cache進行優化。同時,在每一層長樹的時候,需要隨機訪問一個行索引到恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子索引的數組,並且不同特徵訪問的順序也不一樣,也會造成較大的cache miss。
  • XGBoost使用的是pre-sorted算法(對所有特徵都按照特徵的數值進行預排序,基本思想是對所有特徵都按照特徵的數值進行預排序;然後在遍歷分割點的時候用O(#data)的代價找到一個特徵上的最好分割點最後,找到一個特徵的分割點后,將數據分裂成左右子節點。優點是能夠更精確的找到數據分隔點;但這種做法有以下缺點
  • LightGBM使用的是histogram算法,基本思想是先把連續的浮點特徵值離散化成k個整數,同時構造一個寬度為k的直方圖。在遍曆數據的時候,根據離散化后的值作為索引在直方圖中累積統計量,當遍歷一次數據后,直方圖累積了需要的統計量,然後根據直方圖的離散值,遍歷尋找最優的分割點;優點在於

3.6.2 決策樹生長策略

  • XGBoost採用的是帶深度限制的level-wise生長策略,Level-wise過一次數據可以能夠同時分裂同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,容易進行多線程優化,不容易過擬合;但不加區分的對待同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,帶來了很多沒必要的開銷(因為實際上很多恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的分裂增益較低,沒必要進行搜索和分裂)
  • LightGBM採用leaf-wise生長策略,每次從當前所有恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子中找到分裂增益最大(一般也是數據量最大)的一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,然後分裂,如此循環;但會生長出比較深的決策樹,產生過擬合(因此 LightGBM 在leaf-wise之上增加了一個最大深度的限制,在保證高效率的同時防止過擬合)。
  • Histogram做差加速。一個容易觀察到的現象:一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的直方圖可以由它的父親節點的直方圖與它兄弟的直方圖做差得到。通常構造直方圖,需要遍歷該恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子上的所有數據,但直方圖做差僅需遍歷直方圖的k個桶。利用這個方法,LightGBM可以在構造一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的直方圖后,可以用非常微小的代價得到它兄弟恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的直方圖,在速度上可以提升一倍。
  • 直接支持類別特徵:LightGBM優化了對類別特徵的支持,可以直接輸入類別特徵,不需要額外的0/1展開。並在決策樹算法上增加了類別特徵的決策規則。

3.6.3 分佈式訓練方法上(并行優化)

  • 在特徵并行算法中,通過在本地保存全部數據避免對數據切分結果的通信;
  • 在數據并行中使用分散規約(Reducescatter)把直方圖合併的任務分攤到不同的機器,降低通信和計算,並利用直方圖做差,進一步減少了一半的通信量。基於投票的數據并行(ParallelVoting)則進一步優化數據并行中的通信代價,使通信代價變成常數級別。
  • 特徵并行的主要思想是在不同機器在不同的特徵集合上分別尋找最優的分割點,然後在機器間同步最優的分割點。
  • 數據并行則是讓不同的機器先在本地構造直方圖,然後進行全局的合併,最後在合併的直方圖上面尋找最優分割點。
  • Cache命中率優化
  • 基於直方圖的稀疏特徵優化
  • DART(Dropout + GBDT)
  • GOSS(Gradient-based One-Side Sampling):一種新的Bagging(row subsample)方法,前若干輪(1.0f /gbdtconfig->learning_rate)不Bagging;之後Bagging時, 採樣一定比例g(梯度)大的樣本。

4 stacking

  • stacking的思想是將每個基模型的輸出組合起來作為一個特徵向量,重新進行訓練。可以理解為:將訓練好的所有基模型對整個訓練集進行預測,第j個基模型對第i個訓練樣本的預測值將作為新的訓練集中第i個樣本的第j個特徵值,最後基於新的訓練集進行訓練。同理,預測的過程也要先經過所有基模型的預測形成新的測試集,最後再對測試集進行預測。
  • 具體算法如下(為方便理解我舉例說明)
    • (1) 訓練集大小\(400\times10\),400個樣本,每個樣本10個特徵,(如果算上target有11列)。測試集大小為\(120\times10\)
    • (2) 首先對訓練集4折劃分:\(S_1\),\(S_2\),\(S_3\),\(S_4\),每個\(S_i\)的大小都收是\(100\times10\)。模型\(M_1\)第一次用\(S_1\),\(S_2\),\(S_3\)訓練,用\(S_4\)預測得到預測結果\(100\times1\)。重複訓練步驟,直到每一個\(S_i\)都有對應的預測結果\(100\times1\)。合併所有預測結果得到\(P_1\)\(400\times1\)。用\(M_1\)預測得到原始測試集的預測結果\(T_1\)\(120\times1\)
    • (3) 模型\(M_2\)用4折叫交叉得到訓練集的預測結果:\(P_2\)\(400\times1\);得到測試集的預測結果:\(T_2\)\(120\times1\)
    • (4) 模型\(M_3\)用4折叫交叉得到訓練集的預測結果:\(P_3\)\(400\times1\);得到測試集的預測結果:\(T_3\)\(120\times1\)
    • (5) 綜合(2)(3)(4)底層模型得到的訓練集的預測結果\(P_1\)\(P_2\)\(P_3\),得到上層模型的訓練集\(P_{train}\)\(400\times3\);得到上層模型的測試集\(T_{test}\)\(120\times3\)
    • (6) 用(5)得到的訓練集和測試集進行上層模型的訓練。
  • 優點:學習了底層模型之間的關係
  • 缺點:對於數據量要求比較大,因為要平衡第一層和第二層

    5 參考

  • 《百面機器學習》
  • https://zhuanlan.zhihu.com/p/26890738
  • https://www.imooc.com/article/29530
  • https://blog.csdn.net/anshuai_aw1/article/details/83040541

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

【讀後感1】SQL2008技術內幕- SQL邏輯查詢處理

引言觀點

1. 編程語言日新月異,但是從沒有人否定sql 在現代編程中的巨大作用和 持續的可利用性。SQL以對人類友好的閱讀體驗提供數據查詢能力( 相比其他編程語言 ), 同時在各種數據庫平台中,基礎SQL元素是相同或大同小異的,

從我們最早接觸的SQL,Mysql到公司大數據impala 支持SQL, Es也提供類似SQL的查詢, 阿里提出SQLFlow AI框架, SQL的生命力極其頑強。

2. 在我近6年的開發生涯中,確實覺得SQL語言沒有得到開發者足夠的重視,尤其是流行的ORM概念使得了編寫SQL機會越來越少,使用ORM映射框架是需要一些代碼的, 另外ORM只能用於基礎的關係型二維查詢,對於複雜的查詢無能為力,部分工作可通過巧妙的SQL查詢,存儲過程,觸發器來完成。

3. SQL編程有許多獨特之處: 面向集合的思維方式、 查詢元素的邏輯處理順序、三值邏輯(three value logic),理解不透的話在實際編寫SQL時會產生很多錯誤的寫法、性能低下的代碼。

1987年SQL稱為ISO標準,ANSI宣布該語言發音為“ess kyoo ell”, 但由於歷史原因,很多專業人士還是將SQL發音成sequel,而且從英文習慣上,sequel發音更為流暢。 互聯網如此之大,容得下不同的聲音。

結合《SQL2008 技術內幕 T-SQL查詢》和工作經驗,提煉出Web開發者需要熟練掌握以下SQL查詢。

  • SQL邏輯查詢處理

  • SQL 面向集合的思維方式

不敢妄自宣稱是高級編程經驗, 只是認為Web開發者應該Cover這些常見SQL用法。  

SQL邏輯查詢處理

  開發者、數據分析師每天都在寫【SELECT 列a,聚合函數 FROM 表名 WHERE 過濾條件 GROUP BY 列a HAVING 篩選條件】這樣的查詢語句。

  SQL與其他語言不同的最明顯特徵是代碼的處理順序,大多數編程語言中,代碼是按照編寫順序來處理的,但在SQL中第一個要處理的子句是FROM子句,儘管SELECT語句第一個出現,但基本都在最後處理。

       每一步都會生成一個虛擬表,該虛擬表會作為下一步的輸入, 這些虛擬表對於調用者(客戶端應用程序或者外部查詢)都是不可用的,只有最後一步生成的虛擬表才會返回給調用者,這種形態可對比LINQ理解。

 

①FROM        FROM階段負責標識表或要查詢的表,如果指定了表運算符(JOIN, APPLY,PIVOT,UNPIVOT ),還要進行表運算符的處理。

              例如:表聯接運算中涉及的階段是 笛卡爾積、ON篩選器和 添加外部行,FROM階段生成虛擬表VT1.

②WHERE           這個階段根據在WHERE子句中出現的謂詞對VT1中進行篩選,只有讓謂詞計算結果為TRUE的行,才會插入VT2中。

③GROUP BY     按照GROUP BY 子句中指定的列名列表,對VT2中的行進行分組,生成VT3, 最終每個分組只有一個結果行。

④HAVING          根據HAVING子句中出現的謂詞,對VT3中行記錄進行篩選,只有讓謂詞結果為TRUE的行記錄,才會進入VT4, Having 篩選器是唯一可用於分組數據的篩選器。

⑤SELECT    處理SELECT子句中字段(某些字段可能進行一些操作,形成新的字段),形成虛擬表VT5

⑥ORDER BY  根據ORDER BY子句中指定的列名列表,對VT5 中行進行排序,輸出最後結果。

 

着重理解:

  • 第一步的FROM表運算, 一般情況下是TABLE、TempTable,CTE, 還有可能是表運算符(我們常用的是聯接運算符), 所以不能單純認為FROM後面是一個表結構。

  • 表聯接運算符  ON篩選器 與 WHERE有所不同,若採用OUTER JOIN, 應用ON篩選出來的結果不一定是此階段最終結果,因為涉及【添加外部行】, 而WHERE過濾出的結果是此階段的最終結果。 

  • GROUP BY x,y 意味着將(x,y)作為一個整體來分組

  • 有SELECT 和WHERE的時候,先執行WHERE,再執行SELECT,這樣就很容易理解以下SQL的業務含義:

SELECT page_original_url,server_session_id,access_order-1 as access_order FROM PageViewMeasure WHERE access_order >= 2
--- 查詢過濾出access_order>=2的基礎數據集,然後將(原列值-1)重命名為原列名,重命名的用法業務上也許是為了形成新的SQL聯接

SELECT keyword_id,Coalesce(full_keywords,keywords)  as  not_nullField,profile_id,session_server_time,count (*) over () as Count FROM pageview 
WHERE  profile_id =5254 and keyword_id != '-' and day =20181008  and  not_nullField !='-'   
ORDER BY session_server_time 
--- SQL報錯:Could not resolve column/field reference: 'not_nullfield' 也容易理解了:先執行where, 執行where的時候not_nullField字段還沒有形成
  • ROW_NUMBER() OVER(PARTITION BY UserId ORDER BY PageViewServerTime)排名函數中ORDER BY 與SQL語句最後的ORDER BY 同時存在,哪個ORDER BY起最終排序作用?

SELECT page_original_url as name,page_view_server_time, ROW_NUMBER() OVER(PARTITION BY page_original_url ORDER BY page_view_server_time ) as partition_rank ,wd3_page_duration
  FROM pageview WHERE profile_id=5198 AND day between 20190616 and 20190621   
  ORDER  BY wd3_page_duration desc 
  LIMIT 100

  可以認為 ROW_NUMBER() OVER(PARTITION BY col1 ORDER BY col2) as rank 本質上還是產生一個列值,實際是對應以上的第⑤步,因此SQL最後的ORDER BY起最終排序作用,例證如下:

           某些轉載文章寫有: 以上over函數里的分組及排序的執行晚於“where,group by,order by”的執行 ,這樣的結論是錯誤的

  • 若存在LIMIT子句,則LIMIT子句必須在ORDER BY 語法之後

 

      上圖來自《SQL技術內幕T-SQL查詢》邏輯查詢處理一章

 

 這裏拋出一個困惑點:

  在FROM子句中,若存在JOIN表運算符, 可能會按照 【計算笛卡爾積】 【應用ON篩選】【添加外部行】的順序來完成 JOIN的過程, 但是試想一下: 如果兩個表都為大表,先計算笛卡爾積,再篩選 豈不很費內存,

  我也搜索了很多資料,某些資料認為先進行【ON篩選】再進行【JOIN】運算:

https://www.cnblogs.com/liuzhendong/archive/2011/10/27/2226805.html

https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2008/ms189499(v=sql.100)

我更願意相信《SQL技術內幕T-SQL查詢》書中所言:

本章描述的某些邏輯處理步驟可能看起來非常低效,但要記住, 在實踐中,
查詢的實際物理處理可能與邏輯處理有很大不同

在SQL Server 中負責生成實際工作計劃的組件是查詢優化器,以何種順序訪問表、使用什麼訪問方法和索引,應用哪種聯接算法等都是查詢優化器來決定的,優化器會生成多個有效執行計劃並選擇一個開銷最低的計劃。

邏輯查詢處理中各個階段都有其特定的順序,而優化器缺經常可以在它生成的物理執行計劃中走捷徑。

 我們思考一個簡單的SQL:

SELECT * FROM pageview LEFT JOIN  share  ON  pageview.share_pv_id = share.page_view_id
 WHERE pageview.profile_id =5313 AND pageview.day  between 20190615 and 20190624

若實際物理查詢按照上面描述的 邏輯查詢處理, 先進行 FROM 子句中的 LEFT JOIN 計算,再進行 WHERE過濾, 根本無法查出(在FROM子句可能內存就爆滿了)

  現在我們能夠查詢出來,能夠印證 實際物理查詢確實與邏輯查詢處理有很大不同。 

PS: 以上是個人從現象上推斷書中理論,對於實際物理查詢處理並沒有理論支持,若網友們有相關資料,可留言給我。

 

作者: JulianHuang

感謝您的認真閱讀,如有問題請大膽斧正;覺得有用,請下方或加關注。

本文歡迎轉載,但請保留此段聲明,且在文章頁面明顯位置註明本文的作者及原文鏈接。

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

SpringBoot第十七篇:定時任務

作者:追夢1819
原文:https://www.cnblogs.com/yanfei1819/p/11076555.html
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!

引言

  相信大家對定時任務很熟悉,其重要性也不言而喻。定時發短信、定時批量操作、定時統計數據等,都離不開定時任務。本文將講解定時任務在 SpringBoot 項目中的應用。

版本信息

  • JDK:1.8
  • SpringBoot :2.0.1.RELEASE
  • maven:3.3.9
  • IDEA:2019.1.1
  • quartz:2.3.0

定時任務實現方式

JDK自帶的Timer

  Timer 是Java 自帶的定時任務類。可以用作比較簡單的定時任務。通常用的不多。下面以一個小的示例展示其用法。

SpringBoot集成的schedule

這種方式是 SpringBoot 集成的,使用很簡單。

首先,引入 SpringBoot 的基礎 jar:

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

然後再啟動類中添加註解 @EnableScheduling 即可開啟 SpringBoot 定時任務:

@SpringBootApplication
@EnableScheduling
public class TimedTaskDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(TimedTaskDemoApplication.class, args);
    }
}

下面根據 @Scheduled 的不同屬性創建幾個任務:

任務一:

@Component
public class FirstTask {
    /**
     * cron 表達式
     */
//    @Scheduled(cron = "0/2 * * * * *")
    @Scheduled(cron="${cron.schedule}")
    public void run(){
        System.out.println("這是創建的第一個定時任務");
    }
}

作幾點說明:

  1. cron 表達式是 @Scheduled 的屬性之一,其值可以直接設置為 cron 表達式;
  2. @Scheduled(cron="${cron.schedule}") 是動態讀取 application.properties 配置文件中的 cron 表達式。例如項目中的一個需求是每天凌晨0點執行,但是對於測試人員來說,不可能等到凌晨測試。動態讀取可以幫助解決該問題。

任務二:

@Component
public class SecondTask {
    /**
     * 上一次執行完畢時間點之後多長時間再執行(ms)
     */
    @Scheduled(fixedDelay = 2000)
    public void run(){
        System.out.println("這是創建的第二個定時任務");
    }
}

任務三:

@Component
public class ThirdTask {
    /**
     * 與fixedDelay功能相同,上一次執行完畢時間點之後多長時間再執行(ms),區別是:1、時間是字符串;2、支持佔位符
     */
    // @Scheduled(fixedDelayString = "2000")
    @Scheduled(fixedDelayString = "${time.fixedDelay}")
    public void run(){
        System.out.println("這是創建的第三個定時任務");
    }
}

上面的三個任務列舉了 @Scheduled 註解的三個參數。其實除此之外,查看 @Scheduled 的源碼可知,還有其餘的幾個參數:

  • fixedRate:上一次開始執行時間點之後多長時間再執行;
  • fixedRateString:上一次開始執行時間點之後多長時間再執行;
  • fixedRateString:與fixedRate 意思相同,只是使用字符串的形式。唯一不同的是支持佔位符;
  • initialDelay:第一次延遲多長時間后再執行;
  • initialDelayString:與 initialDelay 意思相同,只是使用字符串的形式。唯一不同的是支持佔位符;

整合Quartz

  如果以上的方式都無法滿足項目的需求,則可以試試 Quartz 調度框架。它功能的強大以及使用無需多說了。此處我們看看 Quartz 在 SpringBoot 中的使用。

創建項目,引入 Quartz 調度框架啟動器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

  需要注意版本信息,如果 SpringBoot 版本是2.0以後的版本,直接引入 Quartz 啟動器即可。但是如果是2.0以前的版本,需要引入以下 jar 包:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

下面創建任務了:

public class QuartzTask extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(new Date());
    }
}

任務需要繼承 QuartzJobBean 抽象類,並重寫 executeInternal 方法。

第三步,創建 quartz 配置類,添加 @Configuration 註解:

@Configuration
public class QuartzConfig {
    @Bean
    public JobDetail testQuartzTask() {
        return JobBuilder.newJob(QuartzTask.class).withIdentity("quartztask").storeDurably().build();
    }
    @Bean
    public Trigger testQuartzTrigger2() {
        //cron方式,每隔5秒執行一次
        return TriggerBuilder.newTrigger().forJob(testQuartzTask())
                .withIdentity("quartztask")
                .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
                .build();
    }
}

  上面的示例是使用 cron 表達式,當然也可以是固定時間間隔。本節是闡述 SpringBoot 和 Quartz 的整合,不作 Quartz 的詳細使用。感興趣的讀者可以登錄 Quartz 的官網 或者中文官網自行研究。

總結

  定時任務的實現方式有很多種,除了上面說到的幾種方式,還有利用線程池實現定時任務,有的系統是通過 Liunx 實現定時任務。總之,定時任務的實現方式多種多樣,其方式要根據項目的實際情況而選。切不可為了實現而實現。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

【朝花夕拾】Android自定義View篇之(七)Android事件分發機制(下)滑動衝突解決方案總結

前言

       轉載請聲明,轉自【https://www.cnblogs.com/andy-songwei/p/11072989.html】,謝謝!

       前面兩篇文章,花了很大篇幅講解了Android的事件分發機制的原理性知識。然而,“紙上得來終覺淺,絕知此事要躬行”,前面講的那些原理,也都是為解決實際問題而服務的。本文將結合實際工作中經常遇到的滑動衝突案例,總結滑動衝突的場景以及解決方案。本文的主要內容如下:

 

一、滑動衝突簡介

       滑動組合在平時的UI開發中非常常見,比如下圖中某App界面(圖片來源:https://www.jb51.net/article/90032.htm),該頁面上半部分显示商品列表,而下半部分显示頁面導航。當滑動上面的列表時,列表部分滑動;當列表滑動到底或者滑動下半部分時,整個頁面一起滑動。

       但是在平時的開發中,可能會經常遇到這樣的場景,滑動列表部分時,整個頁面一起滑動,而不是只滑動列表內容。或者一會兒是列表滑動,一會兒是整個頁面滑動,而不是按照預期的要求來滑動。這就是我們常說的滑動衝突問題。滑動衝突的問題,經常讓開發者們頭痛不已。因為經常很多滑動相關的控件,如ScrollView、ListView等,在單獨使用的時候酷炫不已,但將他們組合在一起使用,就失靈了。比如上圖中,手指在屏幕上上下滑動,列表和整個頁面都有滑動功能,此時如果處理不當,就會導致系統也不知道要讓誰來消費這個滑動事件,這就是滑動衝突產生的原因。

 

二、滑動衝突的三種場景

       儘管實際工作中滑動衝突的場景看似各種各樣,但最終可以歸納為三種,如下圖所示:1)圖一:外部滑動和內部滑動方向不一致;2)圖二:外部滑動和內部滑動方向不一致;3)圖三:多層滑動疊加。

 

  1、外部滑動和內部滑動方向不一致

       圖一中只示意了外部為左右滑動,內部為上下滑動的場景。顯然,內外滑動不一致,還包括外部為上下滑動,內部為左右滑動的場景。對於這種場景,平時工作中最常見的使用大概是外層為PageView,內層為一個Fragment+ListView/RecyclerView了。慶幸的是,控件PageView和RecyclerView對事件衝突做了處理的,所以平時使用這兩個控件的時候不會感受到滑動衝突的存在。如果是ScrollView+GridView等這類組合,就需要解決衝突了。

  2、外部滑動和內部滑動方向一致

       同樣,這種場景除了圖二中的內外都是上下滑動的情況外,還包括內外到時左右滑動的場景了。ScollView(垂直滾動)+ListView的組合就是比較常見的場景。第一節中的動態圖就是一個外部滑動和內部滑動方向一致的例子。

  3、多層滑動嵌套

       這種場景一般就是前面兩種場景的嵌套。“騰訊新聞”客戶端就是典型的多層滑動嵌套的使用案例,如下圖中,圖一的左邊是主頁向右滑動時才出現的滑動側邊欄,圖二是主頁界面,頂部導航欄在主頁左右滑動時可以切換,整個“要聞”界面可以上下滑動,“熱點精選”是一個可以左右滑動的橫向列表,下方還有豎直方向的列表……可見這其中嵌套層數不少。

           

 

三、滑動衝突三種場景的處理思路

       儘管滑動衝突看起來比較複雜,但是上述將它們分為三類場景后,就可以根據這三類場景來分別找出對應的分析思路。

  1、內外滑動方向不一致時處理思路

       這一類場景其實比較容易分析,因為外層和內層滑動的方向不一致,所以根據手勢的動向來確定把事件給誰。我們前面兩篇文章中分析過,默認情況下,當點擊內層控件時,事件會先一層層從外層傳到內層,由內層來處理。這裏以外層為左右滑動,內層為上下滑動為例。當判定手勢的滑動為左右時,需要外層來消費事件,所以外層將事件攔截,即在外層的onInterceptTouchEvent中檢測為ACTION_MOVE時返回true;而如果判定手勢的滑動為上下時,需要內層來消費事件,外層不需要攔截,事件會傳遞到內層來處理(具體的代碼實現,在後面會詳細列出)。這樣就通過判斷滑動的方向來決定事件的處理對象,從而解決滑動衝突的問題。

       那麼,如何來判定手勢的滑動方向呢?最常用的辦法就是比較水平和豎直方向上的位移值來判斷。 MotionEvent事件包含了事件的坐標,只要記錄一次移動事件的起點和終點坐標,如下圖所示,通過比較在水平方向的位移|dx|和|dy|的大小,來決定滑動的方向:|dy|>|dx|,本次移動的方向認為是豎直方向;反之,則認為是水平方向。當然,還可以通過夾角α的大小、斜率、速率等方式來作為判斷條件。

  2、內外滑動方向一致時處理思路

       這種場景要比上面一種複雜一些,因為滑動方向一致,所以無法通過上述的方式來判斷將事件交給誰處理。在這種情況下,往往需要根據業務的需要來判定誰來處理事件。比如豎直方向的ScrollView嵌套ListView的場景下,手指在ListView上上下滑動時:當ListView滑動到頂部且手勢向下時,顯然ListView不能再向下滑動了,這種情況下事件需要被外層控件攔截,由ScrollView來消費;當ListView滑動到底部且手勢向上時,顯然ListView也不能再向上滑動了,這種情況下事件也需要被外層控件攔截,由ScrollView來消費;其它情況下,ScrollView就不能再攔截了,滑動事件就需要由ListView來消費了,即此時上下滑動時,滑動的是ListView,而不是ScrollView。後面會以這為案例進行編碼實現。

  3、多層滑動嵌套時處理思路

       場景3看起來比較複雜,但前面也說過了,也是由前面兩種場景嵌套形成的。所以在處理場景的處理方式,就是將其拆分為簡單的場景,然後按照前面的場景分析方式來處理。

 

四、滑動衝突的兩種解決套路

       前面我們將滑動衝突分為了3種場景,並根據每一種場景提供了解決衝突的思路。但是這些思路解決的是判斷條件問題,即什麼情況下事件交給誰的問題。這一節將拋開前面的場景分類,介紹對所有場景適用的兩種通用解決方法,可以通俗地理解為處理滑動衝突的“套路”。這兩種解決滑動衝突的方式為:外部攔截法和內部攔截法。

  1、外部攔截法

       顧名思義,就是在外部滑動控件中處理攔截邏輯。這需要外部控件重寫父類的onInterceptTouchEvent方法,在其中判斷什麼時候需要攔截事件由自身處理,什麼時候需要放行將事件傳給內層控件處理,內部控件不需要做任何處理。這個“套路”的偽代碼錶示所示:

 1 @Override
 2 public boolean onInterceptTouchEvent(MotionEvent ev) {
 3     boolean intercepted = false;
 4     switch (ev.getAction()){
 5         case MotionEvent.ACTION_DOWN:
 6             intercepted = false;
 7             break;
 8         case MotionEvent.ACTION_MOVE:
 9             if(父容器需要自己處理改事件){
10                 intercepted = true;
11             }else {
12                 intercepted = false;
13             }
14             break;
15         case MotionEvent.ACTION_UP:
16             intercepted = false;
17             break;
18             default:
19             break;
20     }
21     return intercepted;
22 }

前面對滑動處理的場景分類,並對不同場景給了分析思路,它們的作用就是在這裏的第9行來做判斷條件的。所以,不論什麼場景,都可以在這個套路的基礎上,修改判斷是否攔截事件的條件語句即可。另外,需要說明一下的是,第6行和第16行,這裏都賦值為false,因為ACTION_DOWN如果被攔截了,該動作序列的其它事件就都無法傳遞到子View中了,ListView也就永遠不能滑動了;而ACTION_UP如果被攔截,那子View就無法被點擊了,這兩點我們前面的文章都講過,這裏再強調一下。

 

  2、內部攔截法

       顧名思義,就是將事件是否需要攔截的邏輯,放到內層控件中來處理。這種方式需要結合requestDisllowInterceptTouchEvent(boolean),在內層控件的重寫方法dispatchTouchEvent中,根據邏輯來決定外層控件何時需要攔截事件,何時需要放行。偽代碼如下:

 1 @Override
 2 public boolean dispatchTouchEvent(MotionEvent ev) {
 3     switch (ev.getAction()){
 4         case MotionEvent.ACTION_DOWN:
 5             getParent().requestDisallowInterceptTouchEvent(true);
 6             break;
 7         case MotionEvent.ACTION_MOVE:
 8             if (父容器需要處理改事件) {
 9                 //允許外層控件攔截事件
10                 getParent().requestDisallowInterceptTouchEvent(false);
11             } else {
12                 //需要內部控件處理該事件,不允許上層viewGroup攔截
13                 getParent().requestDisallowInterceptTouchEvent(true);
14             }
15             break;
16         case MotionEvent.ACTION_UP:
17             break;
18         default:
19             break;
20     }
21     return super.dispatchTouchEvent(ev);
22 }

除此之外,還需要外層控件在onInterceptTouchEvent中做一點處理:

1 @Override
2 public boolean onInterceptTouchEvent(MotionEvent ev) {
3     if (ev.getAction() == MotionEvent.ACTION_DOWN) {
4         return false;
5     } else {
6         return true;
7     }
8 }

ACTION_DOWN事件仍然不能攔截,上一篇文章分析源碼的時候講過,ACTION_DOWN時會初始化一些狀態和標誌位等變量,requestDisllowInterceptTouchEvent(boolean)作用會失效。這裏再順便強調一下,不明白的可以去上一篇文章中閱讀這部分內容。 

       這種方式比“外部攔截法”稍微複雜一些,所以一般推薦使用前者。同前者一樣,這也是一個套路用法,無論是之前提到的何種場景,只要根據實際判斷條件修改上述if語句即可。對於requestDisllowInterceptTouchEvent(boolean)的相關信息,在前面的文章中介紹過,這裏不再贅述了。

 

 五、代碼示例

       前面通過文字描述和偽代碼,對滑動衝突進行了介紹,並提供了一些對應的解決方案。本節將通過一個具體的實例,分別使用上述的套路來解決一個滑動衝突,從而具體演示前面“套路”的使用。

  1、未解決衝突前的示例情況

       本示例外層為一個ScrollView,內層為TextView+ListView+TextView,這兩個TextView分別為“Tittle”和”Bottom”,显示在ListView的頂部和底部,添加它們是為了方便觀察ScrollView的滑動效果。最終的布局效果如下所示:

在手機上的显示效果為:

     

在沒有解決衝突前,如果滑動中間的ListView部分,會出現ListView中的列表內容不會滑動,而是整個ScrollView滑動的現象,或者一會兒ListView滑動,一會兒ScrollView滑動。顯然,這不是我們希望看到的結果。我們希望的是,如果ListView滑到頂部時,而且手勢繼續下滑時,整個頁面下滑,即ScrollView滑動;如果ListView滑到底部了,而且手勢繼續上滑時,希望整個頁面上滑,即也是ScrollView向上滑動。

 

  2、用外部攔截法解決滑動衝突的示例

       前面說過了,這種方式需要外層的控件在重寫的onInterceptTouchEvent時進行攔截判斷,所以需要自定義一個ScrollView控件。

 1 public class CustomScrollView extends ScrollView {
 2 
 3     ListView listView;
 4     private float mLastY;
 5     public CustomScrollView(Context context, AttributeSet attrs) {
 6         super(context, attrs);
 7     }
 8 
 9     @Override
10     public boolean onInterceptTouchEvent(MotionEvent ev) {
11         super.onInterceptTouchEvent(ev);
12         boolean intercept = false;
13         switch (ev.getAction()){
14             case MotionEvent.ACTION_DOWN:
15                 intercept = false;
16                 break;
17             case MotionEvent.ACTION_MOVE:
18                 listView = (ListView) ((ViewGroup)getChildAt(0)).getChildAt(1);
19                    //ListView滑動到頂部,且繼續下滑,讓scrollView攔截事件
20                 if (listView.getFirstVisiblePosition() == 0 && (ev.getY() - mLastY) > 0) {
21                     //scrollView攔截事件
22                     intercept = true;
23                 }
24                 //listView滑動到底部,如果繼續上滑,就讓scrollView攔截事件
25                 else if (listView.getLastVisiblePosition() ==listView.getCount() - 1 && (ev.getY() - mLastY) < 0) {
26                     //scrollView攔截事件
27                     intercept = true;
28                 } else {
29                     //不允許scrollView攔截事件
30                     intercept = false;
31                 }
32                 break;
33             case MotionEvent.ACTION_UP:
34                 intercept = false;
35                 break;
36             default:
37                 break;
38         }
39         mLastY = ev.getY();
40         return intercept;
41     }
42 }

       相比於前面的偽代碼,這裏需要注意一點的是多了第12行。因為本控件是繼承自ScrollView,而ScrollView中的onInterceptTouchEvent做了很多的工作,這裏需要使用ScrollView中的處理邏輯,才需要加上這一句。如果是完全自繪的控件,即直接繼承自ViewGroup,那就無需這一句了,因為控件需要自己完成自己的特色功能。第18行是獲取子控件ListView的實例,這個是參照後面的布局文件activity_event_examples來定位的,也可以通過其它的方式來獲取實例。另外就是ListView的實例可以通過其它方式一次性賦值,而不用這裏每次ACTION_MOVE都獲取一次實例,從性能上考慮會更好,這裏為了便於演示,先忽略這一點。其它要點在註釋中也說得比較明確了,這裏不贅述。

       使用CustomScrollView控件,界面的布局如下:

 1 //==============activity_event_examples=============
 2 <?xml version="1.0" encoding="utf-8"?>
 3 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 4     android:layout_width="match_parent"
 5     android:layout_height="match_parent"
 6     android:orientation="vertical">
 7 
 8     <com.example.demos.customviewdemo.CustomScrollView
 9         android:id="@+id/demo_scrollview"
10         android:layout_width="match_parent"
11         android:layout_height="match_parent">
12 
13         <LinearLayout
14             android:layout_width="match_parent"
15             android:layout_height="match_parent"
16             android:orientation="vertical">
17 
18             <TextView
19                 android:id="@+id/tv_title"
20                 android:layout_width="match_parent"
21                 android:layout_height="100dp"
22                 android:background="@android:color/darker_gray"
23                 android:gravity="center"
24                 android:text="Title"
25                 android:textSize="50dp" />
26 
27             <ListView
28                 android:id="@+id/demo_lv"
29                 android:layout_width="match_parent"
30                 android:layout_height="600dp" />
31 
32             <TextView
33                 android:layout_width="match_parent"
34                 android:layout_height="100dp"
35                 android:background="@android:color/darker_gray"
36                 android:gravity="center"
37                 android:text="Bottom"
38                 android:textSize="50dp" />
39         </LinearLayout>
40     </com.example.demos.customviewdemo.CustomScrollView>
41 </LinearLayout>

這裏需要注意的是,在ScrollView中嵌套ListView時,ListView的高度需要特別處理,如果設置為match_parent或者wrap_content,都會一次只能看到一條item,所以上面給了固定的高度600dp來演示效果。平時工作中,往往還需要對ListView的高度做一些特殊的處理,這不是本文的重點,這裏不細講,讀者可以自行去研究。

       最後就是給ListView填充足夠的數據:

 1 public class EventExmaplesActivity extends AppCompatActivity {
 2 
 3     private String[] data = {"Apple", "Banana", "Orange", "Watermelon",
 4             "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
 5             "Apple", "Banana", "Orange", "Watermelon",
 6             "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango"};
 7 
 8     @Override
 9     protected void onCreate(Bundle savedInstanceState) {
10         super.onCreate(savedInstanceState);
11         setContentView(R.layout.activity_event_exmaples);
12         showList();
13     }
14 
15     private void showList() {
16         ArrayAdapter<String> adapter = new ArrayAdapter<String>(
17                 EventExmaplesActivity.this, android.R.layout.simple_list_item_1, data);
18         ListView listView = findViewById(R.id.demo_lv);
19         listView.setAdapter(adapter);
20     }
21 }

 

  3、用內部攔截法解決滑動衝突的示例

       同樣,前面的偽代碼中也講過,這裏需要在內層控件中重寫的dispatchTouchEvent方法處判斷外層控件的攔截邏輯,所以首先需要自定義ListView。

 1 public class CustomListView extends ListView {
 2 
 3     public CustomListView(Context context, AttributeSet attrs) {
 4         super(context, attrs);
 5     }
 6 
 7     //為listview/Y,設置初始值,默認為0.0(ListView條目一位置)
 8     private float mLastY;
 9 
10     @Override
11     public boolean dispatchTouchEvent(MotionEvent ev) {
12         int action = ev.getAction();
13         switch (action) {
14             case MotionEvent.ACTION_DOWN:
15                 //不允許上層的ScrollView攔截事件.
16                 getParent().requestDisallowInterceptTouchEvent(true);
17                 break;
18             case MotionEvent.ACTION_MOVE:
19                 //滿足listView滑動到頂部,如果繼續下滑,那就允許scrollView攔截事件
20                 if (getFirstVisiblePosition() == 0 && (ev.getY() - mLastY) > 0) {
21                     //允許ScrollView攔截事件
22                     getParent().requestDisallowInterceptTouchEvent(false);
23                 }
24                 //滿足listView滑動到底部,如果繼續上滑,允許scrollView攔截事件
25                 else if (getLastVisiblePosition() == getCount() - 1 && (ev.getY() - mLastY) < 0) {
26                     //允許ScrollView攔截事件
27                     getParent().requestDisallowInterceptTouchEvent(false);
28                 } else {
29                     //其它情形時不允ScrollView攔截事件
30                     getParent().requestDisallowInterceptTouchEvent(true);
31                 }
32                 break;
33             case MotionEvent.ACTION_UP:
34                 break;
35         }
36 
37         mLastY = ev.getY();
38         return super.dispatchTouchEvent(ev);
39     }
40 }

可能有讀者會有些疑惑,從布局結構上看,listView和ScrollView之間還隔了一層LinearLayout,getParent().requestDisallowInterceptTouchEvent(boolean)方法會奏效嗎?實際上這個方法是針對所有的父布局的,而不是只針對直接父布局,這一點需要注意。

       參照偽代碼的套路,這裏還需要對外層的ScrollView做一些邏輯處理:

 1 public class CustomScrollView extends ScrollView {
 2     public CustomScrollView(Context context, AttributeSet attrs) {
 3         super(context, attrs);
 4     }
 5 
 6     @Override
 7     public boolean onInterceptTouchEvent(MotionEvent ev) {
 8         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
 9             return false;
10         } else {
11             return true;
12         }
13     }
14 }

       在布局文件中使用CustomListView,將前面activity_event_examples.xml布局文件中的第27行的ListView替換為com.example.demos.customviewdemo.CustomListView即可。其它的和前面外部攔截法示例一樣,這裏不贅述。

 

結語

       關於滑動衝突的內容就講完了。實際工作中的場景可能比這裏demo中要複雜一些,筆者為了突出重點,所舉的例子選得比較簡單,但原理都一樣的,所以希望讀者能夠好好理解,重要的地方,甚至需要記下來。同樣,Android事件分發機制系列的知識點,要講的也講完了,三篇文章側重於三個方面:1)第一篇重點總結了Touch相關的三個重要方法對事件的處理邏輯;2)第二篇重點分析源碼,從源碼的角度來分析第一篇文章中的邏輯;3)第三篇重點在實踐,側重解決實際工作中經常遇到的事件衝突問題——滑動衝突。當然,事件分發相關的問題遠不是這3篇文章能說清楚的,文中若有描述錯誤或者不妥的地方,歡迎讀者來拍磚!!!

 

參考資料

       任玉剛《Android開發藝術探索》

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

記:使用IScroll.js 開發picker日曆組件遇到的問題及經驗總結

IScroll中文文檔

第一個問題: 邊界留白

  

  就是這種,上邊界(最小),下邊界(最大)有兩個列表的位置是不能選擇的。解決的辦法是:

  

  在HTML中,添加空白節點就行了。

 

第二個問題:初始化之後的滾動停止的事件的第二個參數問題。  

var myScroll = new IScroll('#wrapper');
myScroll.on('scrollEnd', function(){
    //這裏一定不要寫成es6箭頭函數
    //要執行的代碼
    //這個函數沒有參數
});

(1) 第二的個參數,是個函數。它沒有參數,而且不要寫成,不要寫成,不要寫成箭頭的形式。因為這函數裏面的this,是綁定的一些有用信息,比如:this.y是當前滾動的距離等。還有哪些信息可以看 文檔中的 滾動條信息 這一欄。如果寫成ES6的形式,那this指向就變了,這樣就獲取不了所需的信息。

(2) 第二個參數是沒有形參的。即沒有任何可使用的參數。

 

第三個問題:定義了snap選項,但是滾動有偏差

  開發的日曆選擇組件picker是使用rem單位自適應的,雖然在配置項中 有個options.snap,官方說可以對齊到固定的位置和元素,但是在使用自適應單位的情況中,這個配置並沒有展現出真正的效果,滾動的時候一定會出現偏差。

  那怎麼解決這個自適應的問題呢?由於是在滾動結束之後,位置才出現的偏差。那麼我就在滾動結束之後立馬調用修正位置的函數就行了。

  我是在vue中使用的。所以定義下面的函數,因為有 年,月,日,時四個滾動項。所以需要判斷是哪一個正在滾動

fixPos: function(target,num) {
    var step = Math.abs(Math.round(target.y / this.itemHeight));
    var maxYearLen = this.yearArr.length;
    var maxMonthLen = this.monthArr.length;
    var maxDayLen = this.dayArr.length;
    var maxHourLen = this.hourArr.length;
    switch(num){
        case 0:
            step = step > maxYearLen ? maxYearLen - 1 : step;
            break;
        case 1:
            step = step > maxMonthLen ? maxMonthLen - 1 : step;
            break;
        case 2:
            step = step > maxDayLen ? maxDayLen - 1 : step;
            break;
        case 3:
            step = step > maxHourLen ? maxHourLen - 1 : step;
            break;
    }
    var fixPos = step * this.itemHeight;//重新計算較為精確的位置
    target.y = fixPos;//重置原來的滾動距離 this.selectArr[num] = step;//這是保存每個列表滾動的索引值
    target.scrollTo(0, -fixPos);//這是滾動到修正後的位置
},

(1) 大致的思路就是:首先用當前滾動的距離,來除以滾動內容中,每個列表的高度。然後取最近似的值,就是當前應該滾動的列表的個數。

(2) 如果出現突然滾動到最底部,這時候需要滾動的個數大於了滾動內容的最大列表個數,那麼就糾正一下個數為最大列表數 – 1。

(3) 然後設置較為精確的滾動距離。再滾動到指定的位置。

(4) this.itemHeight是在created生命周期的時候就聲明的 this.itemHeight = (document.body.offsetWidth / 750) * 100 * 0.8;  相當於在375px寬度下,每個列表就是40px的高度。在iPhone5 320px下,就是34.133334了。

  調用的時候: 

var yearScroll = new IScroll('#calendarYear');
var that = this;
yearScroll.on('scrollEnd', function() {
    that.fixPos(this, 0);
})

  

第四個問題:使用自適應單位時最大滾動距離不準確

  這個問題和第二個問題類似,解決的方法:

fixMaxScrollY: function(target, num) {
    var yearLen = this.yearArr.length - 1;
    var monthLen = this.monthArr.length - 1;
    var dayLen = this.dayArr.length - 1;
    var hourLen = this.hourArr.length - 1;
    switch(num) {
        case 0:
            target.maxScrollY = -(Math.round(yearLen * this.itemHeight));
            break;
        case 1:
            target.maxScrollY = -(Math.round(monthLen * this.itemHeight));
            break;
        case 2:
            target.maxScrollY = -(Math.round(dayLen * this.itemHeight));
            break;
        case 3:
            target.maxScrollY = -(Math.round(hourLen * this.itemHeight));
            break;
    }
},

(1)  實例化后的滾動對象,有個最大滾動值maxScrollY,主要也是根據滾動內容的列表長度來重置最大滾動距離

(2)  因為有四個滾動的內容項,所以需要傳入當前是第幾個滾動的內容。

  調用的時候:

var yearScroll = new IScroll('#calendarYear');
this.fixMaxScrollY(yearScroll, 0);

 

第五個問題: 日曆組件的內容是在點擊某個按鈕之後再觸發显示的,最初是隱藏。但就是這個原因,導致显示出來的內容滾動不了

  解決的辦法是:使用 xxx.refresh() 刷新函數。這個函數具體的說明可以看文檔中 刷新 這個選項內容

  在控制組件显示的函數中,調用刷新的方法。

this.$nextTick(function(){
    this.scrollTarget[0].refresh();
    this.scrollTarget[1].refresh();
    this.scrollTarget[2].refresh();
    this.scrollTarget[3].refresh();
    //如果默認隱藏,則必須修復滾動的位置
    this.scrollTarget[0].scrollTo(0, -this.itemHeight * this.selectArr[0]);
    this.scrollTarget[1].scrollTo(0, -this.itemHeight * this.selectArr[1]);
    this.scrollTarget[2].scrollTo(0, -this.itemHeight * this.selectArr[2]);
    this.scrollTarget[3].scrollTo(0, -this.itemHeight * this.selectArr[3]);
})

(1) scrollTarget 是初始化滾動實例之後,保存的滾動實例。因為多次會用到。

(2) 因為有年,月,日,時四個滾動內容,所以要刷新四個滾動器。

(3) 如果日曆有最初的滾動位置,那麼也會出現不能跳到指定的位置的問題。所以,也需要初始化最初始的位置。

(4) selectArr 是滾動器滾動的索引值。比如我月份是1月到12月,當前滾動到了6月,那麼此時selectArr[1] 就是5 。

(5) 官方是推薦用setTimeout來使用刷新,但是我使用的是vue來開發的,所以,這裏用vm.$nextTick()來代替setTimeout。

 

第六個問題: 日曆組件復用時,只能初始化滾動第一個日曆組件,而且第一個日曆組件滾動還是有問題。

  這個問題產生的原因很簡單:因為 IScroll 在初始化實例的時候, var myScroll = new IScroll(‘.wrapper’);  它這個css選擇器使用的是querySelector 而不是 querySelectorAll,所以iScroll只會作用到選擇器選中元素的第一個。如果你需要對多個對象使用iScroll,你需要構建自己的循環機制。這是官方的說法。那麼怎樣建立循環機制呢?難道我要在初始化的時候還要循環去 new IScroll(xxx)創建實例嗎?

  其實沒必要。只需要改一改源碼就行了。

  我使用的是 iscroll-lite 這個版本。這裏面 在定義 IScroll 這個函數的時候有這段代碼

this.wrapper = typeof el == 'string' ? document.querySelector(el) : el;

  它每次初始化時,只選擇了 el  滾動對象的第一個元素,那麼,我只需要傳入當前是第幾個日曆組件,再改成querySelectorAll就行了。即:

this.wrapper = typeof el == 'string' ? document.querySelectorAll(el)[childIndex] : el;

  然後在這個定義的 IScroll 函數的參數中,再增加一個參數,表示第幾個元素。

  然後在初始化滾動實例的時候:

var options = {
    snap: '.calendar-scroll-item', //對齊的位置,相當於自動糾正每次移動的距離
    //scrollbars: true,//是否显示滾動條
}
//初始化滾動
var yearScroll = new IScroll('.calendar-scroll>.calendarYear', options , this.curIndex);

  當然這個時候,css選擇器就不要用 id 了。

  然後在 組件的 prop 裏面添加一個屬性。

curIndex:{
    type:Number,
    default:0
}

(1) 默認只使用一個組件,即不傳這個prop 的話就是默認初始化第一個組件的滾動內容。

  使用組件的時候:引入,註冊等步驟就省略了

<calendar :cur-index='0'/>
<calendar :cur-index='1'/>
<calendar :cur-index='2'/>

   這樣不管使用多少個,都能正常初始化滾動了。而且互不影響 。注意,如果傳的是数字,需要v-bind 告訴vue這不是字符串,是表達式。不然傳過去的是字符串

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"