小白學 Python 爬蟲(3):前置準備(二)Linux基礎入門

人生苦短,我用 Python

前文傳送門:

Linux 基礎

CentOS 官網: 。

CentOS 官方下載鏈接: 。

Linux 目前在企業中廣泛的應用於服務器系統,無論是寫好的代碼,還是使用的第三方的開源的產品,絕大多數都是部署在 Linux 上面運行的。

可能很多同學一提到 Linux 就慫了,黒糊糊的一篇,連個界面都沒有,滿屏幕都是神秘代碼,沒有一個看得懂的。

表怕,本文就帶你入門 Linux 。

Linux 有不同的發行版本,而我們在企業中一般使用的是 CentOS ,目前比較常用的版本已經到了 7.x 。

由於 Linux 是開源的,所以不同廠商之間提供的發行版會有非常多,比較常見的有 Ubuntu( 基於Debian的桌面版 ) 、Debian( 國際化組織的開源操作系統 ) 、 RedHat( 紅帽企業系統 ) 、 Fedora( 最初由紅帽公司發起的桌面版系統套件 ) 等等。

因為在企業中使用比較多的還是 CentOS ,所以我們還是拿 CentOS 來介紹。

在 win 系統下的安裝可以使用第三方廠商提供的 VMware 或者 win 自帶的 Hyper-V 構建一個虛擬機進行安裝,也可以使用雲服務廠商提供的入門版的雲服務器(1H1G1M),一般新用戶首年價格都在100元以內。

安裝的過程我就不介紹了,百度一下大把。

安裝完成后,設置好 Linux root 用戶的密碼后,可以使用 ssh 工具進行連接,這裏的工具可以選擇 xshell (個人使用免費,就是官網屬實有點慢),打開 xshell 輸入 ip 、用戶名(root)、密碼后,應該可以看到如下界面:

小編這裏使用的是京東雲的服務器,打碼部分涉及 IP 信息,所以隱藏掉了,屬實怕大神搞我。

因為我們的目標不是 Linux 運維工程師,只需要能正常使用,一些簡單常用指令足夠我們日常操作 Linux 了。

首先介紹一下 Linux 的目錄,因為是使用 root 賬號登錄的,所以我們登錄后的目錄是在 /root ,查詢當前所在目錄可以使用命令 pwd ,如下:

輸入命令 cd / ,進入根目錄,再輸出命令 ls ,查看根目錄下都有什麼目錄:

大致介紹下每個目錄放的都是什麼東西:

目錄 簡介
/bin 常用命令一般在這個目錄。
/boot 存放用於系統引導時使用的各種文件。
/dev 用於存放設備文件。
/etc 一般用於存放系統的管理和配置文件。
/home 存放所有用戶文件的根目錄,是用戶主目錄的基點,比如用戶user的主目錄就是/home/user,可以用~user表示。
/lib 存放跟文件系統中的程序運行所需要的共享庫及內核模塊。共享庫又叫動態鏈接共享庫,作用類似windows里的.dll文件,存放了根文件系統程序運行所需的共享文件。
/usr 用於存放系統應用程序,比較重要的目錄/usr/local 本地系統管理員軟件安裝目錄(安裝系統級的應用)。這是最龐大的目錄,要用到的應用程序和文件幾乎都在這個目錄。
/opt 額外安裝的可選應用程序包所放置的位置。
/root 超級用戶(系統管理員)的主目錄。
/var 用於存放運行時需要改變數據的文件,也是某些大文件的溢出區,比方說各種服務的日誌文件(系統啟動日誌等)等。

很多都是系統使用的目錄,我們無需關注,一般會使用到的目錄有 /etc (修改一些系統配置,如改host文件,系統環境變量等), /usr (這裡會安裝一些應用程序),/opt (這裏其實也是安裝一些應用程序)。

簡單介紹幾個命令,有了這幾個命令,基本上我們就可以愉快的操作起來了:

  1. cd:這個不用多講了吧,就是切換目錄。
  2. ls:這個是查看目錄內容。
  3. pwd:显示當前工作目錄 。
  4. mkdir:創建目錄。
  5. vi:編輯文檔,這個命令稍微複雜一點
    1. vi 文件名 :進入一般模式(不能輸入)
    2. 按下 i 從一般模式,進入到插入模式,這時可以修改文檔
    3. 按下esc從插入模式,退出到一般模式 ,這時無法修改文檔
    4. 在一般模式下,輸入:wq ,保存退出編輯;或者還可以輸入 !q 不保存編輯內容退出。
  6. ps: 查看任務管理器: ps -ef ,例如查看 mysql 的進程,ps -ef | grep mysql 。
  7. kill:這個就是殺進程,常用格式 kill -9 pid(進程編號),配合上面的 ps 命令一起使用,殺掉你想殺的進程。
  8. tar:壓縮與解壓,常用解壓命令 tar -xvzf [需解壓的文件名] ,常用壓縮命令 tar -cvzf [壓縮后的文件名] [被壓縮的文件名] 。
  9. reboot:重啟
  10. halt:關機
  11. rm:刪除命令,常用核彈級命令 rm -rf / ;此命令禁止在任何地方嘗試,一旦執行,將無法逆轉,含義是將跟目錄直接刪除。

下面我們來演示下如何在 CentOS 上安裝 Python3 。

因為 CentOS 本身自帶 Python ,但是版本是 Python2.7 :

這裏我們不去管它,首先去 Python 官網找到 Python 的下載地址:

Python 官網下載鏈接:

小編這裏選擇的是截止目前最新發布的 3.8.0 版本。

這時我們切換到 xshell 的操作界面開始操作起來,首先切換至 /opt 目錄:

cd /opt

然後下載 Python3.8 的安裝包:

wget https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz

這裏遇到新的命令 wget ,這個命令如果 CentOS 未提供,需要先進行安裝:

yum install wget

簡單介紹一下, yum 是在 Linux 中的一個包管理工具,可以進行簡單的安裝操作。

等待進度條下載完,下載完成后直接解壓:

tar -xvzf Python-3.8.0.tgz

解壓后編譯安裝:

# 創建安裝目錄
mkdir /usr/local/python3
cd Python-3.8.0
# 檢查配置
./configure --prefix=/usr/local/python3
# 編譯、安裝
make && make install
# 創建軟連接
ln -s /usr/local/python3/bin/python3 /usr/bin/python3
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3

測試安裝結果:

# 輸入
python3 -V
# 輸出
Python 3.8.0
# 輸入
pip3 -V
# 輸出
pip 19.2.3 from /usr/local/python3/lib/python3.8/site-packages/pip (python 3.8)

因為 Linux 部分功能也是依賴 Python 的,我們不覆蓋當前的 Python 命令的版本,直接創建一個新的 Python 命令 python3 。以及新的 pip 包管理命令 pip3 。

希望各位同學可以自己使用虛擬機安裝一個 CentOS 試試看,後續的部分內容將會涉及 Linux 。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

就該這樣理解 OSI 七層參考模型、淺談不同局域網之間的通信

簡介

說到OSI參考模型,理解網絡與網絡之間的關係,不說太深入難以理解的東西,只求能最大程度上理解與使用。

參考模型是國際標準化組織(ISO)制定的一個用於計算機或通信系統間互聯的標準體系,一般稱為OSI參考模型或七層模型。

概念性的東西,先知道這些就夠了,我們先來聊一聊一個常見的一個模型。

 

局域網與互聯網

互聯網就是許許多多個局域網組成的,從我們最簡單的一個局域網入手,開始理解

這裏舉例兩個不同的局域網,計算機用網線接入交換機、交換機連接網關路由器,另一處

也是通過相同的方式進行連接。先來了解一下OSI參考模型是如何定義這七層的

OSI 參考模型

參考:

這裏定義的七層只是為了方便我們去理解,實際上是不存在的。

簡單的了解一下這七層是如何定義的,具體的功能還是得舉例子來理解說明。

從最上層的應用層開始說起:如何一步步的封裝數據,到最後進行發送。

應用層

應用層是直接面向用戶的最高層,但它卻不是應用程序,它只是為引用程序提供服務的

就好比,我們用的電腦版微信吧!它就是一個實實在在的應用程序,假設要與一個遠方的小姐姐進行聊天會話,這個時候呢,發送一個Hello給遠方的小姐姐。

當你點擊發送的時候,其實做了很多事情,我們就來梳理一下。

需要發送的數據就是:Hello ,當然,應用層首先給這個數據拼接一個AH,這裏就是應用層的報頭,就好比是微信的一個特有的數據,就這樣先理解。

 

表示層

當然,我們總不能發送明文吧,將發送的文本數據進行編碼,平常我們計算機使用的萬國碼UTF-8,肯定要進行一下加密吧

表示層更關心的是所傳送數據的語法和語義,主要包括數據格式變化、與解密、與解壓等

 

會話層

字面意思,就可以理解出這一層表示的意思,建立一個會話,就好比使用Http訪問web的時候,都會存在一個Session 作為標識

讓服務器來區別訪問的計算機。主要功能是負責維護兩個節點之間的傳輸聯接,確保點到點傳輸不中斷,以及管理數據交換等功能。

會話層在應用進程中建立、管理和終止會話。會話層還可以通過對話控制來決定使用何種通信方式,通信或通信。會話層通過自身協議對請求與應答進行協調

 

傳輸層 端到端

傳輸層作為OSI參考模型中,最重要的一層,這裏主要是以端口到端口來區分。這裏涉及到兩個特別重要的協議TCP 以及UDP

一太計算機上同時運行着QQ、微信、以及瀏覽器等。發送數據報,這個數據包到底是哪個程序發出去的呢?當然要從指定的一個端口發出去

計算機的端口範圍 0-65535

0-1023 就是1024個端口為系統佔用端口

了解到這些,就該先來說說UDP協議

UDP 協議

UDP協議定義了端口,同一個主機上的每個應用程序都需要指定唯一的端口號,並且規定網絡中傳輸的數據包必須加上端口信息,當數據包到達主機以後,就可以根據端口號找到對應的應用程序了。UDP協議比較簡單,實現容易,但它沒有確認機制,數據包一旦發出,無法知道對方是否收到,因此可靠性較差,為了解決這個問題,提高網絡可靠性,TCP協議就誕生了。

 

參考:

一個UDP報文包含首部與數據部分,UDP首部佔用8個字節,數據部分最長長度為65535B(字節) 即 64KB

UDP協議是無連接,不保證穩定傳輸的協議,但處理速度較快,通常的音頻、視頻在傳送時候使用UDP較多。

我們這裏的例子是微信,微信能保證數據百分百到達,所以我們採用TCP來具體說明數據的封裝

 

TCP 協議

TCP即傳輸控制協議,是一種面向連接的、可靠的、基於字節流的通信協議。簡單來說TCP就是有確認機制的UDP協議,每發出一個數據包都要求確認,如果有一個數據包丟失,就收不到確認,發送方就必須重發這個數據包。為了保證傳輸的可靠性,TCP協議在UDP基礎之上建立了三次對話的確認機制,即在正式收發數據前,必須和對方建立可靠的連接。TCP數據包和UDP一樣,都是由首部和數據兩部分組成,唯一不同的是,TCP數據包沒有長度限制,理論上可以無限長,但是為了保證網絡的效率,通常TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包不必再分割

 

參考:

 這裏只需要了解的是TCP的基本封裝過程,這裏只涉及到源端口以及目的端口,還未涉及到IP相關的內容。它和UDP協議一樣。就好像是一個改進版的

UDP協議,它能保證數據的可靠傳輸,這個特點記住即可。這裏模擬一下,我們數據的封裝過程。假設微信使用的端口是6666,目標端口就是遠方小姐姐微信

的端口,當然也是一樣的。這裏我為了理解只做簡寫

 

網絡層

從上面幾層來看,我們已經將微信的數據封裝成來一個TCP數據報,裡面包含來微信的端口 假設是6666,當然,就好比寫信一樣,我的信封

已經準備好勒,裏面要發送的內容我也已經準備好了,接下來就是地址了。肯定要指定這個報文我要發送到哪裡去。所以呢IP 網際協議,就誕生了。

IP 網際協議

網絡層引入了IP協議,制定了一套新地址,使得我們能夠區分兩台主機是否同屬一個網絡,這套地址就是網絡地址,也就是所謂的IP地址。IP協議將這個32位的地址分為兩部分,前面部分代表網絡地址,後面部分表示該主機在局域網中的地址。如果兩個IP地址在同一個子網內,則網絡地址一定相同。為了判斷IP地址中的網絡地址,IP協議還引入了子網掩碼,IP地址和子網掩碼通過按位與運算后就可以得到網絡地址

IP地址在這裏我們就比較好理解了。我們平時的生活中都會涉及到。一個IP指向的就是互聯網當中的一台機器或者就是一台路由器了。

我們來封裝數據。再把上面的圖拿下來,說明一下,我們要給E電腦的小姐姐發送消息。比如我是A電腦,小姐姐在另外一個網關下的E電腦

 

 

比較重要的兩個參數:

源地址:192.168.0.120

目標地址:192.168.1.135

進行封裝后的數據,這裏將源地址,告訴路由器(郵局) 發件人 就是源地址,以及收件人 也就是目標地址

 

ARP 協議

這裏暫時不細說這個ARP協議的內容。我們只需要知道 ARP協議是用來拿IP換MAC地址的,上面的IP協議也已經提過了,通過子網掩碼和IP地址的換算,可以得到

網絡號,網絡號就可以區別這兩個IP是否在同一個局域網內。 參考這個秒懂:

數據鏈路層

到這一層,就已經到網卡、網絡設備(交換機)的範疇了。數據鏈路層最重要的協議是以太網協議,數據鏈路層最重要的一點就是數據成幀。

以太網協議

接入以太網的設備必須包含一塊以太網網卡,也就是我們常用的網卡,一組電信號稱作是一個數據幀 、或者叫做一個數據包

網卡都包含一個全球唯一的MAC地址,發送端的和接收端的地址便是指網卡的地址,即Mac地址。 

每塊網卡出廠時都被燒錄上一個實際上唯一的Mac地址,長度為48位2進制,通常由12位16進制數表示,(前六位是廠商編碼,后六位是流水線號)

 進行數據鏈路層的封裝,將本機的MAC(源MAC地址) 和目標MAC地址封裝在頭部,在尾部加入DT報尾,這樣 一個數據幀算是封裝完成了!

等等。我們好像還不知道小姐姐那邊的目標MAC地址,這時候就需要用ARP協議了。我們知道ARP協議就是用來用IP來換MAC地址的。

 

ARP協議

上面已經簡單的了解過了,我們要和在B局域網下的E電腦進行通信,但是我們不知道它的MAC地址,於是我們發送一個ARP請求,來獲取目標的MAC地址

目標MAC 為FF:FF:FF:FF:FF:FF 表示的是廣播地址,這個數據包發出去后,所有的子網機器都會收到,收到的機器判斷目標MAC是否是自己,若不是,則直接丟棄

若是,收到報文的主機會通過單播的形式,將MAC地址回傳給我們。

通過路由協議我們可以得知,若不在一個一個子網內,則會交給路由器

 路由器返回的包裏面,目標MAC就會變成路由器的MAC地址,我們拿路由器的MAC地址組裝數據鏈路層報文即可。

物理層 

經過以上的每一層的層層包裝,這時候,我們已經包裝好了一個以太網數據幀,包含源MAC,目標MAC,源IP,目標IP等等一系列數據。

物理層就是將這個數據通過電信號、光信號的方式傳遞過去的,物理層一般都是我么所說的光纜以及網線這些硬件設備。

 

 

 不同子網間的通信

通過上面的知識,我們已經了解到如何封裝成一個數據幀,以及一些協議的相關內容。那麼這裏就會有一個問題,同一子網、

不同子網、以及相隔很遠的兩個子網是如何進行通信的呢?以及我們撥號上網后,公網IP與內網IP是怎麼一回事呢?

 

同一子網通信

我們先來看一個圖,計算機A要與計算機B進行通信,這時候他們是同處於一個子網內的,這個時候就很簡單了。

按照上面的七層進行封裝數據,這裏的具體參數需要說明一下:

源IP: 0.120(簡寫)

目標IP:0.113

源MAC : A電腦的MAC

目標MAC:B電腦MAC(這裏若不知道就先發送ARP請求)

 

 

A將數據報發送出去后,交換機直接查詢目標MAC所轉發的端口,將這個數據報準確的推送到B電腦連接的那個端口即可。

不同子網通信

A電腦需要與E電腦進行通信,這時候發現A與E不在一個子網內,這時候呢,就需要路由器來協助了

源IP: A的IP

目標IP: E的IP

源MAC:A的 MAC

目標MAC: 路由器C的MAC

 

 

因為不在一個子網內,需要路由器來進行路由這個數據包,送至D路由器后,D路由器拿出數據報中目標的IP,發送ARP請求,

請求E的MAC地址,知道后,將數據報裏面的目標MAC進行替換,然後發送給E即可。

 

公網IP與內網IP通信的方式理解

我們在使用路由器上網后,運營商就會給我們分配一個公網IP,按照圖上的指示,C路由器在進行撥號后,就會給C路由器分配一個公網IP

我這裏假設有這樣兩個。這時候需要封裝數據,該如何封裝呢,還是以A電腦與E電腦進行通信,大家肯定會很迷惑。

這裏就需要了解一個協議:網路地址轉換協議:

以下簡稱NAT,NAT 在IPV4 之前起到很大的作用,我們現在也在用,因為IPV4 IP數量的限制,但接入互聯網的電腦又那麼多

該怎麼辦呢。就是給一個路由下分配一個公網IP,路由器下面的IP與公網IP進行一個轉換,這裏面說的轉換就是:NAT

圖中黑色的就是轉換部分,通過端口的轉換,將多個子網IP映射到公網的一個IP上面

 

網絡地址端口轉換(NAPT)

這種方式支持端口的映射,並允許多台主機共享一個公網IP地址。 支持端口轉換的NAT又可以分為兩類:源地址轉換和目的地址轉換。前一種情形下發起連接的計算機的IP地址將會被重寫,使得內網主機發出的數據包能夠到達外網主機。后一種情況下被連接計算機的IP地址將被重寫,使得外網主機發出的數據包能夠到達內網主機。實際上,以上兩種方式通常會一起被使用以支持雙向通信。 還是舉例,這時候,我們的A電腦需要與E電腦進行通信,E電腦在廣東省,他們撥號后,都會分配一個公網IP,並且已經在路由器裏面完成了NAT映射,
源IP: A電腦IP
目標IP: E電腦映射后的公網IP
源MAC :A電腦MAC
目標MAC :  本地路由器MAC地址   封裝完成后,將數據報送到C路由器,路由器通過映射表,將源IP進行一個替換

 

替換后,交給互聯網上的路由器進行數據報的轉發,這就好像發快遞時候一樣,經過一系列的中轉站,到達目的路由。

到達D路由后,D路由將數據報中的目標地址也進行一個轉換,這個地址是可以相互轉的。現在就是公網映射轉到本機IP

 

轉換后就輕鬆了。按照ARP請求到E機器的MAC地址,然後發報即可。

 

小結

以上內容皆是自己查看一些博主的總結,通過學習后,能夠加深自己對OIS模型、以及TCP、IP、ARP

這些非常重要的協議的一個認識。以及了解到不同層級下面。兩台電腦如何完成一個通行。這裏講的比較淺,

互聯網的奧妙不是那麼容易就可以理解透的。還是那句,不要停止學習的腳步。就好

 

參考:

  • ARP請求 
  • 不同子網內兩台機器的通信方式 
  • OSI 參考模型 
  • 內網端口與外網端口的理解 
  • 網絡地址轉換協議:

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

中高級前端面試秘籍,助你直通大廠(一)

引言

又是一年寒冬季,只身前往沿海工作,也是我第一次感受到沿海城市冬天的寒冷。剛過完金九銀十,經過一場慘烈的江湖廝殺后,相信有很多小夥伴兒已經找到了自己心儀的工作,也有的正在找工作的途中。考慮到年後必定又是一場不可避免的廝殺,這裏提前記錄一下自己平時遇到和總結的一些知識點,自己鞏固複習加強基礎的同時也希望能在你的江湖路上對你有所幫助。筆者在入職最近這家公司之前也曾有過長達3個月的閉關修鍊期,期間查閱資料無數,閱讀過很多文章,但總結下來真正讓你印象深刻的,不是那些前沿充滿神秘感的新技術,也不是為了提升代碼逼格的奇淫巧技,而是那些我們經常由於項目周期緊而容易忽略的基礎知識。所謂萬丈高樓平地起,只有你的地基打得足夠牢固,你才有搭建萬丈高樓的底氣,你才能在你的前端人生路上越走越遠。

這篇主要是先總結一下CSS相關的知識點,可能某些部分不會涉及到太多具體的細節,主要是對知識點做一下匯總,如果有興趣或者有疑惑的話可以自行百度查閱下相關資料或者在下方評論區留言討論,後續文章再繼續總結JS和其他方面相關的知識點,如有不對的地方還請指出。

1. CSS盒模型

CSS盒模型就是在網頁設計中經常用到的CSS技術所使用的一種思維模型。CSS 假定所有的HTML 文檔元素都生成了一個描述該元素在HTML文檔布局中所佔空間的矩形元素框,可以形象地將其看作是一個盒子。CSS 圍繞這些盒子產生了一種“盒子模型”概念,通過定義一系列與盒子相關的屬性,可以極大地豐富和促進各個盒子乃至整個HTML文檔的表現效果和布局結構。

CSS盒模型可以看成是由從內到外的四個部分構成,即內容區(content)、內邊距(padding)、邊框(border)和外邊距(margin)。內容區是盒子模型的中心,呈現盒子的主要信息內容;內邊距是內容區和邊框之間的空間;邊框是環繞內容區和內邊距的邊界;外邊距位於盒子的最外圍,是添加在邊框外周圍的空間。

根據計算寬高的區域我們可以將其分為IE盒模型和W3C標準盒模型,可以通過box-sizing來進行設置:

  • content-box:W3C標準盒模型
  • border-box:IE盒模型

區別:
W3C標準盒模型:width(寬度) = content(內容寬度)
IE盒模型:width(寬度) = content(內容寬度) + padding(內邊距) + border(邊框)

2. BFC

BFC即Block Fromatting Context(塊級格式化上下文),它是頁面中的一塊獨立的渲染區域,並且有一套渲染規則,它決定了其子元素將如何定位,以及和其他元素的關係和相互作用。具有BFC特性的元素可以看成是一個隔離的獨立容器,讓處於BFC內部的元素與外部的元素相互隔離,使內外元素的定位不會相互影響。

IE瀏覽器下為hasLayout,一般可以通過zoom:(除normal外任意值)來觸發,hasLayout是IE瀏覽器渲染引擎的一個內部組成部分。在IE瀏覽器中,一個元素要麼自己對自身的內容進行計算大小和組織,要麼依賴於父元素來計算尺寸和和組織內容。為了調節這兩個不同的概念,渲染引擎採用了hasLayout的屬性,屬性值可以為true或false。當一個元素的hasLayout屬性為true時,我們就說這個元素有一個布局(Layout)。當擁有布局后,它會負責對自己和可能的子孫元素進行尺寸計算和定位,而不是依賴於祖先元素來完成這些工作。

2.1 觸發條件

  • 根元素(<html>)
  • 浮動元素(元素的float不是none)
  • 絕對定位元素(元素的position為absolute或fixed)
  • 行內塊元素(元素的display為inline-block)
  • 表格單元格(元素的display為table-cell,HTML表格單元格默認為該值)
  • 表格標題(元素的display為table-caption,HTML表格標題默認為該值)
  • display值為flow-root的元素
  • overflow屬性的值不為visible
  • 彈性元素(display為flex或inline-flex元素的直接子元素)
  • 網格元素(display為grid或者inline-grid元素的直接子元素)

    2.2 布局規則

    普通文檔流布局規則:

  • 浮動的元素是不會被父級計算高度的
  • 非浮動元素會覆蓋浮動元素的位置
  • margin會傳遞給父級
  • 兩個相鄰元素上下margin會發生重疊

BFC布局規則:

  • 浮動的元素會被父級計算高度(父級觸發了BFC)
  • 非浮動元素不會覆蓋浮動元素的位置(非浮動元素觸發了BFC)
  • margin不會傳遞給父級(父級觸發了BFC)
  • 兩個相鄰元素上下margin不會發生重疊(給其中一個元素增加一個父級,並讓它的父級觸發BFC)

    2.3 應用

  • 防止margin重疊
  • 清除內部浮動(原理是父級計算高度時,浮動的子元素也會參与計算)
  • 自適應兩欄布局
  • 防止元素被浮動元素所覆蓋

    3. 層疊上下文

    層疊上下文(stacking context),是HTML中一個三維的概念。在CSS2.1規範中,每個盒模型的位置都是三維的,分別是平面畫布上的X軸,Y軸以及表示層疊的Z軸。一般情況下,元素在頁面上沿X軸Y軸平鋪,我們察覺不到它們在Z軸上的層疊關係。而一旦元素髮生堆疊,這時就能發現某個元素可能覆蓋了另一個元素或者被另一個元素覆蓋。

如果一個元素含有層疊上下文,我們就可以理解為這個元素在Z軸上就”高人一等”,最終表現就是它離屏幕觀察者更近。

你可以把層疊上下文理解為該元素當了官,而其他非層疊上下文元素則可以理解為普通群眾。凡是”當了官的元素”就比普通元素等級要高,也就是說元素在Z軸上更靠上,更靠近觀察者。

3.1 觸發條件

  • 根層疊上下文(<html>)
  • position屬性為非static值並設置z-index為具體數值
  • CSS3中的屬性也可以產生層疊上下文
    • flex
    • transform
    • opacity
    • filter
    • will-change
    • -webkit-overflow-scrolling

      3.2 層疊等級

      層疊等級(stacking level),又叫”層疊級別”或者”層疊水平”。

  • 在同一個層疊上下文中,它描述定義的是該層疊上下文中的層疊上下文元素在Z軸上的上下順序
  • 在其他普通元素中,它描述定義的是這些普通元素在Z軸上的上下順序

    注意:

    1. 普通元素的層疊等級優先由其所在的層疊上下文決定。
    2. 層疊等級的比較只有在當前層疊上下文元素中才有意義,不同層疊上下文中比較層疊等級是沒有意義的。

根據以上的層疊等級圖,我們在比較層疊等級時可以按照以下的思路來順序比較:

  • 首先判定兩個要比較的元素是否處於同一個層疊上下文中
  • 如果處於同一個層疊上下文中,則誰的層疊等級大,誰最靠上
  • 如果處於不同的層疊上下文中,則先比較他們所處的層疊上下文的層疊等級
  • 當兩個元素層疊等級相同,層疊順序相同時,在DOM結構中後面的元素層疊等級在前面元素之上

4. CSS3中新增的選擇器以及屬性

  • 屬性選擇器:
屬性選擇器 含義描述
E[attr^=”val”] 屬性attr的值以”val”開頭的元素
E[attr$=”val”] 屬性attr的值以”val”結尾的元素
E[attr*=”val”] 屬性attr的值包含“val”子字符串的元素
  • 結構偽類選擇器
選擇器 含義描述
E:root 匹配元素所在文檔的根元素,對於HTML文檔,根元素始終是<html>
E:nth-child(n) 匹配其父元素的第n個子元素,第一個編號為1
E:nth-last-child(n) 匹配其父元素的倒數第n個子元素,第一個編號為1
E:nth-of-type(n) 與:nth-child()作用類似,但是僅匹配使用同種標籤的元素
E:nth-last-of-type(n) 與:nth-last-child() 作用類似,但是僅匹配使用同種標籤的元素
E:last-child 匹配父元素的最後一個子元素,等同於:nth-last-child(1)
E:first-of-type 匹配父元素下使用同種標籤的第一個子元素,等同於:nth-of-type(1)
E:last-of-type 匹配父元素下使用同種標籤的最後一個子元素,等同於:nth-last-of-type(1)
E:only-child 匹配父元素下僅有的一個子元素,等同於:first-child:last-child或 :nth-child(1):nth-last-child(1)
E:only-of-type 匹配父元素下使用同種標籤的唯一一個子元素,等同於:first-of-type:last-of-type或 :nth-of-type(1):nth-last-of-type(1)
E:empty 匹配一個不包含任何子元素的元素,文本節點也被看作子元素
E:not(selector) 匹配不符合當前選擇器的任何元素
  • CSS3新增屬性
屬性 含義描述
transition 過渡效果
transform 變換效果(移動(translate)、縮放(scale)、旋轉(rotate)、傾斜(skew))
transform-origin 設置旋轉元素的基點位置
animation 動畫效果
border-color 為邊框設置多種顏色
border-radius 圓角邊框
box-shadow 邊框陰影
border-image 邊框圖片
background-size 規定背景圖片的尺寸
background-origin 規定背景圖片的定位區域
background-clip 規定背景圖片從什麼位置開始裁切
text-shadow 文本陰影
text-overflow 文本截斷
word-wrap 對長單詞進行拆分,並換行到下一行
opacity 不透明度
box-sizing 控制盒模型的組成模式
rgba 基於r,g,b三個顏色通道來設置顏色值,通過a來設置透明度

5. CSS3中transition和animation的屬性

1) transition(過渡動畫)

用法:transition: property duration timing-function delay
| 屬性 | 含義描述 |
| —- | —- |
| transition-property | 指定哪個CSS屬性需要應用到transition效果 |
| transition-duration | 指定transition效果的持續時間 |
| transition-timing-function | 指定transition效果的速度曲線 |
| transition-delay | 指定transition效果的延遲時間 |

2) animation(關鍵幀動畫)

用法:animation: name duration timing-function delay iteration-count direction fill-mode play-state
| 屬性 | 含義描述 |
| —- | —- |
| animation-name | 指定要綁定到選擇器的關鍵幀的名稱 |
| animation-duration | 指定動畫的持續時間 |
| animation-timing-function | 指定動畫的速度曲線 |
| animation-delay | 指定動畫的延遲時間 |
| animation-iteration-count | 指定動畫的播放次數 |
| animation-direction | 指定是否應該輪流反向播放動畫 |
| animation-fill-mode | 規定當動畫不播放時(當動畫完成時,或當動畫有一個延遲未開始播放時),要應用到元素的樣式 |
| animation-play-state | 指定動畫是否正在運行或已暫停 |

6. 清除浮動的方式以及各自的優缺點

  • 額外標籤法(在最後一個浮動元素的後面新加一個標籤如<div class="clear"></div>,並在其CSS樣式中設置clear: both;)

    優點:簡單,通俗易懂,寫少量代碼,兼容性好
    缺點:額外增加無語義html元素,代碼語義化差,後期維護成本大

  • 給父級設置高度

    優點:簡單,寫少量代碼,容易掌握
    缺點:不夠靈活,只適用於高度固定的布局

  • 觸發父級BFC(如給父元素設置overflow:hidden,特別注意的是:在IE6中還需要觸發hasLayout,例如給父元素設置zoom:1。原理是觸發父級BFC后,父元素在計算高度時,浮動的子元素也會參与計算)

    優點:簡單,代碼簡潔
    缺點:設置overflow:hidden容易造成不會自動換行導致超出的尺寸被隱藏掉,無法显示要溢出的元素

  • 使用after偽元素,常見的寫法如下:
 .clearfix::after {
    content: ".";
    display: block;
    height: 0;
    line-height: 0;
    clear: both;
    visibility:hidden;
    font-size: 0;
 }
 
 .clearfix {
    // 注意此處是為了兼容IE6和IE7瀏覽器,即觸發hasLayout
    zoom: 1;
 }

優點:符合閉合浮動思想,結構語義化正確
缺點:代碼量多,因為IE6-7下不支持after偽元素,需要額外寫zoom:1來觸發hasLayout

7. 居中布局的方式

水平居中:

  • 若是行內元素,則直接給其父元素設置text-align: center即可
  • 若是塊級元素,則直接給該元素設置margin: 0 auto即可
  • 若子元素包含浮動元素,則給父元素設置width:fit-content並且配合margin
.parent {
    width: -webkit-fit-content;
    width: -moz-fit-content;
    width: fit-content;
    margin: 0 auto;
}
  • 使用flex布局的方式,可以輕鬆實現水平居中,即使子元素中存在浮動元素也同樣適用
// flex 2012年版本寫法
.parent {
    display: flex;
    flex-direction: row;
    justify-content: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-orient: horizontal;
    box-pack: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    left: 50%;
    transform: translate(-50%, 0);
}
  • 使用絕對定位的方式,再配合負值的margin-left(此方法需要固定寬度)
.child {
    position: absolute;
    left: 50%;
    width: 200px; // 假定寬度為200px
    margin-left: -100px; // 負值的絕對值為寬度的一半
}
  • 使用絕對定位的方式,再配合left:0;right:0;margin:0 auto;(此方法需要固定寬度)
.child {
    position: absolute;
    left: 0;
    right: 0;
    margin: 0 auto;
    width: 200px; // 假定寬度為200px
}

垂直居中:

  • 若元素是單行文本,則直接給該元素設置line-height等於其父元素的高度
  • 若元素是行內塊級元素,可以配合使用display:inline-block;vertical-align:middle和一個偽元素來讓內容塊居中
.parent::after, .child {
    display: inline-block;
    vertical-align: middle;
}

.parent::after {
    content: "";
    height: 100%;
}
  • 使用vertical-align屬性並且配合使用display:table和display:table-cell來讓內容塊居中
.parent {
    display: table;
}

.child {
    display: table-cell;
    vertical-align: middle;
}
  • 使用flex布局的方式,可以輕鬆實現垂直居中,即使子元素中存在浮動元素也同樣適用
// flex 2012年版本寫法
.parent {
    display: flex;
    align-items: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-orient: vertical;
    box-pack: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    top: 50%;
    transform: translate(0, -50%);
}
  • 使用絕對定位的方式,再配合負值的margin-top(此方法需要固定高度)
.child {
    position: absolute;
    top: 50%;
    height: 200px; // 假定高度為200px
    margin-top: -100px; // 負值的絕對值為高度的一半
}
  • 使用絕對定位的方式,再配合top:0;bottom:0;margin:auto 0;(此方法需要固定高度)
.child {
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    height: 200px; // 假定高度為200px
}

水平垂直居中:

  • 使用flex布局的方式同樣可以輕鬆實現水平垂直居中
// flex 2012年版本寫法
.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-pack: center;
    box-align: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}
  • 使用絕對定位的方式,再配合使用負值的margin-top和負值的margin-left(此方法需要同時固定寬度和高度)
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-top: -50px; // 負值的絕對值為高度的一半
    margin-left: -100px; // 負值的絕對值為寬度的一半
    width: 200px; // 假定寬度為200px
    height: 100px; // 假定高度為100px
}

8. CSS的優先級和權重

選擇器(優先級從高到低) 示例 特殊性值
!important(重要性標識) div { color: #fff !important; } 無,但為了方便記憶,可將其表示為1,0,0,0,0
行內樣式 <div style="color: #fff;"></div> 1,0,0,0
id選擇器 #id 0,1,0,0
類,偽類和屬性選擇器 .content, :first-child, [type="text"] 0,0,1,0
標籤和偽元素選擇器 h1, ::after 0,0,0,1
通配符、子選擇器、相鄰選擇器 *, div > p, p + p 0,0,0,0
繼承 span { color: inherit; } 無
瀏覽器默認值 瀏覽器開發者工具右側的Styles面板中會显示user agent stylesheet字樣 無

9. 移動端1px物理像素邊框

我們知道,在移動端存在物理像素(physical pixel)和設備獨立像素(density-independent pixel)的概念。物理像素也稱為設備像素,它是显示設備中一個最微小的物理部件,每個像素可以根據操作系統設置自己的顏色和亮度。設備獨立像素也稱為密度無關像素,可以認為是計算機坐標系統中的一個點,這個點代表一個可以由程序使用的虛擬像素(比如CSS像素),然後由相關係統轉換為物理像素。根據物理像素和設備獨立像素也衍生出了設備像素比(device pixel ratio)的概念,簡稱為dpr,其定義了物理像素和設備獨立像素的對應關係,其計算公式為設備像素比 = 物理像素 / 設備獨立像素。因為視網膜(Retina)屏幕的出現,使得一個物理像素並不能和一個設備獨立像素完全對等,如下圖所示:

在上圖中,在普通屏幕下1個CSS像素對應1個物理像素,而在Retina屏幕下,1個CSS像素卻對應4個物理像素,即在Retina屏幕下會有不同的dpr值。為了追求在移動端網頁中更好的显示質量,因此我們需要做各種各樣的適配處理,最經典的莫過於1px物理像素邊框問題,我們需要根據移動端不同的dpr值來對邊框進行處理。在JavaScript中,可以通過window.devicePixelRatio來獲取當前設備的dpr,在CSS中,可以通過-webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio和-webkit-max-device-pixel-ratio來進行媒體查詢,從而針對不同的設備,來做一些樣式適配。這裏對於1px像素的邊框問題,給出一種最常見的寫法:

.border-1px {
    position: relative;
}

.border-1px::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 1px;
    background-color: #000;
    -webkit-transform: scaleY(.5);
    transform: scaleY(.5);
}

@media only screen and (-webkit-min-device-pixel-ratio: 2.0), (min-device-pixel-ratio: 2.0) {
    .border-1px::after {
        -webkit-transform: scaleY(.5);
        transform: scaleY(.5);
    }
}

@media only screen and (-webkit-min-device-pixel-ratio: 3.0), (min-device-pixel-ratio: 3.0) {
    .border-1px::after {
        -webkit-transform: scaleY(.33);
        transform: scaleY(.33);
    }
}

10. 實現三欄布局的方式有哪些

三欄布局,顧名思義就是分為左中右三個模塊進行布局,並且左右兩邊固定,中間模塊根據瀏覽器的窗口變化進行自適應,效果圖如下:

這裏給出四種實現三欄布局的方式:

  • 使用絕對定位的方式
.container {
    position: relative;
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.left {
    position: absolute;
    left: 0;
    top: 0;
    width: 150px;
    background: red;
}

.main {
    margin-left: 160px;
    margin-right: 110px;
    background: green;
}

.right {
    position: absolute;
    right: 0;
    top: 0;
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="left">左</div>
    <div class="main">中</div>
    <div class="right">右</div>
</div>

優點:方便快捷,簡單實用,不容易出現問題,而且還可以將<div class="main"></div>元素放到最前面,使得主要內容被優先加載。
缺點:元素脫離了文檔流,可能會造成元素的重疊。

  • 使用flex布局的方式
.container {
    display: flex;      
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.left {
    width: 150px;
    background: red;
}

.main {
    margin: 0 10px;
    flex: 1;
    background: green;
}

.right {
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="left">左</div>
    <div class="main">中</div>
    <div class="right">右</div>
</div>

優點:簡單實用,是現在比較流行的方案,特別是在移動端,大多數布局都採用的這種方式,是目前比較完美的一個。
缺點:需要考慮到瀏覽器的兼容性,根據不同的瀏覽器廠商需要添加相應的前綴。

  • 雙飛翼布局
.content {
    float: left;
    width: 100%;
}

.main,
.left,
.right {
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.main {
    margin-left: 160px;
    margin-right: 110px;
    background: green;
}

.left {
    float: left;
    margin-left: -100%;
    width: 150px;
    background: red;
}

.right {
    float: right;
    margin-left: -100px;
    width: 100px;
    background: blue;
}

<div class="content">
    <div class="main">中</div>
</div>
<div class="left">左</div>
<div class="right">右</div>

優點:比較經典的一種方式,通用性強,沒有兼容性問題,而且支持主要內容優先加載。
缺點:元素脫離了文檔流,要注意清除浮動,防止高度塌陷,同時額外增加了一層DOM結構,即增加了渲染樹生成的計算量。

  • 聖杯布局
.container {
    margin-left: 160px;
    margin-right: 110px;
}

.left,
.main,
.right {
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;    
}

.main {
    float: left;
    width: 100%;
    background: green;      
}

.left {
    position: relative;
    left: -160px;
    margin-left:  -100%;
    float: left;
    width: 150px;
    background: red;
}

.right {
    position: relative;
    right: -110px;
    margin-left:  -100px;
    float: left;
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="main">中</div>
    <div class="left">左</div>
    <div class="right">右</div>
</div>

優點:相比於雙飛翼布局,結構更加簡單,沒有多餘的DOM結構層,同樣支持主要內容優先加載。
缺點:元素同樣脫離了文檔流,要注意清除浮動,防止高度塌陷。

11. 實現等高布局的方式有哪些

等高布局,顧名思義就是在同一個父容器中,子元素高度相等的布局。從等高布局的實現方式來說,可以分為兩種,分別是偽等高和真等高。偽等高是指子元素的高度差依然存在,只是視覺上給人的感覺就是等高,真等高是指子元素的高度真實相等。效果圖如下:

這裏給出五種實現等高布局的方式:

偽等高:

  • 使用padding-bottom和負的margin-bottom來實現
.container {
    position: relative;
    overflow: hidden;
}
    
.left,
.main,
.right {
    padding-bottom: 100%;
    margin-bottom: -100%;
    float: left;
    color: #fff;
}

.left {
    width: 20%;
    background: red;
}

.main {
    width: 60%;
    background: green;
}

.right {
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>

真等高:

  • 使用flex布局的方式
.container {
    display: flex;
}

.left,
.main,
.right {
    flex: 1;
    color: #fff;
}

.left {
    background: red;
}

.main {
    background: green;
}

.right {
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用絕對定位的方式
.container {
  position: relative;
  height: 200px;
}

.left,
.main,
.right {
    position: absolute;
    top: 0;
    bottom: 0;
    color: #fff;
}

.left {
    left: 0;
    width: 20%;
    background: red;
}

.main {
    left: 20%;
    right: 20%;
    background: green;
}

.right {
    right: 0;
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用table布局的方式
.container {
    width: 100%;
    display: table;
}

.left,
.main,
.right {
    display: table-cell;
    color: #fff;
}

.left {
    width: 20%;
    background: red;
}

.main {
    width: 60%;
    background: green;
}

.right {
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用grid網格布局的方式
.container {
    display: grid;
    width: 100%;
    grid-template-columns: 1fr 1fr 1fr;
    color: #fff;
}

.left {
    background: red;
}

.main {
    background: green;
}

.right {
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>

12. CSS實現三角形的原理

工作中我們經常會遇到需要三角形圖標的應用場景,例如內容展開收起、左右箭頭點擊切換輪播,點擊某條列表數據查看詳情等。三角形圖標的應用範圍之廣,使得我們有必要了解一下它的實現原理。
1) 首先我們來實現一個最基礎的邊框效果

.content {
    width: 50px;
    height: 50px;
    border: 2px solid;
    border-color:#ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

2) 然後我們嘗試將border值放大10倍

.content {
    width: 50px;
    height: 50px;
    border: 20px solid;
    border-color: #ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

上圖中我們可以很清楚地看到,在繪製border的時候並不是矩形區域,而是梯形區域,那麼此時如果我們將width和height值設置為0,看會發生什麼:

.content {
    width: 0;
    height: 0;
    border: 20px solid;
    border-color: #ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

此時會看到一個由四個三角形拼裝而成的矩形區域,即由上下左右四個邊框組合而成。因此不難想象,如果我們想得到某一個方向的三角形,我們只需要讓其他方向的邊框不可見就行了,例如我們想得到一個朝左的三角形:

.content {
    width: 0;
    height: 0;
    border: 20px solid;
    border-color: transparent #3366ff transparent transparent;
}

效果如下:

這樣就得到了一個很完美的三角形圖標,是不是很簡單?

13. link與@import的區別

  • 從屬關係區別

    @import是CSS提供的語法規則,只有導入樣式表的作用;link是HTML提供的標籤,不僅可以加載 CSS 文件,還可以定義 RSS,Rel連接屬性,設置瀏覽器資源提示符preload、prefetch等。

  • 加載順序區別

    HTML文檔在解析的過程當中,如果遇到link標籤,則會立即發起獲取CSS文件資源的請求;@import引入的CSS將在頁面加載完畢后才會被加載。

  • 兼容性區別

    @import是CSS2.1才有的語法,因此需要IE5以上才能識別;link標籤作為HTML元素,不存在兼容性問題。

  • DOM可控性區別

    link標籤可以通過JS來動態引入,而@import無法通過JS來插入樣式

const loadStyle = (url) => {
    const link = document.createElement('link');
    link.setAttribute('type', 'text/css');
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('href', url);
    
    document.head.appendChild(link);
}

14. 瀏覽器是怎樣解析CSS選擇器的

CSS選擇器的解析是從右向左解析的。若從左向右地匹配,發現不符合規則,需要進行回溯,會損失很多性能。若從右向左匹配,先找到所有的最右節點,對於每一個節點,向上尋找其父節點直到找到根元素或滿足條件的匹配規則,則結束這個分支的遍歷。兩種匹配規則的性能差別很大,是因為從右向左的匹配在第一步就篩選掉了大量的不符合條件的最右節點(恭弘=叶 恭弘子節點),而從左向右的匹配規則的性能都浪費在了失敗的查找上面。而在CSS解析完畢后,需要將解析的結果與DOM Tree的內容一起進行分析建立一棵 Render Tree,最終用來進行繪圖。在建立Render Tree時瀏覽器就要為每個DOM Tree中的元素根據CSS的解析結果(Style Rules)來確定生成怎樣的Render Tree。

15. CSS的性能優化方案

  • 層級盡量扁平,避免嵌套過多層級的選擇器;
  • 使用特定的選擇器,避免解析器過多層級的查找;
  • 減少使用通配符與屬性選擇器;
  • 減少不必要的多餘屬性;
  • 避免使用!important標識,可以選擇其他選擇器;
  • 實現動畫時優先使用CSS3的動畫屬性,動畫時脫離文檔流,開啟硬件加速;
  • 使用link標籤代替@import;
  • 將渲染首屏內容所需的關鍵CSS內聯到HTML中;
  • 使用資源預加載指令preload讓瀏覽器提前加載CSS資源並緩存;
  • 使用Gulp,Webpack等構建工具對CSS文件進行壓縮處理;

推薦閱讀

交流

終於接近尾聲了,居然花費掉了我一整個周末的時間,不過這篇主要是先總結一下CSS相關的知識點,當然還有很多地方沒有總結到,只是列出了個人覺得比較容易考察的點,如果你有其他補充的,歡迎在下方留言區討論哦,也歡迎關注我的公眾號[前端之境],關注后我可以拉你加入微信前端交流群,我們一起互相交流學習,共同進步。
後續會陸續總結出JS方面、瀏覽器視角、算法基礎和框架方面的內容,希望你能夠喜歡!

文章已同步更新至,若覺文章尚可,歡迎前往star!

你的一個點贊,值得讓我付出更多的努力!

逆境中成長,只有不斷地學習,才能成為更好的自己,與君共勉!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

Rio手把手教學:如何打造容器化應用程序的一站式部署體驗

11月19日,業界應用最為廣泛的Kubernetes管理平台創建者Rancher Labs(以下簡稱Rancher)宣布Rio發布了beta版本,這是基於Kubernetes的應用程序部署引擎。它於今年5月份推出,現在最新的版本是v0.6.0。Rio結合了多種雲原生技術,從而簡化了將代碼從測試環境發布到生產環境的流程,同時保證了強大而安全的代碼體驗。

什麼是Rio?

下圖是Rio的架構:

Rio採用了諸如Kubernetes、knative、linkerd、cert-manager、buildkit以及gloo等技術,並將它們結合起來為用戶提供一個完整的應用程序部署環境。

Rio具有以下功能:

  1. 從源代碼構建代碼,並將其部署到Kubernetes集群

  2. 自動為應用程序創建DNS記錄,並使用Let’s Encrypt的TLS證書保護這些端點

  3. 基於QPS以及工作負載的指標自動擴縮容

  4. 支持金絲雀發布、藍綠髮布以及A/B部署

  5. 支持通過服務網格路由流量

  6. 支持縮容至零的serverless工作負載

  7. Git觸發的部署

Rancher的產品生態

Rio屬於Rancher整套產品生態的一部分,這些產品支持從操作系統到應用程序的應用程序部署和容器運維。當Rio和諸如Rancher 2.3、k3s和RKE等產品結合使用時,企業可以獲得完整的部署和管理應用程序及容器的體驗。

深入了解Rio

要了解Rio如何實現上述功能,我們來深入了解一些概念以及工作原理。

安裝Rio

前期準備

  • Kubernetes版本在1.15以上的Kubernetes集群

  • 為集群配置的kubeconfig(即上下文是你希望將Rio安裝到的集群)

  • 在$PATH中安裝的Rio CLI工具,可參閱以下鏈接,了解如何安裝CLI:
    https://github.com/rancher/rio/blob/master/README.md

安裝

使用安裝好的Rio CLI工具,調用rio install。你可能需要考慮以下情況:

ip-address:節點的IP地址的逗號分隔列表。你可以在以下情況使用:

  • 你不使用(或不能使用)layer-4的負載均衡器

  • 你的節點IP不是你希望流量到達的IP地址(例如,你使用有公共IP的EC2實例)

服 務

在Rio中,service是一個基本的執行單位。從Git倉庫或容器鏡像實例化之後,一個service由單個容器以及服務網格的關聯sidecar組成(默認啟用)。例如,運行使用Golang構建的一個簡單的“hello world”應用程序。

rio run https://github.com/ebauman/rio-demo

或者運行容器鏡像版本:

rio run ebauman/demo-rio:v1

還有其他選項也可以傳遞給rio run,如需要公開的任意端口(-p 80:8080/http),或者自動擴縮的配置(--scale 1-10)。你可以通過這一命令rio help run,查看所有可傳遞的選項。

想要查看你正在運行的服務,請執行rio ps:

$ rio ps
NAME            IMAGE                               ENDPOINT
demo-service    default-demo-service-4dqdw:61825    https://demo-service...

每次你運行一個新的服務,Rio將會為這一服務生成一個全局性的端點:

$ rio endpoints
NAME           ENDPOINTS
demo-service   https://demo-service-default.op0kj0.on-rio.io:30282

請注意,此端點不包括版本——它指向由一個common name標識的服務,並且流量根據服務的權重進行路由。

自動DNS&TLS

默認情況下,所有Rio集群都將為自己創建一個on-rio.io主機名,並以隨機字符串開頭(如lkjsdf.on-rio.io)。該域名成為通配符域名,它的記錄解析到集群的網關。如果使用NodePort服務,則該網關可以是layer-4負載均衡器,或者是節點本身。

除了創建這個通配符域名,Rio還會使用Let’s Encrypt為這個域名生成一個通配符證書。這會允許自動加密任何HTTP工作負載,而無需用戶進行配置。要啟動此功能,請傳遞-p參數,將http指定為協議,例如:

rio run -p 80:8080/http ...

自動擴縮容

Rio可以根據每秒所查詢到的指標自動擴縮服務。為了啟用這一特性,傳遞--scale 1-10作為參數到rio run,例如:

rio run -p 80:8080/http -n demo-service --scale 1-10 ebauman/rio-demo:v1

執行這個命令將會構建ebauman/rio-demo並且部署它。如果我們使用一個工具來添加負載到端點,我們就能夠觀察到自動擴縮容。為了證明這一點,我們需要使用HTTP端點(而不是HTTPS),因為我們使用的工具不支持TLS:

$ rio inspect demo-service
<snipped>
endpoints:
- https://demo-service-v0-default.op0kj0.on-rio.io:30282
- http://demo-service-v0-default.op0kj0.on-rio.io:31976
<snipped>

rio inspect除了端點之外還會显示其他信息,但我們目前所需要的是端點信息。使用HTTP端點以及HTTP基準測試工具rakyll / hey,我們可以添加綜合負載:

hey -n 10000 http://demo-service-v0-default.op0kj0.on-rio.io:31976

這將會發送10000個請求到HTTP端點,Rio將會提高QPS並適當擴大規模,執行另一個rio ps將會展示已經擴大的規模:

$ rio ps
NAME            ...     SCALE       WEIGHT
demo-service    ...     2/5 (40%)   100%

分階段發布、金絲雀部署以及權重

注意

對於每個服務,都會創建一個全局端點,該端點將根據基礎服務的權重路由流量。

Rio可以先交付新的服務版本,然後再推廣到生產環境。分階段發布一個新的版本十分簡單:

rio stage --image ebauman/rio-demo:v2 demo-service v2

這一命令使用版本v2,分階段發布demo-service的新版本,並且使用容器鏡像ebauman/rio-demo:v2。我們通過執行rio ps這一命令,可以看到新階段的發布:

$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  0%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  100%

請注意,新服務的端點具有v2的新增功能,因此即使權重設置為0%,訪問此端點仍將帶你進入服務的v2。這可以讓你能夠在向其發送流量之前驗證服務的運行情況。

說到發送流量:

$ rio weight demo-service@v2=5%
$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  5%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  95%

使用rio weight命令,我們現在將發送我們5%的流量(從全局的服務端點)到新版本。當我們覺得demo-service的v2性能感到滿意之後,我們可以將其提升到100%:

$ rio promote --duration 60s demo-service@v2
demo-service@v2 promoted

超過60秒之後,我們的demo-service@v2服務將會逐漸提升到接收100%的流量。在這一過程中任意端點上,我們可以執行rio ps,並且查看進程:

$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  34%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  66%

路由(Routing)

Rio可以根據主機名、路徑、方法、標頭和cookie的任意組合將流量路由到端點。Rio還支持鏡像流量、注入故障,配置retry邏輯和超時。

創建一個路由器

為了開始制定路由決策,我們必須首先創建一個路由器。路由器代表一個主機名和一組規則,這些規則確定發送到主機名的流量如何在Rio集群內進行路由。你想要要定義路由器,需要執行rio router add。例如,要創建一個在默認測試時接收流量並將其發送到demo-service的路由器,請使用以下命令:

rio route add testing to demo-service

這將創建以下路由器:

$ rio routers
NAME             URL                            OPTS    ACTION      TARGET
router/testing   https://testing-default.0pjk...        to          demo-service,port=80

發送到https://testing-default…的流量將通過端口80轉發到demo-service。

請注意,此處創建的路由為testing-default. 。Rio將始終使用命名空間資源,因此在這種情況下,主機名測試已在默認命名空間中進行了命名。要在其他命名空間中創建路由器,請將 -n <namespace>傳遞給rio命令:

rio -n <namespace> route add ...

基於路徑的路由

為了定義一個基於路徑的路由,當調用rio route add時,指定一個主機名加上一個路徑。這可以是新路由器,也可以是現有路由器。

$ rio route add testing/old to demo-service@v1

以上命令可以創建一個基於路徑的路由,它會在https://testing-default. /old接收流量,並且轉發流量到 demo-service@v1服務。

標頭和基於方法的路由

Rio支持基於HTTP標頭和HTTP verbs的值做出的路由策略。如果你想要創建基於特定標頭路由的規則,請在rio route add命令中指定標頭:

$ rio route add --header X-Header=SomeValue testing to demo-service

以上命令將創建一個路由規則,它可以使用一個X-Header的HTTP標頭和SomeValue的值將流量轉發到demo-service。類似地,你可以為HTTP方法定義規則:

$ rio route add --method POST testing to demo-service

故障注入

Rio路由有一項有趣的功能是能夠將故障注入響應中。通過定義故障路由規則,你可以設置具有指定延遲和HTTP代碼的失敗流量百分比:

$ rio route add --fault-httpcode 502 --fault-delay-milli-seconds 1000 --fault-percentage 75 testing to demo-service

其他路由選項

Rio支持按照權重分配流量、為失敗的請求重試邏輯、重定向到其他服務、定義超時以及添加重寫規則。要查看這些選項,請參閱以下鏈接:

https://github.com/rancher/rio

自動構建

將git倉庫傳遞給rio run將指示Rio在提交到受監控的branch(默認值:master)之後構建代碼。對於Github倉庫,你可以通過Github webhooks啟動此功能。對於任何其他git repo,或者你不想使用webhooks,Rio都會提供一項“gitwatcher”服務,該服務會定期檢查您的倉庫中是否有更改。

Rio還可以根據受監控的branch的拉取請求構建代碼。如果你想要進行配置,請將--build-pr傳遞到rio run。還有其他配置這一功能的選項,包括傳遞Dockerfile的名稱、自定義構建的鏡像名稱以及將鏡像推送到指定的鏡像倉庫。

堆棧和Riofile

Rio使用稱為Riofile的docker-compose-style manifest定義資源

configs:
  conf:
    index.html: |-
      <!DOCTYPE html>
      <html>
      <body>

      <h1>Hello World</h1>

      </body>
      </html>
services:
  nginx:
    image: nginx
    ports:
    - 80/http
    configs:
    - conf/index.html:/usr/share/nginx/html/index.html

Riofile定義了一個簡單的nginx Hello World網頁所有必要的組件。通過rio up部署它,會創建一個Stack(堆棧),它是Riofile定義的資源的集合。

Riofile具有許多功能,例如觀察Git庫中的更改以及使用Golang模板進行模板化。

其他Rio組件

Rio還有許多功能,例如configs、secrets以及基於角色訪問控制(RBAC)。詳情可參閱:

https://rio.io/

Rio可視化

Rio Dashboard

Rio的beta版本包括了一個全新的儀錶盤,使得Rio組件可視化。要訪問此儀錶盤,請執行命令:rio dashboard。在有GUI和默認瀏覽器的操作系統上,Rio將自動打開瀏覽器並加載儀錶盤。

你可以使用儀錶盤來創建和編輯堆棧、服務、路由等。此外,可以直接查看和編輯用於各種組件技術(Linkerd、gloo等)的對象,儘管不建議這樣做。儀錶盤目前處於開發的早期階段,因此某些功能的可視化(如自動縮放和服務網格)尚不可用。

Linkerd

作為Rio的默認服務網格,Linked附帶了一個儀錶盤作為產品的一部分。該儀錶盤可以通過執行rio linkerd來使用,它將代理本地本地主機流量到linkerd儀錶盤(不會在外部公開)。與Rio儀錶盤類似,有GUI和默認瀏覽器的操作系統上,Rio將自動打開瀏覽器並加載儀錶盤:

Linkerd儀錶盤显示了Rio集群的網格配置、流量和網格組件。Linkerd提供了Rio路由的某些功能組件,因此這些配置可能會显示在此儀錶盤上。還有一些工具可用於測試和調試網格配置和流量。

結 論

Rio為用戶提供許多功能,是一款強大的應用程序部署引擎。這些組件可以在部署應用程序時為開發人員提供強大的功能,使流程穩定而安全,同時輕鬆又有趣。在Rancher產品生態中,Rio提供了企業部署和管理應用程序和容器的強大功能。

如果你想了解Rio的更多信息,歡迎訪問Rio主頁或Github主頁:

https://rio.io

https://github.com/rancher/rio

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

從壹開始 [ Design Pattern ] 之二 ║ 單例模式 與 Singleton

前言

這一篇來源我的公眾號,如果你沒看過,正好直接看看,如果看過了也可以再看看,我稍微修改了一些內容,今天講解的內容如下:

 

 

 

 

 

 

 

一、什麼是單例模式

 

【單例模式】,英文名稱:Singleton Pattern,這個模式很簡單,一個類型只需要一個實例,他是屬於創建類型的一種常用的軟件設計模式。通過單例模式的方法創建的類在當前進程中只有一個實例(根據需要,也有可能一個線程中屬於單例,如:僅線程上下文內使用同一個實例)。

1、單例類只能有一個實例。

2、單例類必須自己創建自己的唯一實例。

3、單例類必須給所有其他對象提供這一實例。

 

那咱們大概知道了,其實說白了,就是我們整個項目周期內,只會有一個實例,當項目停止的時候,實例銷毀,當重新啟動的時候,我們的實例又會產品。

上文中說到了一個名詞【創建類型】的設計模式,那什麼是創建類型的設計模式呢?

創建型(Creational)模式:負責對象創建,我們使用這個模式,就是為了創建我們需要的對象實例的。

 

那除了創建型還有其他兩種類型的模式:

結構型(Structural)模式:處理類與對象間的組合

行為型(Behavioral)模式:類與對象交互中的職責分

這兩種設計模式,以後會慢慢說到,這裏先按下不表。

咱們就重點從0開始分析分析如何創建一個單例模式的對象實例。

 

二、如何創建單例模式

 

實現單例模式有很多方法:從“懶漢式”到“餓漢式”,最後“雙檢鎖”模式,這裏咱們就慢慢的,從一步一步的開始講解如何創建單例。

 

1、正常的思考邏輯順序

 

既然要創建單一的實例,那我們首先需要學會如何去創建一個實例,這個很簡單,相信每個人都會創建實例,就比如說這樣的:

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    public WeatherForecast()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}


 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = new WeatherForecast();
     return weather;
 }

 

我們每次訪問的時候,時間都是會變化,所以我們的實例也是一直在創建,在變化:

 

 

相信每個人都能看到這個代碼是什麼意思,不多說,直接往下走,我們知道,單例模式的核心目的就是:

必須保證這個實例在整個系統的運行周期內是唯一的,這樣可以保證中間不會出現問題。

 

那好,我們改進改進,不是說要唯一一個么,好說!我直接返回不就行了:

 

 /// <summary>
 /// 定義一個天氣類
 /// </summary>
 public class WeatherForecast
 {
     // 定義一個靜態變量來保存類的唯一實例
     private static WeatherForecast uniqueInstance;

     // 定義私有構造函數,使外界不能創建該類實例
     private WeatherForecast()
     {
         Date = DateTime.Now;
     }
     /// <summary>
     /// 靜態方法,來返回唯一實例
     /// 如果存在,則返回
     /// </summary>
     /// <returns></returns>
     public static WeatherForecast GetInstance()
     {
         // 如果類的實例不存在則創建,否則直接返回
         // 其實嚴格意義上來說,這個不屬於【單例】
         if (uniqueInstance == null)
         {
             uniqueInstance = new WeatherForecast();
         }
         return uniqueInstance;
     }
     public DateTime Date { get; set; }public int TemperatureC { get; set; }
     public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
     public string Summary { get; set; }
 }

 

 

然後我們修改一下調用方法,因為我們的默認構造函數已經私有化了,不允許再創建實例了,所以我們直接這麼調用:

[HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = WeatherForecast.GetInstance();
     return weather;
 }

 

最後來看看效果:

 

 

這個時候,我們可以看到,時間已經不發生變化了,也就是說我們的實例是唯一的了,大功告成!是不是很開心!

 

但是,別著急,問題來了,我們目前是單線程的,所以只有一個,那如果多線程呢,如果多個線程同時訪問,會不會也會正常呢?

這裏我們做一個測試,我們在項目啟動的時候,用多線程去調用:

 

 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     //WeatherForecast weather = WeatherForecast.GetInstance();

     // 多線程去調用
     for (int i = 0; i < 3; i++)
     {
         var th = new Thread(
         new ParameterizedThreadStart((state) =>
         {
             WeatherForecast.GetInstance();
         })
         );
         th.Start(i);
     }
     return null;
 }

 

然後我們看看效果是怎樣的,按照我們的思路,應該是只會走一遍構造函數,其實不是:

 

 

 

 

 

 

3個線程在第一次訪問GetInstance方法時,同時判斷(uniqueInstance ==null)這個條件時都返回真,然後都去創建了實例,這個肯定是不對的。那怎麼辦呢,只要讓GetInstance方法只運行一個線程運行就好了,我們可以加一個鎖來控制他,代碼如下:

public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        lock (locker)
        {
            // 如果類的實例不存在則創建,否則直接返回
            if (uniqueInstance == null)
            {
                uniqueInstance = new WeatherForecast();
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; }
}

 

這個時候,我們再併發測試,發現已經都一樣了,這樣就達到了我們想要的效果,但是這樣真的是最完美的么,其實不是的,因為我們加鎖,只是第一次判斷是否為空,如果創建好了以後,以後就不用去管這個 lock 鎖了,我們只關心的是 uniqueInstance 是否為空,那我們再完善一下:

 

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        if (uniqueInstance == null)
        {
            lock (locker)
            {
                // 如果類的實例不存在則創建,否則直接返回
                if (uniqueInstance == null)
                {
                    uniqueInstance = new WeatherForecast();
                }
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}

 

這樣才最終的完美實現我們的單例模式!搞定。

 

2、幽靈事件:指令重排

當然,如果你看完了上邊的那四步已經可以出師了,平時我們就是這麼使用的,也是這麼想的,但是真的就是萬無一失么,有一個 JAVA 的朋友提出了這個問題,C# 中我沒有聽說過,是我孤陋寡聞了么:

單例模式的幽靈事件,時令重排會偶爾導致單例模式失效。

 

是不是聽起來感覺很高大上,而不知所云,沒關係,咱們平時用不到,但是可以了解了解:

為何要指令重排?       

指令重排是指的 volatile,現在的CPU一般採用流水線來執行指令。一個指令的執行被分成:取指、譯碼、訪存、執行、寫回、等若干個階段。然後,多條指令可以同時存在於流水線中,同時被執行。
指令流水線並不是串行的,並不會因為一個耗時很長的指令在“執行”階段呆很長時間,而導致後續的指令都卡在“執行”之前的階段上。
相反,流水線是并行的,多個指令可以同時處於同一個階段,只要CPU內部相應的處理部件未被佔滿即可。比如說CPU有一個加法器和一個除法器,那麼一條加法指令和一條除法指令就可能同時處於“執行”階段, 而兩條加法指令在“執行”階段就只能串行工作。
相比於串行+阻塞的方式,流水線像這樣并行的工作,效率是非常高的。

然而,這樣一來,亂序可能就產生了。比如一條加法指令原本出現在一條除法指令的後面,但是由於除法的執行時間很長,在它執行完之前,加法可能先執行完了。再比如兩條訪存指令,可能由於第二條指令命中了cache而導致它先於第一條指令完成。
一般情況下,指令亂序並不是CPU在執行指令之前刻意去調整順序。CPU總是順序的去內存裏面取指令,然後將其順序的放入指令流水線。但是指令執行時的各種條件,指令與指令之間的相互影響,可能導致順序放入流水線的指令,最終亂序執行完成。這就是所謂的“順序流入,亂序流出”。

 

這個是從網上摘錄的,大概意思看看就行,理解雙檢鎖失效原因有兩個重點

1、編譯器的寫操作重排問題.
例 : B b = new B();

上面這一句並不是原子性的操作,一部分是new一個B對象,一部分是將new出來的對象賦值給b.

直覺來說我們可能認為是先構造對象再賦值.但是很遺憾,這個順序並不是固定的.再編譯器的重排作用下,可能會出現先賦值再構造對象的情況.

2、結合上下文,結合使用情景.

理解了1中的寫操作重排以後,我卡住了一下.因為我真不知道這種重排到底會帶來什麼影響.實際上是因為我看代碼看的不夠仔細,沒有意識到使用場景.雙檢鎖的一種常見使用場景就是在單例模式下初始化一個單例並返回,然後調用初始化方法的方法體內使用初始化完成的單例對象.

 

三、Singleton = 單例 ?

 上邊我們說了很多,也介紹了很多單例的原理和步驟,那這裏問題來了,我們在學習依賴注入的時候,用到的 Singleton 的單例注入,是不是和上邊說的一回事兒呢,這裏咱們直接多多線程測試一下就行:

 

/// <summary>
/// 定義一個心情類
/// </summary>
public class Feeling
{
    public Feeling()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
}


 // 單例注入
 services.AddSingleton<Feeling>();


[HttpGet]
public WeatherForecast Get()
{

    // 多線程去調用
    for (int i = 0; i < 3; i++)
    {
        var th = new Thread(
        new ParameterizedThreadStart((state) =>
        {
            //WeatherForecast.GetInstance();
            
            // 此刻的心情
            Feeling feeling = new Feeling();
            Console.WriteLine(feeling.Date);
        })
        );
        th.Start(i);
    }
    return null;
}

 

測試的結果,情理之中,也是意料之外:

 

 

竟然和我們上邊說的是一樣的, 
Singleton是一種懶漢模式 的單例, 因為結論可以看出,有時候我們使用單例模式,並不是寫一個 Sigleton 就能滿足的。    

四、單例模式的優缺點

 

        【優】、單例模式的優點:

             (1)、保證唯一性:防止其他對象實例化,保證實例的唯一性;

             (2)、全局性:定義好數據后,可以再整個項目種的任何地方使用當前實例,以及數據;

        【劣】、單例模式的缺點: 

             (1)、內存常駐:因為單例的生命周期最長,存在整個開發系統內,如果一直添加數據,或者是常駐的話,會造成一定的內存消耗。

 

以下內容來自百度百科:

優點

一、實例控制 單例模式會阻止其他對象實例化其自己的單例對象的副本,從而確保所有對象都訪問唯一實例。
二、靈活性 因為類控制了實例化過程,所以類可以靈活更改實例化過程。  

缺點

一、開銷 雖然數量很少,但如果每次對象請求引用時都要檢查是否存在類的實例,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題。
二、可能的開發混淆 使用單例對象(尤其在類庫中定義的對象)時,開發人員必須記住自己不能使用
new關鍵字實例化對象。因為可能無法訪問庫源代碼,因此應用程序開發人員可能會意外發現自己無法直接實例化此類。
三、對象生存期 不能解決刪除單個對象的問題。在提供內存管理的語言中(例如基於.NET Framework的語言),只有單例類能夠導致實例被取消分配,因為它包含對該實例的私有引用。在某些語言中(如 C++),其他類可以刪除對象實例,但這樣會導致單例類中出現懸浮引用。

 

五、示例代碼

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

如何打造一款m3u8視頻爬蟲

0.前言

m3u8是一種很常見的網頁視頻播放器的視頻源,比如說中國大學MOOC中課程就是使用了該種視頻格式。

隨便打開一門課程,就可以發現在網絡請求中存在一個m3u8的文件,在preview中預覽,它並不像我們想象中是亂碼的視頻流。

裏面是一個列表,有一堆ts結尾的文件名,每個下面還跟了一個EXTINF的字段,好像是時間,在我們播放視頻時,網絡請求中會不斷出現請求ts的內容。

隨便打開一個ts文件,它的內容卻是如圖視頻流一般亂碼的。

說到這裏,你可能有猜測了,m3u8並不是視頻流的文件,而有可能是組織ts文件的規範,EXTINF代表播放每多少秒去請求下一片ts流。

這種邊看邊加載的方法無疑可以減少我們的網絡負荷。

要用爬蟲爬取這類視頻的方法也很簡單,我們只需要獲得m3u8文件,就可以得到視頻的ts地址了,將所有ts請求下來之後進行合併,就可以得到視頻文件了。

不過要提的一點是,很多視頻網站會對他們的ts進行加密,我們下載下來合併之後可能視頻能看,但是播放器放着放着就卡住了,然後之後黑屏畫面。

1.編碼部分

我們先根據m3u8來判斷一下創建咋樣一個代表M3U8視頻對象的類。

我們首先需要定義一個list,來存放這個m3u8視頻下所有的ts文件,也就是後面說到的TS類。

這裏提一點,m3u8裏面的ts的路徑一般對路徑,會和m3u8在同一文件夾,我們代碼中也是這麼認為了,但是難免有些網站會單獨存放m3u8和ts文件,如果遇到這種情況,修改一下代碼即可。

有了ts的名稱,我們還需要URL的前綴,也就是圖中紫色劃線部分,也就是basepath。

此外,我們還需要一個TS對象。

這個對象中存儲TS文件名稱以及時間EXTINF。

定義完實體類,就需要編寫下載視頻的過程了。

首先需要請求到m3u8的文件,此處使用Java的HttpURLConnection來請求獲取,其它語言類似,只需要請求到文件即可。

請求到了m3u8的文本內容,我們還需要解析它 ,從中得到ts的名稱。

得到了M3U8視頻對象之後,我們就可以遍歷請求它的list中TS對象的名稱屬性來下載ts文件了。

這麼多ts文件如果我們在單線程中遍歷請求,會很耗費時間,Java給我們提供了Stream,其中parallel可以讓我們併發去遍歷集合,效率會提升不少。

依舊是使用HttpURLConnection來做請求,不過最好本次設置超時時間。

這樣就可以請求到所有ts文件了。

最後要做的就是合併這些ts文件成為一個MP4文件。

對於未加密的正常ts文件,我們只需要按照編號順序直接拼接即可。

這樣就算是完成了M3U8視頻抓取了。

2.打包使用

下載地址:

在命令行中java -jar m3u8-down.jar [m3u8地址],會显示報錯信息。

也可以直接m3u8-down.jar [m3u8地址],不會显示保存信息,會在後台執行。

最終會在同目錄下生成一個output.mp4的文件,temp文件可以刪除。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

.NET Core 3.0中用 Code-First 方式創建 gRPC 服務與客戶端

.NET Core love gRPC

千呼萬喚的 .NET Core 3.0 終於在 9 月份正式發布,在它的眾多新特性中,除了性能得到了大大提高,比較受關注的應該是 ASP.NET Core 3.0 對 gRPC 的集成了。
它的源碼託管在 grpc-dotnet 這個 Github 庫中,由微軟 .NET 團隊與谷歌 gRPC 團隊共同維護.

.NET Core 對 gRPC 的支持在 grpc 官方倉庫早已有實現(grpc/csharp),但服務端沒有很好地與 ASP.NET Core 集成,使用起來還需要自己進行一些集成擴展。
而 ASP.NET Core 3.0 新增了 gRPC 服務的託管功能,能讓 gRPC 與 ASP.NET Core 框架本身的特性很好地結合,如日誌、依賴注入、身份認證和授權,並由 Kestrel 服務器提供 HTTP/2 鏈接,性能上得到充分保障。

推薦把項目中已有的 RPC 框架或者內部服務間 REST 調用都遷移到 gRPC 上,因為它已經是雲原生應用的標準 RPC 框架,在整個 CNCF 主導下的雲原生應用開發生態里 gRpc 有着舉足輕重的地位。

對於 gRPC 的使用方式,前段時間已經有其他大神寫的幾篇文章了,這裏就不再贅述了。
本文主要介紹的是區別於標準使用規範的,但對.NET 應用更加友好的使用方式,最後會提供源碼來展示。

作為對比,還是要列一下標準的使用步驟:

  1. 定義 proto 文件,包含服務、方法、消息對象的定義
  2. 引入 Grpc.Tools Nuget 包並添加指定 proto 路徑和生成模式
  3. 生成項目,得到服務端的抽象類或客戶端的調用客戶端組件
  4. 實現服務端抽象類,並在 ASP.NET Core 註冊這個服務的路由端點
  5. DI 註冊 gRPC 服務。
  6. 客戶端用 Grpc.Net.ClientFactory Nuget 包進行統一配置和依賴注入

.NET Core 對 gRPC 的大力支持使開發者開發效率大大提高,入門難度也減少了許多,完全可以成為跟 WebApi 等一樣的 .NET Core 技術棧的標配。

proto 在單一語言系統架構中的局限性

使用 proto 文件的好處是多語言支持,同一份 proto 可以生成各種語言的服務和客戶端,可以讓用不同語言開發的微服務直接互相遠程調用。但 proto 文件作為不同服務間的契約,不可以經常修改,否則就會對使用了它的服務造成不同程度的影響,因此對 proto 文件的版本控制需要得到重視。

另外,我們的應用程序還不應該與 gRPC 耦合,否則就會導致系統架構被這些實現細節所綁架。直接依賴 proto 文件和由它生成的代碼,就是對 gRPC 的強耦合。

例如,當應用程序在演進的過程中,複雜度還未達到完全部署隔離的必要時,為了避免因“完全邊界”引入的部署運維複雜性,又能預留隔離的可能性,需要有一層接口層作為“不完全邊界”。

又比如,目前在 windows 系統的 iis 上還不支持 grpc-dotnet,當有 windows 上的應用程序需要使用 RPC,就需要換成 REST 的實現了。

因此,為了不讓應用程序對 gRPC 過於依賴,還應該使用一層抽象(接口)層與其解耦,用接口來隔離對 RPC 實現的依賴,這樣在需要使用不同的實現時,可以通過註冊不同的實現來方便地切換。

在這些場景下,本文要介紹的 Code-First gRPC 使用方法就發揮作用了。

Code-First gRPC

說了這麼久,我好像還沒正式介紹 Code-First gRPC,到底他有多適合在單一語言系統架構中實現 gRPC 呢?下面要介紹的就是基於大名鼎鼎的 protobuf-net 實現的 gRPC 框架,protobuf-net.Grpc。

protobuf-net 是在過去十幾年前到現在一直在 .NET 中有名的 Protobuf 庫,想用 Protobuf 序列化時就會用到這個庫。他的特性就是可以把 C# 代碼編寫的類能以 Protobuf 的協議進行序列化和反序列化,而不是 proto 文件再生成這些類。而 protobuf-net.Grpc 則是一脈相承,可以把 C# 寫的接口,在服務端方便地把接口的實現類註冊成 ASP.NET Core 的 gRPC 服務,在客戶端把接口動態代理實現為調用客戶端,調用前面的這個服務端。

用法很簡單,只要聲明一個接口為您的服務契約:

[ServiceContract]
public interface IMyAmazingService {
    ValueTask<SearchResponse> SearchAsync(SearchRequest request);
    // ...
}

然後實現該接口的服務端:

public class MyServer : IMyAmazingService {
    // ...
}

或者向系統獲取客戶端:

var client = http.CreateGrpcService<IMyAmazingService>();
var results = await client.SearchAsync(request);

這相當於以下 .proto 中的服務:

service MyAmazingService {
    rpc Search (SearchRequest) returns (SearchResponse) {}
    // ...
}

protobuf-net.Grpc 同樣通過普通類型定義支持 gRPC 的四種模式,把 C# 8.0 中最新的 IAsyncEnumerable 類型識別成 proto 中的 stream,單向流、雙向流都可以實現!而且用 IAsyncEnumerable 實現可比 proto 生成的類方便很多。

例如 proto 雙向流定義:

rpc chat(stream ChatRequest) returns ( stream ChatResponse);

生成出來的方法是:

Task BathTheCat(IAsyncStreamReader<ChatRequest> requestStream, IServerStreamWriter<ChatResponse> responseStream)

而 protobuf-net.Grpc 只要定義一個方法:

IAsyncEnumerable<ChatResponse> SubscribeAsync(IAsyncEnumerable<ChatRequest> requestStream);

由此可見,protobuf-net.Grpc 無需在契約層引入第三方庫,充分運用了 C# 類型系統,把方法、類型映射到兼容了 gRPC 的服務定義上。

上文所說的 proto 局限也迎刃而解了,函數調用、gRPC、REST 都能方便切換。(REST 實現可以參考我的開源框架 shriek-fx 中的 Shriek.ServiceProxy.Http )組件。

下一篇,我將主要介紹利用 protobuf-net.Grpc 的 gRPC 雙向流模式與 Blazor 實現一個簡單的在線即時聊天室。

相關鏈接:

  • protobuf-net.Grpc:
  • shriek-fx:
  • GrpcChat:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

Java多線程系列——多線程方法詳解

Java多線程系列文章是Java多線程的詳解介紹,對多線程還不熟悉的同學可以先去看一下我的這篇博客,這篇博客從宏觀層面介紹了多線程的整體概況,接下來的幾篇文章是對多線程的深入剖析。

 

多線程的常用方法

1、currentThread()方法:

介紹:currentThread()方法可返回該代碼正在被哪個線程調用的信息。

示例:

例1:

public class Test01 {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());
	}
	
}

結果:
main

  

結果說明,main方法被名為main的線程調用

 

例2:

class Mythread extends Thread{
	
	public Mythread() {
		System.out.println("構造方法的打印:"+Thread.currentThread().getName());
	}
	
	@Override
	public void run() {
		System.out.println("run方法的打印:"+Thread.currentThread().getName());
	}
}

public class Test01 {

	public static void main(String[] args) {
		Mythread t=new Mythread();
		t.start();//①
	}
	
}

結果:
構造方法的打印:main
run方法的打印:Thread-0

  

從結果可知:Mythread的構造方法是被main線程調用的,而run方法是被名稱為Thread-0的線程調用的,run方法是線程自動調用的

現在我們將①處的代碼改為t.run(),現在的輸出結果如下:

構造方法的打印:main
run方法的打印:main

  

從結果中我們可以看到兩次的結果显示都是main線程調用了方法,因為當你使用t.start()方法的時候是線程自動調用的run()方法,所以輸出的是Thread-0,當你直接調用run()方法時,和調用普通方法沒有什麼區別,所以是main線程調用run()

 

2、isAlive()方法:

介紹:isAlive()方法的功能是判斷當前的線程是否處於活動狀態

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		System.out.println("run =="+this.isAlive());
	}
	
}

public class Test01 {

	public static void main(String[] args) {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
end ==true
run ==true

  

方法isAlive()的作用是測試線程是否處於活動狀態。那麼什麼情況下是活動狀態呢?活動狀態就是線程已經啟動且尚未停止。線程處於正在運行或準備開始運行的狀態,就認為線程是存活的

①處代碼的結果為false,因為此時線程還未啟動;

②處代碼調用了run()方法輸出結果為run ==true,此時線程處於活動狀態;

③處代碼的結果為true,有的同學看到這個輸出可能會不理解,不是說線程處於活動狀態isAlive()方法的結果才是true,現在程序都已經運行結束了為什麼還是true?這裏的輸出結果是不確定的,我們再來看下面一段代碼

我們將例1中的代碼稍做修改,代碼如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		Thread.sleep(1000);//這裏加了一行代碼,讓當前線程沉睡1秒
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
run ==true
end ==false

  

現在我們看到③處的代碼結果為end ==false,因為thread對象已經在1秒內執行完畢,而上面代碼輸出結果為true是因為thread線程未執行完畢。

 

3、sleep()方法:

介紹:

方法sleep()的作用是在指定的毫秒數內讓當前“正在執行的線程”休眠(暫停執行),這個“正在執行的線程”是指this.currentThread()返回的線程。

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run threadName="+this.currentThread().getName()+" begin");
			Thread.sleep(2000);
			System.out.println("run threadName="+this.currentThread().getName()+" end");
			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin ="+System.currentTimeMillis());
		thread.run();//①
		System.out.println("end ="+System.currentTimeMillis());
	}
	
}

結果:
begin =1574660731663
run threadName=main begin
run threadName=main end
end =1574660733665

  

從結果中可以看出main線程暫停了2秒(因為這裏調用的是thread.run())

下面我們將①處的代碼改成thread.start(),再來看下運行結果:

begin =1574661491412
end =1574661491412
run threadName=Thread-0 begin
run threadName=Thread-0 end

  

由於main線程與thread線程是異步執行的,所以首先打印的信息為begin和end,而thread線程是隨後運行的,在最後兩行打印run begin和run end的信息。

 

4、getId()方法:

介紹:getId()方法的作用是取得線程的唯一標識

示例:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread thread=Thread.currentThread();
		System.out.println(thread.getName()+" "+thread.getId());
	}
	
}

結果:main 1

  

從運行結果可以看出,當前執行代碼的線程名稱是main,線程id值為1

 

5、停止線程:

介紹:停止線程是在多線程開發時很重要的技術點,掌握此技術可以對線程的停止進行有效的處理。停止線程在Java語言中並不像break語句那樣乾脆,需要一些技巧性的處理。

在java中有三種方法可以停止線程

  1. 使用退出標誌,讓線程正常退出,也就是當run方法執行完之後終止
  2. 使用stop方法強制終止線程,但是不推薦使用,因為stop和suspend及resume一樣,是java廢棄的方法
  3. 使用interrupt方法中斷線程(推薦使用)

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(2000);
		thread.interrupt();
	}
	
}

  

運行結果:

 

從運行結果我們可以看出最後i=500000,調用interrupt方法沒有停止線程,那麼該如何停止線程呢?

在介紹如何停止線程時,我們先來介紹一下如何判斷線程是否處於停止狀態

Thread類中提供了兩種方法用來判斷線程是否停止:

1、this.interrupted():測試當前線程是否已經中斷,執行后具有將狀態標誌清除為false的功能

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
}

  

2、this.isInterrupted():測試線程Thread對象是否已經中斷,但是不清除狀態標誌

public boolean isInterrupted() {
        return isInterrupted(false);
}

  

讀者可以仔細觀看一下這兩個方法的聲明有什麼不同?

 

例2:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(1000);
		thread.interrupt();
		System.out.println("是否停止1?="+thread.interrupted());
		System.out.println("是否停止2?="+thread.interrupted());
		System.out.println("end!");
	}
	
}

  

結果:

 

 

 輸出結果显示調用了thread.interrupt()方法后線程並未停止,這也就證明了interrupted()方法的解釋:測試當前線程是否已經中斷。這個當前線程是main,它從未斷過,所以打印的結果是兩個false。

如果想讓main線程結束該怎麼做?

將main方法改成如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread.currentThread().interrupt();
		System.out.println("是否停止1?="+Thread.interrupted());
		System.out.println("是否停止2?="+Thread.interrupted());
		System.out.println("end!");
	}
	
}

結果:
是否停止1?=true
是否停止2?=false
end!

  

從輸出結果我們可以看出,方法interrupted()的確判斷出當前線程是否是停止狀態。但為什麼第2個值是false?

查看一下官方文檔的介紹:

測試當前線程是否已經中斷。線程的中斷狀態由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。

 文檔中說明的非常清楚,interrupted()方法具有清除狀態的功能,所以第二次調用interrupted方法返回的值時false。

下面我們來看一下isInterrupted()方法,將main方法改成如下代碼:

public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		thread.interrupt();
		Thread.sleep(1000);
		System.out.println("是否停止1?="+thread.isInterrupted());
		System.out.println("是否停止2?="+thread.isInterrupted());
		System.out.println("end");
}
結果:

是否停止1?=true
是否停止2?=true
end

  

從結果可以看出,方法isInterrrupted()並未清除狀態,所以結果為兩個true。

 

例3:在沉睡中停止

當線程調用sleep()方法后再調用interrupt()方法後會有什麼結果:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run begin");
			Thread.sleep(200000);
			System.out.println("run end");
		} catch (InterruptedException e) {
			System.out.println("在沉睡中被停止,進入catch!"+this.isInterrupted());
			e.printStackTrace();
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		try {
			Mythread thread=new Mythread();
			thread.start();
			Thread.sleep(200);
			thread.interrupt();
		}catch(Exception e) {
			System.out.println("main catch");
			e.printStackTrace();
		}
		System.out.println("end!");
	}
	
}

  

 

 

6、暫停線程:

暫停線程意味着此線程還可以恢復運行。在java多線程中,可以使用suspend()方法暫停線程,使用resume()方法恢複線程的執行

例1:

class Mythread extends Thread{
	
	private long i=0;
	public long getI() {
		return i;
	}
	
    public void setI(long i) {
		this.i = i;
	}
	
	@Override
	public void run() {
		while(true) {
			i++;
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(5000);
		//A段
		thread.suspend();
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		
		//B段
		thread.resume();
		Thread.sleep(5000);
		
		//C段
		thread.suspend();
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		
	}
	
}

  

結果:

 


從控制台打印的時間上來看,線程的確被暫停了,而且還可以恢復成運行狀態。

 

7、yield方法:

介紹:yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		long beginTime=System.currentTimeMillis();
		int count=0;
		for(int i=0;i<500000;i++) {
               //Thread.yield();① count=count+(i+1); } long endTime=System.currentTimeMillis(); System.out.println("用時:"+(endTime-beginTime)+"毫秒!"); } } public class Test01 { public static void main(String[] args) throws InterruptedException { Mythread thread=new Mythread(); thread.start(); } }

結果:用時:2毫秒!

  

現在將①處的代碼取消註釋,我們再來看一下運行結果:

用時:213毫秒!

  

將CPU讓給其他資源導致速度變慢

 

8、線程優先級:

介紹:

在操作系統中,線程可以劃分優先級,優先級較高的線程得到的CPU資源較多,也就是CPU優先執行優先級較高的線程對象中的任務。

設置線程優先級有助於幫“線程規劃器”確定在下一次選擇哪一個線程來優先執行。

設置線程的優先級使用setPriority()方法,此方法在JDK的源代碼如下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
}

  

在Java中,線程的優先級為1-10這10個等級,如果小於1或大於10,則JDK拋出異常throw new IllegalArgumentException()。

通常高優先級的線程總是先執行完,但是並不是一定的,高優先級和低優先級的線程會交替進行,高優先級執行的次數多一些

 

線程優先級的繼承特性:

在Java中,線程的優先級具有繼承性,比如A線程啟動B線程,則B線程的優先級與A是一樣的。

class Mythread2 extends Thread{
	@Override
	public void run() {
		System.out.println("Mythread2 run priority="+this.getPriority());
	}
}


class Mythread1 extends Thread{
	
	@Override
	public void run() {
		System.out.println("Mythread run priority="+this.getPriority());
		Mythread2 thread2=new Mythread2();
		thread2.start();
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		System.out.println("main thread begin priority="+Thread.currentThread().getPriority());
    	        //Thread.currentThread().setPriority(6);①
		System.out.println("main thread end priority="+Thread.currentThread().getPriority());
		Mythread1 thread1=new Mythread1();
		thread1.start();
	}
	
}

結果:
main thread begin priority=5
main thread end priority=5
Mythread run priority=5
Mythread2 run priority=5

  

可以看到上面幾個線程的優先級都為5

現在將①處的代碼註釋掉后的結果是:

main thread begin priority=5
main thread end priority=6
Mythread run priority=6
Mythread2 run priority=6

  

優先級被更改后再繼續繼承

 

9、守護線程:

在java中有兩種線程,一種是用戶線程,另一種是守護線程。

守護線程是一種特殊的線程,它的特性有“陪伴”的含義,當進程中不存在非守護線程了,則守護線程自動銷毀。典型的守護線程就是垃圾回收線程,當進程中沒有非守護線程了,則垃圾回收線程也就沒有存在的必要了,自動銷毀。用個比較通俗的比喻來解釋一下:“守護線程”:任何一個守護線程都是整個JVM中所有非守護線程的“保姆”,只要當前JVM實例中存在任何一個非守護線程沒有結束,守護線程就在工作,只有當最後一個非守護線程結束時,守護線程才隨着JVM一同結束工作。Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是GC(垃圾回收器),它就是一個很稱職的守護者。

 

class Mythread extends Thread{
	
	private int i=0;
	
	
	@Override
	public void run() {
		
		try {
			while(true) {
				i++;
				System.out.println("i="+(i));
				Thread.sleep(1000);
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		try {
			Mythread thread=new Mythread();
			thread.setDaemon(true);
			thread.start();
			Thread.sleep(5000);
			System.out.println("我離開Thread對象就不再打印了,也就是停止了");
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
}

  

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

Java開發中常用jar包整理及使用

本文整理了我自己在Java開發中常用的jar包以及常用的API記錄。

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

common-lang3

簡介

一個現在最為常用的jar包,封裝了許多常用的工具包

依賴:

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>

主要常見的類如下:

  • 數組工具類 ArrayUtils
  • 日期工具類 DateUtils DateFormatUtils
  • 字符串工具類 StringUtils
  • 数字工具類 NumberUtils
  • 布爾工具類 BooleanUtils
  • 反射相關工具類 FieldUtils、MethodUtils、MemberUtils、TypeUtils、ConstructorUtils
  • 對象工具類 ObjectUtils
  • 序列化工具類 SerializationUtils

API介紹

這裏我只介紹經常使用的幾個工具類及方法,ArrayUtils,StringUtils,NumberUtils,DateUtils,其他的請查看官方API文檔吧

1.ArrayUtils

方法名 說明
add
remove
clone 複製數組
addAll
removeAll 第二個參數傳入需要刪除的下標(可以指定多個下標)
toObject 把數值(int[],double[])轉為包裝類(Int[],Double[])
indexOf 在數組按順序查找,找到第一個滿足對應的數值的下標
lastIndexOf 在數組按順序查找,找到最後一個滿足對應的數值的下標
contains 數組是否包含某個值
isEmpty 判斷數組是否為空
isNotEmpty 判斷數組是否不為空
reverse 數組反轉
subarray 指定區間截取數組,區間為半開區間,不包含末尾
toArray 接收一個多個對象,把這幾個對象轉為對應類型的數組
toMap 將一個二維數組轉為Map

2.NumberUtils

方法名 說明
min 比較三個數,返回最小值 或比較指定的幾個數,返回最小值
max 比較三個數,返回最大值 或比較指定的幾個數,返回最大值
createInt 從傳入的String中創建對應類型的數值,createDouble,createFloat…
toInt 將指定字符串轉為Int類型,可以選擇指定默認數值,如果字符串為null則返回默認數值,除此之外,還有toDouble,toLong…等轉為不同類型的方法
compare 比較兩個同類型數值的大小
isDigits 判斷字符串是否只包含数字
isParsable 判斷字符串是否可轉換為Long,Int等類型
isNumber 判斷字符串是否為數值(0x,0X開頭等進制數值)

3.DateUtils

方法名 說明
parseDate 將Date對象轉為字符串
isSameDay 判斷兩個Dated對象是否為同一天
isSameDay 判斷兩個Dated對象是否為同一天
addHour 將指定的Date對象加上指定小時,除此之外,還有addMonth,addDay..等

DateFormatUtils正如其名,是用來把時間轉為字符串,這裏就不再多說

4.StringUtils

方法名 說明
join 將指定的數組連接成字符串,並添加指定的分割字符
containOnly 字符串是否只包含某個字符串
substringBefore 截取指定字符串前面的內容
substringAfter 截取指定字符串後面的內容(不包括指定字符串)
substringBetween 截取字符串某區間內容,如substringBetween(“abcde”,”a”,”e”)=”bcd”
difference 比較兩個字符串,返回兩個字符串不同的內容,具體可以看API文檔給出的示例
isBlank 判斷字符串是否為空白,null,””,” “這三個結果都是為true
isEmpty 判斷字符串是否為空(只要不為null,或傳入的String對象的長度不為0即為true)
countMatches 判斷指定的字符串在某個字符串中出現的次數
deleteWhitespace 刪除字符串中的空格
defaultIfBlank 如果字符串為空白,則返回一個指定的默認值(null或某個String)
defaultIfEmpty 如果字符串為空,則返回一個指定的默認值(null或某個String)
capitalize 將指定字符串首字母大寫
abbreviate 將指定字符串的後面三位轉為…
swapCase 將字符串中的字母大小寫反轉,如aBc變為AbC
lowerCase 將字符串的字母全部轉為小寫
upperCase 將字符串的字母全部轉為大寫
left 取字符串左邊幾個字符,如left(“hello”,3)=”hel”,right與此相反
leftPad 字符串的長度不夠,則使用指定字符填充指定字符串,如leftPad(“hel”,5,”z”)=”zzhel”,rightPad方法與此相反
prependIfMissing 指定字符串不以某段字符串開頭,則自動添加開頭,如prependIfMissing(“hello”,”li”)=”lihello”
prependIfMissing 指定字符串不以某段字符串開頭(忽略大小寫),則自動添加開頭
getCommonPrefix 獲得多個字符串相同的開頭內容,接收參數為多個字符串
removeEnd 刪除字符串中結尾(滿足是以某段內容結尾),如removeEnd(“hello”,”llo”)=”he”
removeEndIgnoreCase 與上面一樣,忽略大小寫
removeStart 與上面的相反
remove 刪除字符串中的指定內容,如remove(“hello”,”l”)=”heo”
removeIgnoreCase 刪除字符串中的指定內容,如remove(“hello”,”l”)=”heo”
strip 清除字符串開頭和末尾指定的字符(第二個參數為null,用來清除字符串開頭和末尾的空格),如strip(” abcxy”,”xy”)=” abc”,strip(” abcxy”,”yx”)=” abc”
stripStart 清除字符串開頭指定字符
stripEnd 清除字符串末尾指定的字符

common-io

簡介

常用的IO流工具包

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

API

我們主要關心的就是Utils後綴的那幾個類即可,可以看到,common-io庫提供了FileUtils,FileSystemUtils,FileNameUtils,FileFilterUtils,IOUtils

FileUtils

  • 寫出文件
  • 讀取文件
  • 創建一個有父級文件夾的文件夾
  • 複製文件和文件夾
  • 刪除文件和文件夾
  • URL轉文件
  • 通過過濾器和擴展名來篩選文件和文件夾
  • 比較文件內容
  • 文件最後修改時間
  • 文件校驗

FileSystemUtils

關於文件系統的相關操作,如查看C盤的大小,剩餘大小等操作

IOUtils

字面意思,是封裝了IO流的各種操作的工具類

Log4j

簡介

Log4J 是 Apache 的一個開源項目,通過在項目中使用 Log4J,我們可以控制日誌信息輸出到控制台、文件、GUI 組件、甚至是數據庫中。

我們可以控制每一條日誌的輸出格式,通過定義日誌的輸出級別,可以更靈活的控制日誌的輸出過程,方便項目的調試。

依賴:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

結構

Log4J 主要由 Loggers (日誌記錄器)、Appenders(輸出端)和 Layout(日誌格式化器)組成。

其中Loggers 控制日誌的輸出級別與日誌是否輸出;
Appenders 指定日誌的輸出方式(輸出到控制台、文件等);
Layout 控制日誌信息的輸出格式。

日誌級別:

級別 說明
OFF 最高日誌級別,關閉左右日誌
FATAL 將會導致應用程序退出的錯誤
ERROR 發生錯誤事件,但仍不影響系統的繼續運行
WARN 警告,即潛在的錯誤情形
INFO 一般和在粗粒度級別上,強調應用程序的運行全程
DEBUG 一般用於細粒度級別上,對調試應用程序非常有幫助
ALL 最低等級,打開所有日誌記錄

我們主要使用這四個:Error>Warn>Info>Debug

使用

我們可以使用兩種方式來運行Log4j,一種是java代碼方式,另外一種則是配置文件方式

例子(Java方式)

public class Log4JTest {
    public static void main(String[] args) {   
        //獲取Logger對象的實例(傳入當前類)         
        Logger logger = Logger.getLogger(Log4JTest.class);
        //使用默認的配置信息,不需要寫log4j.properties
        BasicConfigurator.configure();
        //設置日誌輸出級別為WARN,這將覆蓋配置文件中設置的級別,只有日誌級別低於WARN的日誌才輸出
        logger.setLevel(Level.WARN);
        logger.debug("這是debug");
        logger.info("這是info");
        logger.warn("這是warn");
        logger.error("這是error");
        logger.fatal("這是fatal");
    }
}

例子(配置文件方式)

上面的例子,我們想要實現打印Log,但是每次都要寫一遍,浪費時間和精力,所以,Log4j提供了另外一種方式來配置好我們的信息

創建一個名為log4j.properties的文件,此文件需要放在項目的根目錄(約定),如果是maven項目,直接放在resources文件夾中即可

log4j.properties

#控制台
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n

#log jdbc
log4j.logger.java.sql.ResultSet=INFO
log4j.logger.org.apache=WARN
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

#log mybatis設置
#log4j.logger.org.apache.ibatis=DEBUG
log4j.logger.org.apache.ibatis.jdbc=error
log4j.logger.org.apache.ibatis.io=info
log4j.logger.org.apache.ibatis.datasource=info

#springMVC日誌
log4j.logger.org.springframework.web=WARN

# 文件輸出配置
log4j.appender.A = org.apache.log4j.DailyRollingFileAppender
log4j.appender.A.File = D:/log.txt #指定日誌的輸出路徑
log4j.appender.A.Append = true
log4j.appender.A.Threshold = DEBUG
log4j.appender.A.layout = org.apache.log4j.PatternLayout #使用自定義日誌格式化器
log4j.appender.A.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n #指定日誌的輸出格式
log4j.appender.A.encoding=UTF-8 #指定日誌的文件編碼

#指定日誌的輸出級別與輸出端
log4j.rootLogger=DEBUG,Console,A

#指定某個包名日誌級別(不能超過上面定義的級別,否則日誌不會輸出)
log4j.logger.com.wan=DEBUG

之後使用的話就比較簡單了

//Logger的初始化(這個推薦定義為全局變量,方便使用)
Logger logger = Logger.getLogger(Log4JTest.class);
//輸出Log
logger.info("這是info");

參考鏈接:

lombok

簡介

平常我們創建實體類的時候,需要get/set方法,極其麻煩,雖然IDEA等IDE都是有提供了快捷生成,不過,最好的解決方法還是省略不寫

而lombok就是這樣的一個框架,實現省略get/set方法,當然,lombok的功能不只有此,還有equal,toString方法也可以由此框架自動生成

lombok的原理是使用註解,之後就會在編譯過程中,給Class文件自動加上get/set等方法

不過IDEA似乎無法識別,代碼檢查還是會報錯,所以,使用IDEA的時候還得安裝一個插件,在plugin搜索lombok,之後安裝重啟即可,如圖

之後為Java項目添加依賴

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>

使用示例

1.實體類省略get/set
估計Kotlin中的data關鍵字就是參照着lombok實現的

//這裏我們只需要為類添加Data註解,就會自動生成對應屬性的get/set方法,toString,equal等方法
@Data
public class User {
    private String username;
    private String password;
}

2.需要無參構造以及get/set方法

@Getter
@Setter
@NoArgsConstructor
public class User {
    private String username;
    private String password;
}

3.鏈式調用set方法

@Data
@Accessors(chain = true)
public class User {
    private String username;
    private String password;
}

//使用
User user = new User();
user.setUsername("helo").setPassword("123");

4.參數不為空

//如果調用此方法,就會抱一個空指針錯誤
public String print(@NotNull String str){
    ...
}

5.只需要toString

@ToString(callSuper=true, includeFieldNames=true)
public class User {
    private String username;
    private String password;
    //省略的get/set方法
}

6.builder模式創建實體類對象

@Data
@Builder
public class User {
    private String username;
    private String password;
}
//使用
User user1 = User.builder().username("user1").password("123").build();

7.工具類

@UtilityClass
public class MyUtils{
    //會將此方法自動轉為靜態方法
    public void print(String str){
        ...
    }
}
//使用
MyUtils.print("hello");

8.自動關閉流

public static void main(String[] args) throws Exception {
    //使用Cleanup會自動調用close方法
    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    byte[] b = new byte[1024];
    while (true) {
        int r = in.read(b);
        if (r == -1) break;
        out.write(b, 0, r);
    }
}

9.省略Logger時的初始化

@Log4j
@Log
public class User{
    //會自動添加此語句
    //Logger logger = Logger.getLogger(User.class);
    ...
}

參考:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

.NET高級特性-Emit(2)類的定義,.NET高級特性-Emit(1)

  在上一篇博文發了一天左右的時間,就收到了博客園許多讀者的評論和推薦,非常感謝,我也會及時回復讀者的評論。之後我也將繼續撰寫博文,梳理相關.NET的知識,希望.NET的圈子能越來越大,開發者能了解/深入.NET的本質,將工作做的簡單又高效,拒絕重複勞動,拒絕CRUD。

  ok,咱們開始繼續Emit的探索。在這之前,我先放一下我往期關於Emit的文章,方便讀者閱讀。

  《》

一、基礎知識

  既然C#作為一門面向對象的語言,所以首當其沖的我們需要讓Emit為我們動態構建類。

  廢話不多說,首先,我們先來回顧一下C#類的內部由什麼東西組成:

  (1) 字段-C#類中保存數據的地方,由訪問修飾符、類型和名稱組成;

  (2) 屬性-C#類中特有的東西,由訪問修飾符、類型、名稱和get/set訪問器組成,屬性的是用來控制類中字段數據的訪問,以實現類的封裝性;在Java當中寫作getXXX()和setXXX(val),C#當中將其變成了屬性這種語法糖;

  (3) 方法-C#類中對邏輯進行操作的基本單元,由訪問修飾符、方法名、泛型參數、入參、出參構成;

  (4) 構造器-C#類中一種特殊的方法,該方法是專門用來創建對象的方法,由訪問修飾符、與類名相同的方法名、入參構成。

  接着,我們再觀察C#類本身又具備哪些東西:

  (1) 訪問修飾符-實現對C#類的訪問控制

  (2) 繼承-C#類可以繼承一個父類,並需要實現父類當中所有抽象的方法以及選擇實現父類的虛方法,還有就是子類需要調用父類的構造器以實現對象的創建

  (3) 實現-C#類可以實現多個接口,並實現接口中的所有方法

  (4) 泛型-C#類可以包含泛型參數,此外,類還可以對泛型實現約束

  以上就是C#類所具備的一些元素,以下為樣例:

public abstract class Bar
{
    public abstract void PrintName();
}
public interface IFoo<T> { public T Name { get; set; } } //繼承Bar基類,實現IFoo接口,泛型參數T
public class Foo<T> : Bar, IFoo<T>
  //泛型約束
  where T : struct {
//構造器 public Foo(T name):base() { _name = name; } //字段 private T _name; //屬性 public T Name { get => _name; set => _name = value; } //方法 public override void PrintName() {
    Console.WriteLine(_name.ToString()); }
}

  在探索完了C#類及其定義后,我們要來了解C#的項目結構組成。我們知道C#的一個csproj項目最終會對應生成一個dll文件或者exe文件,這一個文件我們稱之為程序集Assembly;而在一個程序集中,我們內部包含和定義了許多命名空間,這些命令空間在C#當中被稱為模塊Module,而模塊正是由一個一個的C#類Type組成。

 

 

 

   所以,當我們需要定義C#類時,就必須首先定義Assembly以及Module,如此才能進行下一步工作。

二、IL概覽

   由於Emit實質是通過IL來生成C#代碼,故我們可以反向生成,先將寫好的目標代碼寫成cs文件,通過編譯器生成dll,再通過ildasm查看IL代碼,即可依葫蘆畫瓢的編寫出Emit代碼。所以我們來查看以下上節Foo所生成的IL代碼。

  

 

 

   從上圖我們可以很清晰的看到.NET的層級結構,位於樹頂層淺藍色圓點表示一個程序集Assembly,第二層藍色表示模塊Module,在模塊下的均為我們所定義的類,類中包含類的泛型參數、繼承類信息、實現接口信息,類的內部包含構造器、方法、字段、屬性以及它的get/set方法,由此,我們可以開始編寫Emit代碼了

三、Emit編寫

  有了以上的對C#類的解讀和IL的解讀,我們知道了C#類本身所需要哪些元素,我們就開始根據這些元素來開始編寫Emit代碼了。這裏的代碼量會比較大,請讀者慢慢閱讀,也可以參照以上我寫的類生成il代碼進行比對。

  在Emit當中所有創建類型的幫助類均以Builder結尾,從下錶中我們可以看的非常清楚

元素中文 元素名稱 對應Emit構建器名稱
程序集  Assembly AssemblyBuilder
模塊  Module ModuleBuilder
類  Type TypeBuilder
構造器  Constructor ConstructorBuilder
屬性  Property PropertyBuilder
字段  Field FieldBuilder
方法  Method MethodBuilder

  由於創建類需要從Assembly開始創建,所以我們的入口是AssemblyBuilder

  (1) 首先,我們先引入命名空間,我們以上節Foo類為樣例進行編寫

using System.Reflection.Emit;

  (2) 獲取基類和接口的類型

var barType = typeof(Bar);
var interfaceType = typeof(IFoo<>);

  (3) 定義Foo類型,我們可以看到在定義類之前我們需要創建Assembly和Module

//定義類
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Edwin.Blog.Emit");
var typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

  (4) 定義泛型參數T,並添加約束

//定義泛型參數
var genericTypeBuilder = typeBuilder.DefineGenericParameters("T")[0];
//設置泛型約束
genericTypeBuilder.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

  (5) 繼承和實現接口,注意當實現類的泛型參數需傳遞給接口時,需要將泛型接口添加泛型參數后再調用AddInterfaceImplementation方法

//繼承基類
typeBuilder.SetParent(barType);
//實現接口
typeBuilder.AddInterfaceImplementation(interfaceType.MakeGenericType(genericTypeBuilder));

  (6) 定義字段,因為字段在構造器值需要使用,故先創建

//定義字段
var fieldBuilder = typeBuilder.DefineField("_name", genericTypeBuilder, FieldAttributes.Private);

  (7) 定義構造器,並編寫內部邏輯

//定義構造器
var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { genericTypeBuilder });
var ctorIL = ctorBuilder.GetILGenerator();
//Ldarg_0在實例方法中表示this,在靜態方法中表示第一個參數
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
//為field賦值
ctorIL.Emit(OpCodes.Stfld, fieldBuilder);
ctorIL.Emit(OpCodes.Ret);

  (8) 定義Name屬性

//定義屬性
var propertyBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None, genericTypeBuilder, Type.EmptyTypes);

  (9) 編寫Name屬性的get/set訪問器

//定義get方法
var getMethodBuilder = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, genericTypeBuilder, Type.EmptyTypes);
var getIL = getMethodBuilder.GetILGenerator();
getIL.Emit(OpCodes.Ldarg_0);
getIL.Emit(OpCodes.Ldfld, fieldBuilder);
getIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(getMethodBuilder, interfaceType.GetProperty("Name").GetGetMethod()); //實現對接口方法的重載
propertyBuilder.SetGetMethod(getMethodBuilder); //設置為屬性的get方法
//定義set方法
var setMethodBuilder = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { genericTypeBuilder });
var setIL = setMethodBuilder.GetILGenerator();
setIL.Emit(OpCodes.Ldarg_0);
setIL.Emit(OpCodes.Ldarg_1);
setIL.Emit(OpCodes.Stfld, fieldBuilder);
setIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(setMethodBuilder, interfaceType.GetProperty("Name").GetSetMethod()); //實現對接口方法的重載
propertyBuilder.SetSetMethod(setMethodBuilder); //設置為屬性的set方法

   (10) 定義並實現PrintName方法

//定義方法
var printMethodBuilder = typeBuilder.DefineMethod("PrintName", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.Standard, null, Type.EmptyTypes);
var printIL = printMethodBuilder.GetILGenerator();
printIL.Emit(OpCodes.Ldarg_0);
printIL.Emit(OpCodes.Ldflda, fieldBuilder);
printIL.Emit(OpCodes.Constrained, genericTypeBuilder);
printIL.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
printIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
printIL.Emit(OpCodes.Ret);
//實現對基類方法的重載
typeBuilder.DefineMethodOverride(printMethodBuilder, barType.GetMethod("PrintName", Type.EmptyTypes));

  (11) 創建類

var type = typeBuilder.CreateType(); //netstandard中請使用CreateTypeInfo().AsType()

  (12) 調用

var obj = Activator.CreateInstance(type.MakeGenericType(typeof(DateTime)), DateTime.Now);
(obj as Bar).PrintName();
Console.WriteLine((obj as IFoo<DateTime>).Name);

四、應用

  上面的樣例僅供學習只用,無法運用在實際項目當中,那麼,Emit構建類在實際項目中我們可以有什麼應用,提高我們的編碼效率

  (1) 動態DTO-當我們需要將實體映射到某個DTO時,可以用動態DTO來代替你手寫的DTO,選擇你需要的字段回傳給前端,或者前端把他想要的字段傳給後端

  (2) DynamicLinq-我的第一篇博文有個讀者提到了表達式樹,而linq使用的正是表達式樹,當表達式樹+Emit時,我們就可以用像SQL或者GraphQL那樣的查詢語句實現動態查詢

  (3) 對象合併-我們可以編寫實現一個像js當中Object.assign()一樣的方法,實現對兩個實體的合併

  (4) AOP動態代理-AOP的核心就是代理模式,但是與其對應的是需要手寫代理類,而Emit就可以幫你動態創建代理類,實現切面編程

  (5) …

五、小結

  對於Emit,確實初學者會對其感到複雜和難以學習,但是只要搞懂其中的原理,其實最終就是C#和.NET語言的本質所在,在學習Emit的同時,也是在鍛煉你的基本功是否紮實,你是否對這門語言精通,是否有各種簡化代碼的應用。

  保持學習,勇於實踐;Write Less,Do More;作者之後還會繼續.NET高級特性系列,感謝閱讀!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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