Java描述設計模式(23):訪問者模式

本文源碼: ||

一、生活場景

1、場景描述

電競是遊戲比賽達到“競技”層面的體育項目。利用电子設備作為運動器械進行的、人與人之間的智力對抗運動。通過電競,可以提高人的反應能力、協調能力、團隊精神等。但是不同人群的對電競的持有的觀念不一樣,有的人認為電競就是沉迷網絡,持反對態度,而有的人就比較贊同。下面基於訪問者模式來描述該場景。

2、場景圖解

3、代碼實現

public class C01_InScene {
    public static void main(String[] args) {
        DataSet dataSet = new DataSet() ;
        dataSet.addCrowd(new Youth());
        dataSet.addCrowd(new MiddleAge());
        CrowdView crowdView = new Against() ;
        dataSet.display(crowdView);
        crowdView = new Approve() ;
        dataSet.display(crowdView);
    }
}
/**
 * 雙分派,不同人群管理
 */
abstract class Crowd {
    abstract void accept(CrowdView action);
}
class Youth extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getYouthView(this);
    }
}
class MiddleAge extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getMiddleAgeView (this);
    }
}
/**
 * 不同人群觀念的管理
 */
abstract class CrowdView {
    // 青年人觀念
    abstract void getYouthView (Youth youth);
    // 中年人觀念
    abstract void getMiddleAgeView (MiddleAge middleAge);
}
class Approve extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人贊同電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人贊同電競");
    }
}
class Against extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人反對電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人反對電競");
    }
}
/**
 * 提供一個數據集合
 */
class DataSet {
    private List<Crowd> crowdList = new ArrayList<>();
    public void addCrowd (Crowd crowd) {
        crowdList.add(crowd);
    }
    public void display(CrowdView crowdView) {
        for(Crowd crowd : crowdList) {
            crowd.accept(crowdView);
        }
    }
}

二、訪問者模式

1、基礎概念

訪問者模式是對象的行為模式,把作用於數據結構的各元素的操作封裝,操作之間沒有關聯。可以在不改變數據結構的前提下定義作用於這些元素的不同的操作。主要將數據結構與數據操作分離,解決數據結構和操作耦合問題核心原理:被訪問的類裏面加對外提供接待訪問者的接口。

2、模式圖解

3、核心角色

  • 抽象訪問者角色

聲明多個方法操作,具體訪問者角色需要實現的接口。

  • 具體訪問者角色

實現抽象訪問者所聲明的接口,就是各個訪問操作。

  • 抽象節點角色

聲明接受操作,接受訪問者對象作為參數。

  • 具體節點角色

實現抽象節點所規定的具體操作。

  • 結構對象角色

能枚舉結構中的所有元素,可以提供一個高層的接口,用來允許訪問者對象訪問每一個元素。

4、源碼實現

public class C02_Visitor {
    public static void main(String[] args) {
        ObjectStructure obs = new ObjectStructure();
        obs.add(new NodeA());
        obs.add(new NodeB());
        Visitor visitor = new VisitorA();
        obs.doAccept(visitor);
    }
}
/**
 * 抽象訪問者角色
 */
interface Visitor {
    /**
     * NodeA的訪問操作
     */
    void visit(NodeA node);
    /**
     * NodeB的訪問操作
     */
    void visit(NodeB node);
}
/**
 * 具體訪問者角色
 */
class VisitorA implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
class VisitorB implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
/**
 * 抽象節點角色
 */
abstract class Node {
    /**
     * 接收訪問者
     */
    abstract void accept(Visitor visitor);
}
/**
 * 具體節點角色
 */
class NodeA extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationA(){
        System.out.println("NodeA.operationA");
    }
}
class NodeB extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationB(){
        System.out.println("NodeB.operationB");
    }
}
/**
 * 結構對象角色類
 */
class ObjectStructure {
    private List<Node> nodes = new ArrayList<>();
    public void detach(Node node) {
        nodes.remove(node);
    }
    public void add(Node node){
        nodes.add(node);
    }
    public void doAccept(Visitor visitor){
        for(Node node : nodes) {
            node.accept(visitor);
        }
    }
}

三、Spring框架應用

1、Bean結構的訪問

BeanDefinitionVisitor類,遍歷bean的各個屬性;接口 BeanDefinition,定義Bean的各樣信息,比如屬性值、構造方法、參數等等。這裏封裝操作bean結構的相關方法,但卻沒有改變bean的結構。

2、核心代碼塊

public class BeanDefinitionVisitor {
    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        this.visitParentName(beanDefinition);
        this.visitBeanClassName(beanDefinition);
        this.visitFactoryBeanName(beanDefinition);
        this.visitFactoryMethodName(beanDefinition);
        this.visitScope(beanDefinition);
        if (beanDefinition.hasPropertyValues()) {
            this.visitPropertyValues(beanDefinition.getPropertyValues());
        }
        if (beanDefinition.hasConstructorArgumentValues()) {
            ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
            this.visitIndexedArgumentValues(cas.getIndexedArgumentValues());
            this.visitGenericArgumentValues(cas.getGenericArgumentValues());
        }
    }
}

四、模式總結

1、優點描述

(1) 訪問者模式符合單一職責原則、使程序具有良好的擴展性、靈活性;

(2) 訪問者模式適用與攔截器與過濾器等常見功能,數據結構相對穩定的場景;

2、缺點描述

(1) 訪問者關注其他類的內部細節,依賴性強,違反迪米特法則,這樣導致具體元素更新麻煩;

(2) 訪問者依賴具體元素,不是抽象元素,面向細節編程,違背依賴倒轉原則;

五、源代碼地址

GitHub·地址
https://github.com/cicadasmile/model-arithmetic-parent
GitEE·地址
https://gitee.com/cicadasmile/model-arithmetic-parent

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

十年風雨,一個普通程序員的成長之路(九)一眼望到頭,一眼望不到頭

還有十幾天就是我的32歲生日,然後,33了,要過年了。

古人三十而立,我卻在這狹窄的圈子里兜兜轉轉。

多年前的喊的一句創業口號,現在還是口號。

焦慮、迷茫。

這两天一場網易的暴力裁員事件,犹如一盆涼水當頭澆下。

讓我又陷入了一年前的時刻。

渾身提不起勁。什麼都不想做。

不知前路在哪裡?

 

回過頭來看,對於當事人來說曲折圓轉的半生,之於他人,不過又是一個復讀機的普通人生而已。

上學、畢業、工作、買房、結婚、生子、還貸。

沒有家庭是形單影只,凄凄涼涼。

有了家庭卻只能蠅營狗苟,負重而行。

一眼望到頭的路罷了。

 

我們都只是這庸碌的銅爐里,亂糟糟破爛爛的一塊廢銅爛鐵而已。

所以財富神話才那麼多捧臭腳的雇從。

因為世間大多的煩惱,便是沒錢的煩惱。

所以我們信努力改變命運、知識改變命令、堅持改變命運。

其實是金錢改變命運。

 

滿是一些交錢的APP,讓你堅持下去,給你鼓勵。永遠溫馨對你。

你會改變命運的,你會財富自由的。

我們如飢似渴,似乎覺得比身邊的人多get了一門技能,升職加薪也是指日可待了。

如果你沒有升職加薪、財富自由、改變命運,那不過是你還不夠努力罷了。

不過是,交的錢還不夠多罷了。

一眼望不到頭。

 

什麼時候是個頭?

寫博客、開公眾號、寫小說。尋找出路。

我們永遠相信自己是天命之子。

堂吉訶德騎着馬,夾着騎士長槍,無知無畏地沖向了風車。

大風車吱喲喲地轉,這裏的風景呀真好看……哦,畫風跑偏了。

大風車吱喲喲地轉,不為堂吉訶德所動,不為騎士長槍所動。似能流轉萬世。

 

天地不仁,以萬物為芻狗。

你又憑什麼跳出世間這個熔爐呢?

只能在時間的鐵鎚下越來越彎曲自己的身子。

在夕陽里傴僂着身子,苟延殘喘。

一眼望到頭,又一眼望不到頭。

 

在這條短暫卻又無盡的路上,那麼多的大V與培訓機構告訴你:

劉強東曾跟你一樣賣過盜版盤跟電腦。

馬雲還沒你強,曾被肯德基拒絕臨時工。

比爾蓋茨中途就退了學,你跟惠普也就差個車庫而已了。

遺憾的是,給你上課的老師,可能正兒八經的資金來源還沒有你多。

你以為打開了得到,便真能得道。

你以為買了極客時間,便真成了極客。

你以為加了大V,便算是有了人脈。

你以為入了知識星球,便真學到了知識。

遺憾的是,大多時候,它們與書架上落灰的書籍沒什麼兩樣。

 

家庭的壓力也讓你學習的時間慢慢變少。

有了一點獨處的時間,你卻又想打兩把遊戲,松一松這命運壓迫的喉嚨,大口地喘息兩聲。

在遊戲中,孩子的哭鬧、老婆的絮絮叨叨,都已變成遙遠的過去。

可是玩了一會,你卻又充滿了負罪感。空虛與寂寞隨之而來。

因為普通人改變命運的機會太少了。

所以只能讀書改變命運。

 

在兩千年的中華文明史中,知識改變命運的箴言已印刻在了基因里。

犹如稻草。

給溺水的人,最後一點光芒與希望。

因為你這一生,我這一生。一眼便已能看到頭。

所以佛度來生。

所以道修逍遙。

若有仙人撫我頂,怎可結髮受長生?

我願化為北冥之鯤,潛於九淵,扶搖九天,逍遙星河之外。

只是一聲“爸爸,我要尿尿”。夢,便醒了。

 

你心裏嘆了一口氣,便把兒子從你跟你老婆身邊抱下了床。

穿衣、洗漱,照了照鏡子,才刮的鬍子又長了出來。

算了,反正是非單身的程序員,也沒什麼可講究的。

關上門,復讀機的一天又開始了。

可是我還是渾身提不起勁,總感覺失去了什麼。

於是,寫下此文。

一眼望到頭,一眼望不到頭。

 

——————————————————–
歡迎關注我的公眾號:姚毛毛的博客

這裡有我的編程生涯感悟與總結,有Java、Linux、Oracle、mysql的相關技術,有工作中進行的架構設計實踐和讀書理論,有JVM、Linux、數據庫的性能調優,有……

有技術,有情懷,有溫度

歡迎關注我:姚毛毛& 妖生

 

 

 

 

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

文件與文件系統壓縮

目錄

在Linux下面有相當多的壓縮命令可以運行,這些壓縮命令可以讓我們更方便地從網絡上面下載容量較大的文件。此外,我們知道在Linux下面,擴展名沒有什麼特殊的意義。 不過,針對這些壓縮命令所產生的壓縮文件,為了方便記憶,還是會有一些特殊的命名方式,就讓我們來看看吧!

文件壓縮

什麼是文件壓縮呢?我們稍微談一談它的原理,目前我們使用的計算機系統中都是使用所謂的字節單位來計量。不過,事實上,計算機最小的計量單位應該是bit才對,此外,我們也知道 1字節=8比特(1Byte=8bit),但是如果今天我們只是記錄一個数字,即1這個数字,它會如何記錄?假設一個字節可以看成下面的模樣:

由於 1Byte=8bit,所以每個字節當中會有8個空格,而每個空格只可以是0、1

由於我們記錄的数字是1,考慮計算機所謂的二進制,如此一來,1會在最右邊佔據1個位,而其他的7個位將會自動地被填上0.如下圖所示

你看看,其實在這樣的例子中,那7個位應該是空的才對。不過,為了要滿足目前我們的操作系統數據的讀寫,所以就會將該數據轉為字節的形式來記錄。而一些聰明的計算機工程師就利用一些複雜的計算方式,將這些沒有使用到的空間【丟】出來,以讓文件佔用的空間變小,這就是壓縮的技術。
另一種壓縮技術也很有趣,它是將重複的數據進行統計記錄。舉例來說,如果你的數據為【111······】共有100個1時,那麼壓縮技術會記錄為【100個1】而不是真的有100個1的位存在。這樣也能夠精簡文件記錄的容量,非常有趣吧!
簡單地說,你可以將它想成,其實文件裏面有相當多的空間存在,並不是完全填滿的,而壓縮技術就是將這些空間填滿,以讓整個文件佔用的容量下降。不過,這些壓縮過的文件並無法直接被我們的操作系統所使用,因此,若要使用這些被壓縮過的文件數據,則必須將它還原回未壓縮前的模樣,那就是所謂的解壓縮。而至於壓縮后與壓縮的文件所佔用的磁盤空間大小,就可以被稱為是壓縮比
這個壓縮與解壓縮的操作有什麼好處呢?
1.最大的好處就是壓縮過的文件容量變小了,所以你的硬盤無形之中就可以容納更多的數據。
2.此外,在一些網絡數據的傳輸中,也會由於數據量的降低,好讓網絡帶寬可以用來做更多的工作,而不是老卡在一些大型文件傳輸上面。

Linux系統常見壓縮命令

在Linux的環境中,壓縮文件的擴展名大多是: *.tar、*.tar.gz、*.gz、*.Z、*.bz2、*.xz。為什麼會有這樣的擴展名?不是說Linux的擴展名沒有什麼作用嗎?
這是因為Linux支持的壓縮命令非常多,且不同的命令所用的壓縮技術並不相同,當然彼此之間可能就無法互通/解壓縮文件。所以,當你下載到某個文件時,自然就需要知道該文件是由哪種壓縮命令所製作出來的,好用來對照對照着解壓縮,也就是說,雖然Linux文件的屬性基本上是與文件名沒有絕對關係的,但是為了幫助我們人類小小的腦袋,所以適當的擴展名還是必要的,下面我們就列出幾個常見的壓縮文件擴展名:

*.gz         gzip程序壓縮的文件
*.bz2        bzip2程序壓縮的文件
*.xz         xz程序壓縮的文件
*.zip        zip程序壓縮的文件
*.Z          compress程序壓縮的文件
*.tar        tar程序打包的文件,並沒有壓縮過
*.tar.gz     tar程序打包的文件,並且經過gzip的壓縮
*.tar.bz2    tar程序打包的文件,並且經過bzip2的壓縮
*.tar.xz     tar程序打包的文件,並且經過xz的壓縮

Linux常見的壓縮命令就是gzip、bzip2以及最新的xz,至於compress已經不流行了。為了支持windows常見的zip,其實Linux也早就有zip命令了。gzip是由GNU計劃所開發出來的壓縮命令,該命令支持已經替換了compress。後台GNU又開發出了bzip2及xz這幾個壓縮比更好的壓縮命令。不過,這些命令通常僅能針對一個文件來壓縮與解壓縮,如此一來,每次壓縮與解壓縮都要一大堆文件,豈不煩人?此時,這個所謂的【打包軟件,tar】就顯得很重要。
這個tar可以將很多文件打包成一個文件,甚至是目錄也可以這麼玩。不過,單純的tar功能僅僅是打包而已,即將很多文件結合為一個文件,事實上,它並沒有提供壓縮的功能,後台,GNU計劃中,將整個tar與壓縮的功能結合在一起,如此一來,提供用戶更方便且更強大的壓縮與打包功能,下面我們就來談一談這些在Linux下面基本的壓縮命令。

gzip

gzip可以說是應用最廣的壓縮命令了,目前gzip可以解開compress、zip和gzip等軟件所壓縮的文件,至於gzip所建立的壓縮文件為*.gz,讓我們來看看這個命令的語法:

gzip [-cdtvn] 文件名
選項與參數:
-c: 將壓縮的數據輸出到屏幕上,可通過數據流重定向來處理;
-d: 解壓縮的參數;
-t: 可以用來檢驗一個壓縮文件的一致性,看看文件有無錯誤;
-v: 可以显示出原文件/壓縮文件的壓縮比等信息;
-n: n為数字的意思,代表壓縮等級,-1最快,但壓縮比最差,-9最慢,但是壓縮比最好,默認是-6

示例1:壓縮文件(gzip -v 文件名)

示例2:解壓縮文件(gzip -d 文件名)

示例3:按照指定壓縮比壓縮(gzip -9 文件名)

示例4:查看壓縮文件的內容(zcat 文件名)

示例5:壓縮為指定文件名(gzip -c 文件名 > 指定文件名)

當你使用gzip進行壓縮時,在默認的狀態下原本的文件會被壓縮成為.gz後綴的文件,源文件就不存在了,這點與一般習慣使用Windows做壓縮的朋友所熟悉的情況不同,要注意。cat/more/less可以使用不同的方式來讀取純文本文件,那麼zcat/zmore/zless則可以對應於cat/more/less的方式來讀取純文件文件被壓縮后的壓縮文件。

bzip2

若說gzip是為了替換compress並提供更好的壓縮比而成立的,那麼bzip2則是為了替換gzip並提供更加的壓縮比而來。bzip2真是很不錯的東西,這玩意的壓縮比竟然比gzip還要好,至於bzip2的用法幾乎與gzip相同,看看下面的用法吧!

bzip2 [-cdkzvn] 文件名
選項與參數:
-c: 將壓縮的數據輸出到屏幕上,可通過數據流重定向來處理;
-d: 解壓縮的參數;
-k: 保留原始文件,而不是刪除原始文件;
-z: 壓縮的參數(默認值,可以不加);
-v: 可以显示出原文件/壓縮文件的壓縮比等信息;
-n: n為数字的意思,代表壓縮等級,-1最快,但壓縮比最差,-9最慢,但是壓縮比最好,默認是-6

示例:

bzip2 -v 待壓縮文件名
bzip2 -d 壓縮后的文件名
bzip2 -9 -c 待壓縮的文件名 > 自定義壓縮文件名

xz

雖然bzip2已經具有很棒的壓縮比,不過顯然某些自由軟件開發者還不滿足,因此後來還推出了xz這個壓縮比更高的軟件。這個軟件的用法也跟gzip/bzip2幾乎一模一樣,那我們就來看一看。

xz [-cdtlkn] 文件名
選項與參數:
-c: 將壓縮的數據輸出到屏幕上,可通過數據流重定向來處理;
-d: 解壓縮的參數;
-k: 保留原始文件,而不是刪除原始文件;
-l: 列出壓縮文件的相關信息;
-t: 測試壓縮文件的完整性,看看有沒有錯誤;
-z: 壓縮的參數(默認值,可以不加);
-n: n為数字的意思,代表壓縮等級,-1最快,但壓縮比最差,-9最慢,但是壓縮比最好,默認是-6

示例:

xz -v 待壓縮的文件名
xz -l 壓縮后的文件名
xz -d 壓縮后的文件名
xz -k 待壓縮的文件名

打包命令

前面談到的命令大多僅能針對單一文件來進行壓縮,雖然gzip、bzip2、xz也能夠針對目錄來進行壓縮,不過,這幾個命令對目錄的壓縮指的是將目錄內的所有文件【分別】進行壓縮的操作。而不像在Windows的系統,可以使用類似WinRAR這一類的壓縮軟件來將好多數據包成一個文件的樣式。
這種將多個文件或目錄包成一個大文件的命令功能,我們可以稱它是一種打包命令,那Linux有沒有這種打包命令?有,那就是大名鼎鼎的tar,tar可以將多個目錄或文件打包成一個大文件,同時還可以通過gzip、bzip2、xz的支持,將該文件同時進行壓縮。更有趣的是,由於tar的使用太廣泛了,目前Windows的WinRAR也支持.tar.gz文件名的解壓縮。

tar

tar的選項與參數特別多,我們只講幾個常用的選項,更多選項您可以自行man tar查詢。

tar [-z|-j|-J] [cv] [-f 待建立的新文件名] filename... <== 打包與壓縮。
tar [-z|-j|-J] [cv] [-f 既有的tar文件名]  <== 查看文件名
tar [-z|-j|-J] [xv] [-f 既有的tar文件名]  <== 解壓縮
選項與參數:
-c: 建立打包文件,可搭配-v來查看過程中被打包的文件名(filename);
-t: 查看打包文件的內容含有那些文件名,重點在查看【文件名】;
-x: 解包或解壓縮功能,可以搭配-C(大寫)在特定目錄解壓,特別留意的是,-c、-t、-x不可同時出現在一串命令行中;
-z: 通過gzip的支持進行壓縮/解壓縮: 此時文件名最好為*.tar.gz;
-j: 通過bzip2的支持進行壓縮/解壓縮:此時文件名最好為*.tar.bz2;
-J: 通過xz的支持進行壓縮/解壓縮: 此時文件名最好為 *.tar.xz,特別留意,-z、-j、-J不可以同時出現在一串命令行中;
-v: 在壓縮/解壓縮的過程中,將正在處理的文件名显示出來;
-f filename: -f後面要立刻接要被處理的文件名,建議-f單獨寫一個選項(比較不會忘記)。
-C 目錄: 這個選項用在解壓縮,若要在特定目錄解壓縮,可以使用這個選項
-p(小寫): 保留備份數據的原本權限與屬性,常用於備份(-c)重要的配置文件;
-P(大寫): 保留絕對路徑,亦即允許備份數據中含有根目錄存在之意

其實最簡單的使用tar就只要記住下面的命令即可:

  • 壓縮: tar -jcv -f filename.tar.bz2 要被壓縮的文件或目錄名稱;
  • 查詢: tar -jtv -f filename.tar.bz2
  • 解壓縮: tar -jxv -f filename.tar.bz2 -C 欲解壓縮的目錄

示例:

tar -zcvf 文件名.tar.gz 文件名(目錄)

tar -ztvf 文件名.tar.gz

tar -zxvf 文件名.tar.gz

資料:

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

※公開收購3c價格,不怕被賤賣!

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

【決戰西二旗】|你真的懂快速排序?

本文首發於:

微信公眾號:後端技術指南針

持續乾貨 歡迎關注!

看似青銅實則王者

很多人提起快排和二分都覺得很容易的樣子,但是讓現場Code很多就翻車了,就算可以寫出個遞歸版本的代碼,但是對其中的複雜度分析、邊界條件的考慮、非遞歸改造、代碼優化等就無從下手,填鴨背誦基本上分分鐘就被面試官擺平了。

 

那年初識快速排序

快速排序Quicksort又稱劃分交換排序partition-exchange sort,簡稱快排,一種排序算法。最早由東尼·霍爾(C. A. R. Hoare)教授在1960年左右提出,在平均狀況下,排序n個項目要O(nlogn)次比較。

在最壞狀況下則需要O(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他算法更快,因為它的內部循環可以在大部分的架構上很有效率地達成。

快速排序的核心思想

在計算機科學中,分治法(Divide&Conquer)是建基於多項分支遞歸的一種很重要的算法範式,快速排序是分治思想在排序問題上的典型應用。

所謂分治思想D&C就是把一個較大規模的問題拆分為若干小規模且相似的問題。再對小規模問題進行求解,最終合併所有小問題的解,從而形成原來大規模問題的解。

字面上的解釋是”分而治之”,這個技巧是很多高效算法的基礎,如排序算法(歸併排序、快速排序)、傅立恭弘=叶 恭弘變換(快速傅立恭弘=叶 恭弘變換)。

分治法中最重要的部分是循環遞歸的過程,每一層遞歸有三個具體步驟:

  • 分解:將原問題分解為若干個規模較小,相對獨立,與原問題形式相同的子問題。
  • 解決:若子問題規模較小且易於解決時,則直接解。否則,遞歸地解決各子問題。
  • 合併:將各子問題的解合併為原問題的解。

快速排序的發明者

查爾斯·安東尼·理查德·霍爾爵士(Sir Charles Antony Richard Hoare縮寫為C. A. R. Hoare,1934年1月11日-),昵稱為東尼·霍爾(Tony Hoare),生於大英帝國錫蘭可倫坡(今斯里蘭卡),英國計算機科學家,圖靈獎得主。

他設計了快速排序算法、霍爾邏輯、交談循序程式。在操作系統中,他提出哲學家就餐問題,併發明用來作為同步程序的監視器(Monitors)以解決這個問題。他同時證明了監視器與信號標(Semaphore)在邏輯上是等價的。

1980年獲頒圖靈獎、1982年成為英國皇家學會院士、2000年因為他在計算機科學與教育方面的傑出貢獻,獲得英國王室頒贈爵士頭銜、2011年獲頒約翰·馮諾依曼獎,現為牛津大學榮譽教授,並在劍橋微軟研究院擔任研究員。

快速排序的基本過程

快速排序使用分治法來把一個序列分為小於基準值和大於基準值的兩個子序列。

遞歸地排序兩個子序列,直至最小的子序列長度為0或者1,整個遞歸過程結束,詳細步驟為:

  • 挑選基準值: 從數列中挑出一個元素稱為基準pivot,選取基準值有數種具體方法,此選取方法對排序的時間性能有決定性影響。
  • 基準值分割: 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面,與基準值相等的數可以到任何一邊,在這個分割結束之後,對基準值的排序就已經完成。
  • 遞歸子序列: 遞歸地將小於基準值元素的子序列和大於基準值元素的子序列排序,步驟同上兩步驟,遞歸終止條件是序列大小是0或1,因為此時該數列顯然已經有序。

快速排序的遞歸實現

    • 版本一 C實現
#include<stdio.h>

int a[9]={5,1,9,6,7,11,3,8,4};

void exchange(int *p,int *q){
    int temp=*p;
    *p=*q;
    *q=temp;
}

int quicksort(int left,int right){
    if(left>=right){
        return 0;
    }

    int i,j,temp;
    temp=a[left];
    i=left;
    j=right;

    while(i!=j){
        while(i<j&&a[j]>=temp){
            j--;
        }
        exchange(&a[i],&a[j]); 
        while(i<j&&a[i]<=temp){
            i++; 
        }
        exchange(&a[i],&a[j]);
    }
    quicksort(i+1,right);
    quicksort(left,i-1); 
}

int main(){
    quicksort(0,8);
    for(int i=0;i<=8;i++){
        printf("%d ",a[i]);
    }
}
    • 版本二 C++實現
 1 #include<iostream>
 2 using namespace std;
 3 
 4 template <typename T>
 5 void quick_sort_recursive(T arr[], int start, int end) {
 6     if (start >= end)
 7         return;
 8     T mid = arr[end];
 9     int left = start, right = end - 1;
10     //整個範圍內搜尋比樞紐值小或大的元素,然後左側元素與右側元素交換
11     while (left < right) {
12             //試圖在左側找到一個比樞紐元更大的元素
13         while (arr[left] < mid && left < right)
14             left++;
15                 //試圖在右側找到一個比樞紐元更小的元素
16         while (arr[right] >= mid && left < right)
17             right--;
18                 //交換元素
19         std::swap(arr[left], arr[right]);
20     }
21         //這一步很關鍵
22     if (arr[left] >= arr[end])
23         std::swap(arr[left], arr[end]);
24     else
25         left++;
26     quick_sort_recursive(arr, start, left - 1);
27     quick_sort_recursive(arr, left + 1, end);
28 }
29 
30 //模板化
31 template <typename T> 
32 void quick_sort(T arr[], int len) {
33     quick_sort_recursive(arr, 0, len - 1);
34 }
35 
36 int main()
37 {
38     int a[9]={5,1,9,6,7,11,3,8,4};
39     int len = sizeof(a)/sizeof(int);
40     quick_sort(a,len-1);
41     for(int i=0;i<len-1;i++)
42         cout<<a[i]<<endl;
43 }

兩個版本均可正確運行,但代碼有一點差異:

  • 版本一 使用雙指針交替從左(右)兩邊分別開始尋找大於基準值(小於基準值),然後與基準值交換,直到最後左右指針相遇。
  • 版本二 使用雙指針向中間集合,左指針遇到大於基準值時則停止,等待右指針,右指針遇到小於基準值時則停止,與左指針指向的元素交換,最後基準值放到合適位置。

過程說起來比較抽象,穩住別慌!靈魂畫手大白會畫圖來演示這兩個過程。

快速排序的遞歸演示

  • 版本一遞歸代碼的排序過程示意圖:

第一次遞歸循環為例:

步驟1: 選擇第一個元素為基準值pivot=a[left]=5,right指針指向尾部元素,此時先由right自右向左掃描直至遇到<5的元素,恰好right起步元素4<5,因此需要將4與5互換位置;

步驟2: 4與5互換位置之後,輪到left指針從左向右掃描,注意一下left的起步指針指向了由步驟1交換而來的4,新元素4不滿足停止條件,因此left由綠色虛箭頭4位置遊走到元素9的位置,此時left找到9>5,因此將此時left和right指向的元素互換,也就是元素5和元素9互換位置;

步驟3: 互換之後right指針繼續向左掃描,從藍色虛箭頭9位置遊走到3的位置,此時right發現3<5,因此將此時left和right指向的元素互換,也就是元素3和元素5互換位置;

步驟4: 互換之後left指針繼續向右掃描,從綠色虛箭頭3位置遊走到6的位置,此時left發現6>5,因此將此時left和right指向的元素互換,也就是元素6和元素5互換位置;

步驟5: 互換之後right指針繼續向左掃描,從藍色虛箭頭6位置一直遊走到與left指針相遇,此時二者均停留在了pivot=5的新位置上,且左右兩邊分成了兩個相對於pivot值的子序列;

循環結束:至此出現了以5為基準值的左右子序列,接下來就是對兩個子序列實施同樣的遞歸步驟。

第二次和第三次左子序列遞歸循環為例:

步驟1-1:選擇第一個元素為基準值pivot=a[left]=4,right指針指向尾部元素,此時先由right指針向左掃描,恰好起步元素3<4,因此將3和4互換;

步驟1-2:互換之後left指針從元素3開始向右掃描,一直遊走到與right指針相遇,此時本次循環停止,特別注意這種情況下可以看到基準值4隻有左子序列,無右子序列,這種情況是一種退化,就像冒泡排序每次循環都將基準值放置到最後,因此效率將退化為冒泡的O(n^2);

步驟1-3:選擇第一個元素為基準值pivot=a[left]=3,right指針指向尾部元素,此時先由right指針向左掃描,恰好起步元素1<3,因此將1和3互換;

步驟1-4:互換之後left指針從1開始向右掃描直到與right指針相遇,此時注意到pivot=3無右子序列且左子序列len=1,達到了遞歸循環的終止條件,此時可以認為由第一次循環產生的左子序列已經全部有序。

循環結束:至此左子序列已經排序完成,接下來對右子序列實施同樣的遞歸步驟,就不再演示了,聰明的你一定get到了。

特別注意:

以上過程中left和right指針在某個元素相遇,這種情況在代碼中是不會出現的,因為外層限制了i!=j,圖中之所以放到一起是為了直觀表達終止條件。

  • 版本二C++版本動畫演示:

 

分析一下:

個人覺得這個版本雖然同樣使用D&C思想但是更加簡潔,從動畫可以看到選擇pivot=a[end],然後左右指針分別從index=0和index=end-1向中間靠攏。

過程中掃描目標值並左右交換,再繼續向中間靠攏,直到相遇,此時再根據a[left]和a[right]以及pivot的值來進行合理置換,最終實現基於pivot的左右子序列形式。

腦補場景:

上述過程讓我覺得很像統帥命令左右兩路軍隊從兩翼會和,並且在會和過程中消滅敵人有生力量(認為是交換元素),直到兩路大軍會師。

此時再將統帥王座擺到正確的位置,此過程中沒有統帥王座的反覆變換,只有最終會師的位置,以王座位中心形成了左翼子序列和右翼子序列。

再重複相同的過程,直至完成大一統。

腦補不過癮 於是湊圖一張:

快速排序的多種版本

吃瓜時間:

印象中2017年初換工作的時候去CBD一家公司面試手寫快排,我就使用C++模板化的版本二實現的,但是面試官質疑說這不是快排,爭辯之下讓我們彼此都覺得對方很Low,於是很快就把我送出門SayGoodBye了^_^。

我想表達的意思是,雖然快排的遞歸版本是基於D&C實現的,但是由於pivot值的選擇不同、交換方式不同等諸多因素,造成了多種版本的遞歸代碼。

並且內層while循環裏面判斷>=還是>(即是否等於的問題),外層循環判斷本序列循環終止條件等寫法都會不同,因此在寫快排時切忌死記硬背,要不然邊界條件判斷不清楚很容易就死循環了。

看下上述我貼的兩個版本的代碼核心部分:

//版本一寫法
while(i!=j){
    while(i<j&&a[j]>=temp){
        j--;
    }
    exchange(&a[i],&a[j]); 
    while(i<j&&a[i]<=temp){
        i++; 
    }
    exchange(&a[i],&a[j]);
}

//版本二寫法
while (left < right) {
    while (arr[left] < mid && left < right)
        left++;
    while (arr[right] >= mid && left < right)
        right--;
    std::swap(arr[left], arr[right]);
}

覆蓋or交換:

代碼中首先將pivot的值引入局部變量保存下來,這樣就認為A[L]這個位置是個坑,可以被其他元素覆蓋,最終再將pivot的值填到最後的坑裡。

這種做法也沒有問題,因為你只要畫圖就可以看到,每次坑的位置是有相同元素的位置,也就是被備份了的元素。

個人感覺 與其叫坑不如叫備份,但是如果你代碼使用的是基於指針或者引用的swap,那麼就沒有坑的概念了。

這就是覆蓋和交換的區別,本文的例子都是swap實現的,因此沒有坑位被最後覆蓋一次的過程。

快速排序的迭代實現

所謂迭代實現就是非遞歸實現一般使用循環來實現,我們都知道遞歸的實現主要是藉助系統內的棧來實現的。

如果調用層級過深需要保存的臨時結果和關係會非常多,進而造成StackOverflow棧溢出。

Stack一般是系統分配空間有限內存連續速度很快,每個系統架構默認的棧大小不一樣,筆者在x86-CentOS7.x版本使用ulimit -s查看是8192Byte。

避免棧溢出的一種辦法是使用循環,以下為筆者驗證的使用STL的stack來實現的循環版本,代碼如下:

#include <stack>
#include <iostream>
using namespace std;

template<typename T>
void qsort(T lst[], int length) {
    std::stack<std::pair<int, int> > mystack;
    //將數組的首尾下標存儲 相當於第一輪循環
    mystack.push(make_pair(0, length - 1));

    while (!mystack.empty()) {
        //使用棧頂元素而後彈出
        std::pair<int,int> top = mystack.top();
        mystack.pop();

        //獲取當前需要處理的子序列的左右下標
        int i = top.first;
        int j = top.second;

        //選取基準值
        T pivot = lst[i];

        //使用覆蓋填坑法 而不是交換哦
        while (i < j) {
            while (i < j and lst[j] >= pivot) j--;
            lst[i] = lst[j];
            while (i < j and lst[i] <= pivot) i++;
            lst[j] = lst[i];
        }
        //注意這個基準值回填過程
        lst[i] = pivot;

        //向下一個子序列進發
        if (i > top.first) mystack.push(make_pair(top.first, i - 1));
        if (j < top.second) mystack.push(make_pair(j + 1, top.second));
    }
}

int main()
{
    int a[9]={5,1,9,6,7,11,3,8,4};
    int len = sizeof(a)/sizeof(int);
    qsort(a,len);
    for(int i=0;i<len-1;i++)
        cout<<a[i]<<endl;
}

下期精彩

由於篇幅原因,目前文章已經近6000字,因此筆者決定將快排算法的優化放到另外一篇文章中,不過可以提前預告一下會有哪些內容:

  • 基準值選擇對性能的影響
  • 基準值選擇的多種策略
  • 尾遞歸的概念原理和優勢
  • 基於尾遞歸實現快速排序
  • STL中sort函數的底層實現
  • glibc中快速排序的實現

參考資料

 

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

碼農當自強

碼農當自強

導航

  • 初出茅廬
  • 跳槽才能漲薪
  • 力拔山兮氣蓋世
  • 止步中層
  • 契約精神?聯盟
  • 技工?匠人
  • 碼農當自強

  有人的地方就有江湖。有江湖必有俠客。IT人的江湖水生草闊,從來都盛產俠客和隱士。很多人離開這片江湖,沒有留下自己的故事,而那些有故事的終究成了傳說。

初出茅廬

  本文的主人公木木君,2011年畢業於一個普通二本大學的計算機專業。那年六月,他懷揣夢想,來到西部的一座准一線城市。

  “天高任鳥飛,海闊憑魚游”。同大多數應屆畢業生一樣,木木君懷揣夢想,滿腔熱心,對未來充滿希冀,希望能夠在這座大城市打拚出自己的一片天地。“求突破,求提高,求發展”,這是他給自己設定的未來五年計劃,分三個步驟執行。

  IT行業的門檻向來很高,大多數民企很少招生應屆生,特別是非985,211大學的童鞋,容易碰壁。木木君面試了20家大大小小的公司,花了一個多月,總算找了個正規公司。這是一家生產機頂盒的工廠,幾千人的公司,僅有不到50人的研發團隊。木木君在這裏開啟了自己的IT職業生涯。團隊不比正規的軟件公司,但同事和睦相處,也能夠接觸到實際的開發項目,總算有所突破。

  “笨鳥先飛”。木木君憑藉自己的努力,半年之內就把部門內部的項目都摸了一遍,並在原有基礎上增加了很多功能。

跳槽才能漲薪

  IT江湖潛規則之跳槽才能漲薪。

  有一天,木木君和往常一樣正在配合運維同事改進新的OA系統。“滴滴滴~”,一波QQ消息來襲。木木君點開消息,是師兄。木木君和這個師兄其實不是一個專業,師兄比他高一個年級,因為大學被同一個老師帶着一起做過項目,經常串寢室,比較熟悉。師兄告訴他,他換工作了,這是他兩年來第三次換工作,在軟件園,月薪8K。聊天完畢,木木君沉思良久,開始對高大上的軟件園產生嚮往。

  在第一家公司待了11個月,木木君選擇了離開,換到了一家軟件園的公司,並有機會和華為的工程師一起合作開發項目。值得一提的是,這次跳槽工資幾乎翻了一倍。

  新公司是行業里排名靠前的,公司制度規範,開發流程標準。團隊里研發同事嚴謹,當然壓力也很大。

  剛進公司的前兩個月,壓力挺大,見識了以前沒有接觸過的框架和組件,以及很多聽不懂的術語。每晚和同事加班到8點半,有時甚至11點才從公司離開,儘管很累,但是能夠感受的技術和經驗上的進步。

  在這家公司待了兩年。見識 了一些牛人,甚至有些一個人頂一個小團隊的人。華為的狼性文化在木木君心中打下烙印,深入血液,變成做事風格的一部分。在以後的工作中,也是秉持了這種文化。

力拔山兮氣蓋世

  大多數碼農,在工作三年左右能夠迎來自己的一個技術上的小高峰。

  木木君在第二家公司待了兩年感覺就像到山上跟了一個武林高手學了一身本領,瞬間有了自信。離職的時候,我和我同事還開玩笑說,出去之後至少都是8K…

  “移動互聯網時代已經來臨,站在風口上豬也能飛”。那一年,手機APP大行其道,獨領風騷,是個公司都想做APP。y也是在那一年小米火了,華為剛開始邁入手機領域。進入第三家公司,是一個不到一百人的軟件公司,做旅遊APP的,期望在這裡能夠接觸到移動端。

  “圈子不同,不硬融”。木木君在這家公司真正見識了什麼是公司內耗。小幫派林立,你再努力也無法融入。一年不到,領導換了幾茬,還玩的是家天下。

止步中層

  對大多數人而言,職場的中層就是天花板。

  “天下之大,竟無用武之地”。就像一個武林俠客,讓他困惑不是練武的孤獨和寂寞,而是竟然沒有用武之地。

  “此處不留爺,自有留爺處”。很快,木木君便進入一件互聯網產品公司,主營電商相關Saas軟件。這裏部門分工明確,需求,產品研發,安全,運維,DBA,測試一應俱全。公司產品成熟,客戶穩定,可以學習真正的大數據和自動化運維。

  在這裏團隊從零開始搭建自動化平台,並逐步迭代,一路摸爬滾打,基本能夠滿足公司100多台服務器的自動化運維。

  木木君一腔熱情,終於受到領導器重,升入中層。團隊不大,但是業務不少,支撐多個部門的系統研發和運維,幾乎人人都掛了2+項目。身為leader的木木君,更是不在話下。特殊時期,幾乎一人承擔一個團隊開發任務。甚至非常時期通宵支持…

  四年過去,木木君也進入了而立之年。2018年,經濟不景氣的一年…公司漲薪已經渺茫…陸續身邊逐漸有人跳槽。不到半年,身邊已經有三個人跳槽,其中一個老領導走了留下一句“待了六年,已經沒有上升空間”。

契約精神?聯盟

  我們是一個團隊,不是一個家庭。

  企業跟員工應該是一種什麼樣的關係?

  領英的執行總裁在《聯盟》這本書中開頭就寫道:“我們是一個團隊,不是一個家庭”。

  近期屢屢爆出的HR被辭退事件和員工因患病被辭退事件,引起了輿論共鳴。但是希望大家認識到員工和公司的關係是合作和聯盟關係。未來更是如此…

技工?匠人

  碼農,一群靠技術謀生的人。只是在互聯網的光環加持下,變得“高精尖”。但是,放在歷史的長河中來看,也不過是特定時代的勞動者。他們和上一個時代的磚瓦工,木匠其實沒有太大差別。是社會生產力發展到一定階段,一種工種對另一種工種的替代。

  互聯網給技術人帶來紅利,容易讓技術人感到天生的優越感,再加上身處大廠就容易自我感覺良好。

  吳軍博士對的碼農層級的分類,可以看出,技術人的發展方向不只是在技術本身,還要具備綜合能力。

  • 第五級:能獨立解決問題,完成工程工作。

  • 第四級:能指導和帶領其他人一同完成更有影響力的工作。

  • 第三級:能獨立設計和實現產品,並且在市場上獲得成功。

  • 第二級:能設計和實現別人不能做出的產品,也就是說他的作用很難取代。

  • 第一級:開創一個產業。

  試問諸君在第幾層?

  說到底,技術人應該秉持匠人精神

碼農當自強

  生活不止眼前的苟且,還有詩和遠方。

  2019年各大公司的財報,都反應很多公司效益差強人意。很多互聯網公司也在尋找新的風口和增長點。而立之年的木木君,雖然躊躇滿志,但再次陷入迷茫。深處IT行業多年,幾乎五年一個風口,技術行業更新快,玩的是創新和顛覆。移動互聯網時代是如何革傳統行業的命,木木君歷歷在目…

  同時,各大媒體充斥着中年危機的推文,一時間人人自危,催生了各種打着知識旗號販賣焦慮的二道販子。不禁讓木木君想起了之前有個大神的文章《屌絲的出路》。那個大神也是一段傳奇。

  很多碼農都在焦慮,但是又不知道如何做?技術人有一個通病,純粹。這不是缺點,但是生活是多元的,要多主動接觸技術之外的世界。

  • 副業

  同時,可以多一些副業嘗試。比如,和朋友一起接一些項目。創建自己的博客。口才好的,可以錄一些技術教學視頻放到網上。

  • 健身

  身體是革命的本錢,這裏的健身不是一定要去健身房,而是要學會鍛煉身體和合理作息。

  • 投資

  年輕的時候,做一點投資和理財。投資房產也是不錯的選擇。投資可以為未來增加一筆資產。

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

原創|我是如何從零學習開發一款跨平台桌面軟件的(Markdown編輯器)

原始衝動

最近一直在學習 Electron 開發桌面應用程序,目的是想做一個桌面編輯器,雖然一直在使用Typora這款神器,但無奈Typora太過國際化,在國內水土不服,無法滿足我的一些需求。

比如實現本地圖片上傳到雲端(mac版可以藉助iPic),無法幫我把本地圖片和文章一起發布到博客園、CSDN、SegmentFault、掘金等國內知名博客平台,要麼使用一些免費或付費的圖床,藉助類似iPic的工具,把圖片一鍵上傳到雲端。

我個人也嘗試過七牛雲的免費10G存儲空間,但是說實話,這些免費的空間到最後一定是為了讓你成為付費用戶,各種限制各種吐槽在網上很容易可以搜索到。

免費的圖床如新浪微博等,還算是比較好的圖床工具,相比一些網絡上的壓根不知道啥公司甚至是歸屬個人的免費圖床,新浪應該是比較靠譜的,相對來說可以保證圖片的存活時間,我個人用過一些免費的圖床網站,記得印象深刻的就是服務器出問題,網站掛個公告,曾經的圖片再去訪問就是默認的404。

雖然新浪家大業大不是說倒閉就倒閉的,圖片相對穩定可靠,不過新浪的圖片服務器會檢測訪問來源Referer來防止外部網站引用,造成訪問403。

總結起來就是一句話,圖片還是隨着文章一鍵發布到博客平台比較好。要丟一起丟~

心理掙扎

緣起這個動機,但是下定決心依舊是困難重重。

我個人是一個Java工程師,雖說搞過Andorid、HTML前端,但對前端深感不適的我果斷放棄了。對於桌面程序開發,我連Swing都不會,造一個Markdown編輯器有點難,何況還要加上這些定製功能。

猶猶豫豫,還是決定去嘗試一下。於是調研寫跨平台的一些途徑。

先嘗試Swing,不過Swing不好實現我期望的一些功能,改成JavaFX倒是可以,不過說實話,寫起來很累,太過繁瑣,就放棄了。最後把目光瞄向electron,就它了,HTML+Js+Css,聽起來就很簡單,事實證明,無論是測試還是打包都很方便。

決定之後,便開始進行 Electron 的系統學習。

邁出第一步

第一步就是安裝 Electron 的本地開發環境,這也是大多數應用開發的第一步。

你需要安裝 Node.js 在你的本地電腦,Electron 也是依賴於 Node.js 的環境,嚴格來說, Electron 通過將 Chromium 和 Node.js 合併到同一個運行時環境中,並將其打包為Mac,Windows和Linux系統下的應用來實現這一目的。

關於 Electron 的具體開發流程,這裏不再贅述,你完全可以在開發中使用Web前端開發的思維,除了在處理多個窗口之間交互的時候,就不得不了解Eelctron的進程機制。

主進程和渲染進程

Electron 運行 package.json 的 main 腳本的進程被稱為主進程。 在主進程中運行的腳本通過創建web頁面來展示用戶界面。 一個 Electron 應用總是有且只有一個主進程。

由於 Electron 使用了 Chromium 來展示 web 頁面,所以 Chromium 的多進程架構也被使用到。 每個 Electron 中的 web 頁面運行在它自己的渲染進程中。

在普通的瀏覽器中,web頁面通常在沙盒環境中運行,並且無法訪問操作系統的原生資源。 然而 Electron 的用戶在 Node.js 的 API 支持下可以在頁面中和操作系統進行一些底層交互。

主進程與渲染進程的區別

主進程使用 BrowserWindow 實例創建頁面。 每個 BrowserWindow 實例都在自己的渲染進程里運行頁面。 當一個 BrowserWindow 實例被銷毀后,相應的渲染進程也會被終止。

主進程管理所有的web頁面和它們對應的渲染進程。 每個渲染進程都是獨立的,它只關心它所運行的 web 頁面。

在頁面中調用與 GUI 相關的原生 API 是不被允許的,因為在 web 頁面里操作原生的 GUI 資源是非常危險的,而且容易造成資源泄露。 如果你想在 web 頁面里使用 GUI 操作,其對應的渲染進程必須與主進程進行通訊,請求主進程進行相關的 GUI 操作。

主進程與渲染進程通信

那麼進程間如何通訊?

Electron為主進程( main process)和渲染器進程(renderer processes)通信提供了多種實現方式,如可以使用ipcRenderer 和 ipcMain模塊發送消息,使用 remote模塊進行RPC方式通信。

你還可以用 Electron 內的 IPC 機制實現。將數據存在主進程的某個全局變量中,然後在多個渲染進程中使用 remote 模塊來訪問它。

示例代碼:

// 在主進程中
global.sharedObject = {
  someProperty: 'default value'
}
// 在第一個頁面中
require('electron').remote.getGlobal('sharedObject').someProperty = 'new value'
// 在第二個頁面中
console.log(require('electron').remote.getGlobal('sharedObject').someProperty)

使用Electron的API

Electron在主進程和渲染進程中提供了大量API去幫助開發桌面應用程序, 在主進程和渲染進程中,你可以通過require的方式將其包含在模塊中以此,獲取Electron的API

const electron = require('electron')

所有Electron的API都被指派給一種進程類型。 許多API只能被用於主進程或渲染進程中,但其中一些API可以同時在上述兩種進程中使用。 每一個API的文檔都將聲明你可以在哪種進程中使用該API。

Electron中的窗口是使用BrowserWindow類型創建的一個實例, 它只能在主進程中使用。

// 這樣寫在主進程會有用,但是在渲染進程中會提示'未定義'
const { BrowserWindow } = require('electron')

const win = new BrowserWindow()

因為進程之間的通信是被允許的, 所以渲染進程可以調用主進程來執行任務。 Electron通過remote模塊暴露一些通常只能在主進程中獲取到的API。 為了在渲染進程中創建一個BrowserWindow的實例,通常使用remote模塊為中間件:

// 這樣寫在渲染進程中時行得通的,但是在主進程中是'未定義'
const { remote } = require('electron')
const { BrowserWindow } = remote

const win = new BrowserWindow()

使用Node.js的API

Electron同時在主進程和渲染進程中對Node.js 暴露了所有的接口。 這裡有兩個重要的定義:

1)所有在Node.js可以使用的API,在Electron中同樣可以使用。 在Electron中調用如下代碼是有用的:

const fs = require('fs')

const root = fs.readdirSync('/')

// 這會打印出磁盤根級別的所有文件
// 同時包含'/'和'C:\'。
console.log(root)

2)你可以在你的應用程序中使用Node.js的模塊。 選擇您最喜歡的 npm 模塊。 npm 提供了目前世界上最大的開源代碼庫,那裡包含良好的維護、經過測試的代碼,提供給服務器應用程序的特色功能也提供給Electron。

例如,在你的應用程序中要使用官方的AWS SDK,你需要首先安裝它的依賴:

npm install --save aws-sdk

然後在你的Electron應用中,通過require引入並使用該模塊,就像構建Node.js應用程序那樣:

// 準備好被使用的S3 client模塊
const S3 = require('aws-sdk/clients/s3')

有一個非常重要的提示: 原生Node.js模塊 (即指,需要編譯源碼過後才能被使用的模塊) 需要在編譯后才能和Electron一起使用。

最終產品殺青落地

終於搞明白了 Electron 的應用架構,那麼接着就要進入產品的開發階段。比較慶幸的是,ELectron 的UI完全由CSS+HTML組成,這部分可用的框架太多了,我選擇了又老又知名的 BootStarp 框架搭建界面UI,還引用了JS框架JQuery。選擇了 electron-store 作為本地存儲文件,至於最關鍵的Markdown語法解析,對比了一番主流解析框架,最終選擇了 markdown-it。貼一下效果圖:

這款軟件我給他起名為 JustWrite,意思就是現在就寫,也是在督促自己吧,畢竟猶豫徘徊,等於白來。

現在軟件的功能除了包含一鍵發布本地文章加本地圖片到博客園、CSDN、SegmentFault、掘金、開源中國等平台,我還打算將他打造為一個體驗不錯的Markdown寫作軟件。現在你閱讀的這篇文章,就是我使用 JustWrite 書寫的,使用的字體是我個人喜歡的幼圓體,除此之外,還有六款風格迥異的字體可以切換使用。字號也是可以動態放大或者縮小,還可以關閉右側預覽,專註於寫作,如下圖所示:

這些截圖是我截屏后使用快捷鍵Ctrl+V一鍵粘貼的,圖片會自動放到當前md文件所在目錄下的picture文件夾內。

關於 JustWrite 從構思到實踐的心路歷程大致就以上這些了,這次開發 JustWrite 也讓我過了一把產品經理的癮,基本已經滿足了我的日常需求。如果你有更好的想法和創意也可以告訴我,說不定第二天就會實現了。

Github:

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

※公開收購3c價格,不怕被賤賣!

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

不只是換殼上市!EC-05 與 Gogoro S2 實測比較

EC-05 由台灣山葉(YAMAHA)和 Gogoro 聯手打造,動力系統採用與 Gogoro S2 相同的 G2 鋁合金水冷永磁同步馬達(S-Version),許多人不禁疑問 EC-05 和 Gogoro S2 到底有什麼差別?難道只是換了一個車殼?更別說 EC-05 的價格還比 Gogoro S2 貴了 2,820 元。為了比較其中的差別,《科技新報》試駕了 EC-05 和 2020 年式的 Gogoro S2,帶來兩台車的第一手觀察。

EC-05 配備嵌合式環型頭燈,能在夜間騎乘時避免光線發散,確保前方視野的明亮程度。Gogoro S2 同樣配備環型頭燈,不過形狀略有不同,和一般機車一樣完全外露。EC-05 的後照鏡為菱形設計,視野較為寬廣,Gogoro S2 的後照鏡是圓形,視野較狹窄,而且騎乘者容易被自己的身體擋住部分視線。

Gogoro S2 前方配備了多功能置物箱,可以擺放手機和手套等小型物品,也能做為飲料置杯架使用,EC-05 就沒有這項裝置。Gogoro S2 的前方置物箱內附 USB 充電插槽,EC-05 的 USB 插孔則位於後座的置物箱。EC-05 使用無線智慧鑰匙,靠近車輛後按壓鑰匙的灰色部分進行解鎖。Gogoro S2 則使用 iQ system 智慧鑰匙卡,只要輕觸 S 標誌就能解鎖。

EC-05(左)配備嵌合式環型頭燈,Gogoro S2 的頭燈則外露。

EC-05 後照鏡呈菱形,視野較寬廣。

Gogoro S2 的後照鏡為圓形。

Gogoro S2 配備多功能置物箱,並附有 USB 充電插槽。

EC-05 的 USB 充電插槽位於後置物箱內側。

EC-05 以無線智慧鑰匙解鎖。

試乘時騎行陽明山的山路,EC-05 的穩定度和避震能力優於 Gogoro S2,在路面凹凸不平時較不容易顛簸,雙載騎乘時的差距更加明顯。EC-05 的座墊將後座的後方略為墊高,讓座墊呈現「山」字型的結構,側邊則採切削設計,讓騎乘者的腿部更為舒適。Gogoro S2 的後座較為平滑,中間使用皮質材質並標有「S」字樣。雖然乍看之下差異不大,但實際騎乘時後座乘客的感受明顯不同。

EC-05 的乘客在機車減速和加速時都更容易維持穩定,比較不會有往後飛出去或往前貼到前方騎乘者身上的狀況,即使長時間乘坐也不會滑動。Gogoro S2 的後座把手較為平直,EC-05 的把手彎曲向上,對於手不夠長的乘客更容易抓握。相較於 Gogoro S2,EC-05 能給予後座乘客更安穩的乘坐體驗。

EC-05 的座墊呈現「山」字型的凸起,讓後座乘客乘坐時更穩定。

Gogoro S2 的座墊較為平滑。

EC-05 與 Gogoro S2 座墊比較。

EC-05 的後座把手彎曲向上,較容易抓握。

Gogoro S2 的後座把手平直,手沒那麼長的後座乘客較難抓握。

EC-05(右)與 Gogoro S2 的後座把手差異從車尾可以明顯看出。

由於 EC-05 與 Gogoro S2 採用相同的動力系統,因此性能上的數據相差無幾。EC-05 最大馬力為 10hp,安全極速達到時速 90 公里,靜止加速到時速 50 公里需要 3.9 秒。Gogoro S2 最大馬力為 10.18hp,安全極速達到時速 92 公里,靜止加速到時速 50 公里僅需 3.8 秒,各方面都是 Gogoro S2 稍勝。

相較於 Gogoro S2,EC-05 的儀表板略為調整傾斜角度並向前方移動,用以減少騎乘者所需的視線移動。EC-05 的腳踏板具有菱形的凸起幫助止滑,讓騎乘時腳的位置可以更穩定。Gogoro S2 的腳踏板則是完全平滑,並有著「gogoro」的字樣。

EC-05 在車及座墊高度上也有更動,車高為 1,180 mm 略高於 Gogoro S2 的 1,080 mm,座高則以 768 mm 略低於 Gogoro S2 的 780 mm。調整過後 EC-05 在龍頭和座墊間的空間增加,騎乘時較為舒適也不容易意外按壓到喇叭等按鍵。不過 EC-05 的龍頭相較 Gogoro S2 略為沉重,力氣較小的使用者操控時可能會稍微費力。

EC-05 的腳踏板具有止滑的菱形格紋。

Gogoro S2 的腳踏板較為平滑。

身高 176 公分的騎乘者與 EC-05 的比例。

身高 156 公分的騎乘者與 EC-05 的比例。

YAMAHA 的設計理念標榜「人機官能」,也就是在設計時結合機械結構和人體工學。這樣的概念讓 EC-05 看似與 Gogoro S2 僅有微幅的差異,但這些細節卻在騎乘時能發揮不成比例的效果。至於這樣的差別值不值得多花 2,820 元購買,那就要看個人是否在意這些細節帶來的感受。

EC-05 提供藍灰色、深黑色、深藍灰色和白銀色 4 種顏色讓消費者選擇,定價為台幣 99,800 元。Gogoro S2 僅有灰黑色一個款式,定價為台幣 96,980 元。

(合作媒體:。圖片來源:)

延伸閱讀:

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

【集合系列】- 深入淺出的分析IdentityHashMap

一、摘要

在集合系列的第一章,咱們了解到,Map 的實現類有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。

應該有很多人不知道 IdentityHashMap 的存在,其中不乏工作很多年的 Java 開發者,本文主要從數據結構和算法層面,探討 IdentityHashMap 的實現。

二、簡介

IdentityHashMap 的數據結構很簡單,底層實際就是一個 Object 數組,但是在存儲上並沒有使用鏈表來存儲,而是將 K 和 V 都存放在 Object 數組上。

當添加元素的時候,會根據 Key 計算得到散列位置,如果發現該位置上已經有改元素,直接進行新值替換;如果沒有,直接進行存放。當元素個數達到一定閾值時,Object 數組會自動進行擴容處理。

打開 IdentityHashMap 的源碼,可以看到 IdentityHashMap 繼承了AbstractMap 抽象類,實現了Map接口、可序列化接口、可克隆接口。

public class IdentityHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, java.io.Serializable, Cloneable
{
    /**默認容量大小*/
    private static final int DEFAULT_CAPACITY = 32;
    
    /**最小容量*/
    private static final int MINIMUM_CAPACITY = 4;
    
    /**最大容量*/
    private static final int MAXIMUM_CAPACITY = 1 << 29;
    
    /**用於存儲實際元素的表*/
    transient Object[] table;
    
    /**數組大小*/
    int size;

    /**對Map進行結構性修改的次數*/
    transient int modCount;

    /**key為null所對應的值*/
    static final Object NULL_KEY = new Object();
    
    ......
}

可以看到類的底層,使用了一個 Object 數組來存放元素;在對象初始化時,IdentityHashMap 容量大小為64

public IdentityHashMap() {
        //調用初始化方法
        init(DEFAULT_CAPACITY);
}
private void init(int initCapacity) {
        //數組大小默認為初始化容量的2倍
        table = new Object[2 * initCapacity];
}

三、常用方法介紹

3.1、put方法

put 方法是將指定的 key, value 對添加到 map 里。該方法首先會對map做一次查找,通過==判斷是否存在key,如果有,則將舊value返回,將新value覆蓋舊value;如果沒有,直接插入,數組長度+1,返回null

源碼如下:

public V put(K key, V value) {
        //判斷key是否為空,如果為空,初始化一個Object為key
        final Object k = maskNull(key);

        retryAfterResize: for (;;) {
            final Object[] tab = table;
            final int len = tab.length;
            //通過key、length獲取數組小編
            int i = hash(k, len);
            
            //循環遍歷是否存在指定的key
            for (Object item; (item = tab[i]) != null;
                 i = nextKeyIndex(i, len)) {
                 //通過==判斷,是否數組中是否存在key
                if (item == k) {
                        V oldValue = (V) tab[i + 1];
                        //新value覆蓋舊value
                    tab[i + 1] = value;
                    //返回舊value
                    return oldValue;
                }
            }
            
            //數組長度 +1
            final int s = size + 1;
            //判斷是否需要擴容
            if (s + (s << 1) > len && resize(len))
                continue retryAfterResize;

            //更新修改次數
            modCount++;
            //將k加入數組
            tab[i] = k;
            //將value加入數組
            tab[i + 1] = value;
            size = s;
            return null;
        }
}

maskNull 函數,判斷 key 是否為空

private static Object maskNull(Object key) {
        return (key == null ? NULL_KEY : key);
}

hash 函數,通過 key 獲取 hash 值,結合數組長度通過位運算獲取數組散列下標

private static int hash(Object x, int length) {
        int h = System.identityHashCode(x);
        // Multiply by -127, and left-shift to use least bit as part of hash
        return ((h << 1) - (h << 8)) & (length - 1);
}

nextKeyIndex 函數,通過 hash 函數計算得到的數組散列下標,進行加2;因為一個 key、value 都存放在數組中,所以一個 map 對象佔用兩個數組下標,所以加2。

private static int nextKeyIndex(int i, int len) {
        return (i + 2 < len ? i + 2 : 0);
}

resize 函數,通過數組長度,進行擴容處理,擴容之後的長度為當前長度的2倍

private boolean resize(int newCapacity) {
        //擴容后的數組長度,為當前數組長度的2倍
        int newLength = newCapacity * 2;

        Object[] oldTable = table;
        int oldLength = oldTable.length;
        if (oldLength == 2 * MAXIMUM_CAPACITY) { // can't expand any further
            if (size == MAXIMUM_CAPACITY - 1)
                throw new IllegalStateException("Capacity exhausted.");
            return false;
        }
        if (oldLength >= newLength)
            return false;

        Object[] newTable = new Object[newLength];
        //將舊數組內容轉移到新數組
        for (int j = 0; j < oldLength; j += 2) {
            Object key = oldTable[j];
            if (key != null) {
                Object value = oldTable[j+1];
                oldTable[j] = null;
                oldTable[j+1] = null;
                int i = hash(key, newLength);
                while (newTable[i] != null)
                    i = nextKeyIndex(i, newLength);
                newTable[i] = key;
                newTable[i + 1] = value;
            }
        }
        table = newTable;
        return true;
}

3.2、get方法

get 方法根據指定的 key 值返回對應的 value。同樣的,該方法會循環遍曆數組,通過==判斷是否存在key,如果有,直接返回value,因為 key、value 是相鄰的存儲在數組中,所以直接在當前數組下標+1,即可獲取 value;如果沒有找到,直接返回null

值得注意的地方是,在循環遍歷中,是通過==判斷當前元素是否與key相同,如果相同,則返回value。咱們都知道,在 java 中,==對於對象類型參數,判斷的是引用地址,確切的說,是堆內存地址,所以,這裏判斷的是key的引用地址是否相同,如果相同,則返回對應的 value;如果不相同,則返回null

源碼如下:

public V get(Object key) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
        
        //循環遍曆數組,直到找到key或者,數組為空為值
        while (true) {
            Object item = tab[i];
            //通過==判斷,當前數組元素與key相同
            if (item == k)
                return (V) tab[i + 1];
            //數組為空
            if (item == null)
                return null;
            i = nextKeyIndex(i, len);
        }
}

3.3、remove方法

remove 的作用是通過 key 刪除對應的元素。該方法會循環遍曆數組,通過==判斷是否存在key,如果有,直接將keyvalue設置為null,對數組進行重新排列,返回舊 value。

源碼如下:

public V remove(Object key) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);

        while (true) {
            Object item = tab[i];
            if (item == k) {
                modCount++;
                //數組長度減1
                size--;
                    V oldValue = (V) tab[i + 1];
                //將key、value設置為null
                tab[i + 1] = null;
                tab[i] = null;
                //刪除該元素后,需要把原來有衝突往後移的元素移到前面來
                closeDeletion(i);
                return oldValue;
            }
            if (item == null)
                return null;
            i = nextKeyIndex(i, len);
        }
}

closeDeletion 函數,刪除該元素后,需要把原來有衝突往後移的元素移到前面來,對數組進行重寫排列;

private void closeDeletion(int d) {
        // Adapted from Knuth Section 6.4 Algorithm R
        Object[] tab = table;
        int len = tab.length;

        Object item;
        for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
             i = nextKeyIndex(i, len) ) {
            int r = hash(item, len);
            if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {
                tab[d] = item;
                tab[d + 1] = tab[i + 1];
                tab[i] = null;
                tab[i + 1] = null;
                d = i;
            }
        }
}

四、總結

  1. IdentityHashMap 的實現不同於HashMap,雖然也是數組,不過IdentityHashMap中沒有用到鏈表,解決衝突的方式是計算下一個有效索引,並且將數據keyvalue緊挨着存在map中,即table[i]=keytable[i+1]=value

  2. IdentityHashMap 允許keyvalue都為null,當keynull的時候,默認會初始化一個Object對象作為key

  3. IdentityHashMap在保存、刪除、查詢數據的時候,以key為索引,通過==來判斷數組中元素是否與key相同,本質判斷的是對象的引用地址,如果引用地址相同,那麼在插入的時候,會將value值進行替換;

IdentityHashMap 測試例子:

public static void main(String[] args) {
        Map<String, String> identityMaps = new IdentityHashMap<String, String>();

        identityMaps.put(new String("aa"), "aa");
        identityMaps.put(new String("aa"), "bb");
        identityMaps.put(new String("aa"), "cc");
        identityMaps.put(new String("aa"), "cc");
        //輸出添加的元素
        System.out.println("數組長度:"+identityMaps.size() + ",輸出結果:" + identityMaps);
    }

輸出結果:

數組長度:4,輸出結果:{aa=aa, aa=cc, aa=bb, aa=cc}

儘管key的內容是一樣的,但是key的堆地址都不一樣,所以在插入的時候,插入了4條記錄。

五、參考

1、JDK1.7&JDK1.8 源碼

2、

3、

作者:炸雞可樂
出處:

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

小白理解安卓虛擬機以及華為的’諾亞方舟’

虛擬機提到虛擬機,大家可能第一反應就是java中好像有虛擬機這個玩意。但是安卓中的虛擬機是什麼呢?是和java一樣的嗎?那麼我們先來了解一下java中的JVM!

JVM,搞java的肯定對它了解不少。JVM本質上就是一個軟件,是計算機硬件的一層軟件抽象,在這之上才幹夠運行Java程序,JAVA在編譯後會生成相似於彙編語言的JVM字節碼,與C語言編譯后產生的彙編語言不同的是,C編譯成的彙編語言會直接在硬件上跑。但JAVA編譯後生成的字節碼是在JVM上跑,須要由JVM把字節碼翻譯成機器指令。才幹使JAVA程序跑起來。JVM運行在操作系統上,屏蔽了底層實現的差異。從而有了JAVA吹噓的平台獨立性和Write Once Run Anywhere。依據JVM規範實現的詳細虛擬機有幾十種,主流的JVM包括Hotspot、Jikes RVM等。都是用C/C++和彙編編寫的,每一個JRE編譯的時候針對每一個平台編譯。因此下載JRE(JVM、Java核心類庫和支持文件)的時候是分平台的,JVM的作用是把平台無關的.class裏面的字節碼翻譯成平台相關的機器碼,來實現跨平台。

說白了,簡單點,就是:

                                                                      Java

                                                                      .java文件 -> .class文件 -> .jar文件

最後執行是class文件,有的會被再次打包成jar文件。

了解了這些之後,我們再去了解Android 中的虛擬機。

一、Dalvik虛擬機

Dalvik虛擬機( Dalvik Virtual Machine ),簡稱Dalvik VM或者DVM。這就是Android中的虛擬機。最初它的產生,是因為Google為了解決與Oracle之間關於Java相關專利和授權的糾紛,開發了DVM。

Android既然存在虛擬機,肯定也是在這個DVM上執行的。它的執行流程和JVM很像:

                                                                       Android

                                                                      .java文件 –> .class文件 -> .dex文件->.apk

DVM執行的是.dex格式文件,JVM執行的是.class文件,android程序編譯完之後生產.class文件,然後,dex工具會把.class文件處理成.dex文件,然後把資源文件和.dex文件等打包成.apk文件,apk就是android package的意思。

除了上面所說的,專利授權的原因除外,其實還有因為如下原因:

    dvm是基於寄存器的虛擬機,而jvm是基於虛擬棧的虛擬機。寄存器存取速度比棧快得多,dvm可以根據硬件實現最大的優化,比較適合移動設備。

    class文件存在很多的冗餘信息,dex工具會去除冗餘信息,並把所有的.class文件整合到.dex文件中,減少了I/O操作,提高了類的查找速度。

不光是上面這些差異,還有運行環境。  

   Dalvik : 一個應用啟動都運行一個單獨的虛擬機運行在一個單獨的進程中

   JVM: 只能運行一個實例, 也就是所有應用都運行在同一個JVM中

 

 這個是早先的安卓虛擬機,運行速度還是相當慢的。基於寄存器的虛擬機允許更快的執行時間,但代價是編譯后的程序更大。於是新的Dex字節碼格式odex產生了。它的作用等同於dex,只不過是dex優化后的格式。在App安裝的過程中,會通過Socket向/system/bin/install進程發送dex_opt的指令,對Dex文件進行優化。在DexClassLoader動態加載Dex文件時,也會進行Dex的優化,形成odex文件。

 

為了適應硬件速度的提升,隨後在Android 2.2的DVM中加入了JIT 編譯器(Just-In-Time Compiler)。Dalvik 使用 JIT 進行即時編譯,藉助 Java HotSpot VM,JIT 編譯器可以對執行次數頻繁的 dex/odex 代碼進行編譯與優化,將 dex/odex 中的 Dalvik Code(Smali 指令集)翻譯成相當精簡的 Native Code 去執行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。

JIT編譯器的引入,提升了安裝速度,減少了佔用的空間,但隨之帶來的問題就是:多個dex加載會非常慢;JIT中的解釋器解釋的字節碼會帶來CPU和時間的消耗;還有熱點代碼的Monitor一直在運行帶來電量的損耗。

 

 這種情況下,手機動不動就卡是難以避免的。相信各位如果那時候用着Android手機,一定印象非常深刻。因為並不是那麼好用。

這樣的狀況一直持續到Andorid 4.4,帶來了全新的虛擬機運行環境 ART(Android RunTime)的預覽版和全新的編譯策略 AOT(Ahead-of-time)。但那時候。 ART 是和 Dalvik 共存的,用戶可以在兩者之間進行選擇(感覺很奇怪,作為一個愛好者,我當時看到這個東西可以切換都是不曉得是什麼玩意,用戶可都是小白啊,沒有必要共存的吧)。在Android 5.0的時候,ART 全面取代 Dalvik 成為 Android 虛擬機運行環境,至此。Dalvik 退出歷史舞台,AOT 也成為唯一的編譯模式。

二、ART 

AOT 和 JIT 的不同之處在於:JIT 是在運行時進行編譯,是動態編譯,並且每次運行程序的時候都需要對 odex 重新進行編譯;而 AOT 是靜態編譯,應用在安裝的時候會啟動 dex2oat 通過靜態編譯的方式,來將所有的dex文件(包括Multidex)編譯oat文件,編譯完后的oat其實是一個標準的ELF文件,只是相對於普通的ELF文件多加了oat data section以及oat exec section這兩個段而已。(這兩個段裏面主要保存了兩種信息:Dex的文件信息以及類信息和Dex文件編譯之後的機器碼)。預編譯成 ELF 文件,每次運行程序的時候不用重新編譯,是真正意義上的本地應用。運行的文件格式也從odex轉換成了oat格式。

 

其實在Android5.0的時候我們能夠明顯感覺手機好用很多就是因為這個原因,從根本上換掉了那種存在着無法解決弊端的虛擬機。在 Android 5.x 和 6.x 的機器上,系統每次 OTA 升級完成重啟的時候都會有個應用優化的過程,這個過程就是剛才所說的 dex2oat 過程,這個過程比較耗時並且會佔用額外的存儲空間。

AOT 模式的預編譯解決了應用啟動和運行速度和耗資源(電等)問題的同時也帶來了另外兩個問題:

      1、應用安裝和系統升級之後的應用優化比較耗時,並且會更耗時間。因為系統和apk都是越來越大的。

      2、優化后的文件會佔用額外的存儲空間

在經過了兩個Android大版本的穩定后,在Android7.0又再次迎來了JIT的 回歸。

JIT的回歸,可不是把AOT模式給取代了,而是形成 了AOT/JIT 混合編譯模式,這種模式至今仍在使用

應用在安裝的時候 dex 不會被編譯。

應用在運行時 dex 文件先通過解析器(Interpreter)後會被直接執行(這一步驟跟 Android 2.2 – Android 4.4之前的行為一致),與此同時,熱點函數(Hot Code)會被識別並被 JIT 編譯后存儲在 jit code cache 中並生成 profile 文件以記錄熱點函數的信息。

手機進入 IDLE(空閑) 或者 Charging(充電) 狀態的時候,系統會掃描 App 目錄下的 profile 文件並執行 AOT 過程進行編譯。

(Profile文件會在JIT運行的過程中生成:每個APP都會有自己的Profile文件,保存在App本身的Local Storage中。Profile會保存所調用的類以及函數的Index,通過profman工具進行分析生成)

 

 

個人理解:哪種模式擅長干什麼就讓他去干什麼。

混合編譯模式綜合了 AOT 和 JIT 的各種優點,使得應用在安裝速度加快的同時,運行速度、存儲空間和耗電量等指標都得到了優化。

之前一直在說流暢,真的流暢在Android7.0上才感受到了些許。Android7.0系統也被用了相當長的一段時間。之後的Android8.0和Android9.0都是對各方面的優化,例如編譯文件、編譯器、GC。。

其中,值得一提的是華為的方舟編譯器。

  • 首先會判斷該設備支不支持方舟編譯器,如果支持,則從應用商店下發方舟版本的包
  • 方舟編譯器會把dex文件通過自己的IR翻譯方舟格式的機器碼,據資料說也是一個ELF文件,但是會增加一些段,猜測是Dex中類信息相關的段
  • 通過這種方式,來消除Java與JNI之間的通信的損耗,以及提升運行時的效率
  • 在方舟內部,還重新完善了GC算法,使得GC的頻率大大降低,減少應用卡頓的現象
  • 目前方舟只支持64位的So,並且對於加殼的So會出現一些問題。

方舟編譯器適配的應用,下載手機上都是方舟版本的包,特製的包用方舟編譯器編譯效率大大提升,之後直接執行就可以了,直接略過了在ART虛擬機上預編譯的過程。這樣的結果是很完美的,但是卻也沒辦法跳過一個弊端。那就是生態。還是不管是安卓還是iOS,這麼多年的時間沉澱中,他們的生態系統早就達到了一個非常完善的地步。安卓和iOS應用已經多達上千萬,而方舟適配應用的數量還非常有限。

谷歌宣布將停止對華為提供安卓系統更新之後,華為曝光了自主研發的鴻蒙操作系統。當時網友各種力挺。不過後來,華為董事長梁華在談及鴻蒙系統時稱,鴻蒙系統是為物聯網開發的,用於自動駕駛、遠程醫療等低時延場景。鴻蒙系統是不是兩手準備我們不得而知。但是,一個操作系統最重要的就是它的生態環境。縱觀華為現在的整個格局,目的非常明確,用方舟編譯器來擴大自己的用戶群體。當用戶的基數足夠龐大時,可以隨時隨地建立一個完善的生態系統。如果在未來某一天,Android全面限制華為的使用之後,在這危機關頭鴻蒙系統還是很有可能扛起國產手機的一面大旗。哪怕不是鴻蒙,我們也需要這樣一個生態不是嗎?

最初,突然去了解Android中的虛擬機,一個是想要明白到底Android中的虛擬機和JVM是不是一回事,還有就是想要明白華為發布方舟編譯器到底快到了哪裡。

上述相關資料均來自網絡,侵權必刪。

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

※公開收購3c價格,不怕被賤賣!

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

canvas入門,就是這個feel!

鈣素

Canvas 是在HTML5中新增的標籤用於在網頁實時生成圖像,並且可以操作圖像內容,基本上它是一個可以用JavaScript操作的位圖。也就是說我們將通過JS完成畫圖而不是css

canvas 默認布局為 inline-block,可以認為是一種特殊的圖片。

走起 ~

canvas 劃線

<canvas id="can" width="800" height="800"></canvas>

(寬高不能放在style裏面,否則比例不對)

canvas裏面的widthheight相當於圖片的原始尺寸,加了外部style的寬高,就相當於對圖片進行壓縮和拉伸。

// 1、獲取原生dom對象
let dom = document.getElementById('can');

// 2、獲取繪圖對象
let can = dom.getContext('2d'); // 3d是webgl

// 定義線條起點
can.moveTo(0,0);

// 定義線條中點(非終點)
can.lineTo(400,400);
can.lineTo(800,0);

// 對標記範圍進行描邊
can.stroke()

// 對標記範圍進行填充
can.fill();

設置線條屬性

線條默認寬度是 1

(一定要在繪圖之前設置。)

can.lineWidth = 2; //設置線條寬度
can.strokeStyle = '#f00';  // 設置線條顏色

can.fillStyle = '#f00';  // 設置填充區域顏色

折線樣式

  • miter:尖角(當尖角長度值過長時會自動變成折角,如果強制显示尖角:can.miterLimit = 100 設置尖角長度閾值。
  • round:圓角
  • bevel:折角
can.lineJoin = 'miter';
can.moveTo(100, 100);
can.lineTo(300, 100);
can.lineTo(100, 200);
can.stroke()

can.lineJoin = 'round';
can.moveTo(400, 100);
can.lineTo(600, 100);
can.lineTo(400, 200);
can.stroke()

can.lineJoin = 'bevel';
can.moveTo(700, 100);
can.lineTo(900, 100);
can.lineTo(700, 200);
can.stroke()

設置線帽

  • round:加圓角線帽
  • square:加直角線帽
  • butt:不加線帽
    can.lineCap = 'round';
    can.moveTo(100, 100);
    can.lineTo(300, 100);
    can.stroke()
    
     // 新建繪圖,使得上一次的繪畫樣式不會影響下面的繪畫樣式(代碼加在上一次繪畫和下一次繪畫中間。)
    can.beginPath()
    
    can.lineCap = 'square';
    can.moveTo(100, 200);
    can.lineTo(300, 200);
    can.stroke()
    
    can.beginPath()
    
    can.lineCap = 'butt';
    can.moveTo(100, 300);
    can.lineTo(300, 300);
    can.stroke()

畫矩形

// 參數:x,y,寬,高

can.rect(100,100,100,100);
can.stroke();

// 畫完即填充
can.fillRect(100,100,100,100);

畫圓弧

// 參數:圓心x,圓心y,半徑,圓弧起點與圓心的夾角度數,圓弧終點與圓心的夾角度數,true(逆時針繪畫)

can.arc(500,300,200,0,2*Math.PI/360*90,false);
can.stroke()

示例:

can.moveTo(500,300);
can.lineTo(500 + Math.sqrt(100), 300 + Math.sqrt(100))
can.arc(500, 300, 100, 2 * Math.PI / 360 *startDeg, 2 * Math.PI / 360 *endDeg, false);
can.closePath()//將圖形起點和終點用線連接起來使之成為封閉的圖形
can.fill()

Tips:

1、can.beginPath() // 新建繪圖,使得上一次的繪畫樣式不會影響下面的繪畫樣式(代碼加在上一次繪畫和下一次繪畫中間。)

2、can.closePath() //將圖形起點和終點用線連接起來使之成為封閉的圖形。

旋轉畫布

can.rotate(2*Math.PI/360*45); // 一定要寫在開始繪圖之前
can.fillRect(0,0,200, 10);

旋轉整個畫布的坐標系(參考坐標為畫布的(0,0)位置)

縮放畫布

can.scale(0.5,2);
can.fillRect(0,0,200, 10);

示例:

整個畫布:x方向縮放為原來的0.5,y方向拉伸為原來的2倍。

畫布位移

can.translate(100,100)
can.fillRect(0,0,200, 10);

保存與恢復畫布狀態

can.save() // 存檔:保存當前畫布坐標系狀態
can.restore() // 讀檔:恢復之前保存的畫布坐標系狀態

需要正確坐標系繪圖的時候,再讀檔之前的正確坐標系。

can.restore() // 將當前的畫布坐標系狀態恢復成上一次保存時的狀態
can.fillRect(dom.width/2, dom.height/2, 300, 100)

指針時鐘(案例)

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>clock</title>
    <style type="text/css">
        #can {
            width: 1000px;
            height: 600px;
            background: linear-gradient(45deg, green, skyblue);
        }
    </style>
</head>

<body>
    <canvas id="can" width="2000" height="1200"></canvas>
</body>

<script type="text/javascript">
    let dom = document.getElementById('can');

    let can = dom.getContext('2d');

    // 把畫布的圓心移動到畫布的中心
    can.translate(dom.width / 2, dom.height / 2);
    // 保存當前的畫布坐標系
    can.save()


    run();



    function run() {
        setInterval(function() {
            clearCanvas();
            draw();
        }, 10);
    }

    // 繪圖
    function draw() {
        let time = new Date();
        let hour = time.getHours();
        let min = time.getMinutes();
        let sec = time.getSeconds();
        let minSec = time.getMilliseconds();

        drawPannl();
        drawHour(hour, min, sec);
        drawMin(min, sec);
        drawSec(sec, minSec);
        drawPoint();
    }

    // 最簡單的方法:由於canvas每當高度或寬度被重設時,畫布內容就會被清空
    function clearCanvas() {
        dom.height = dom.height;
        can.translate(dom.width / 2, dom.height / 2);
        can.save()
    }

    // 畫錶盤
    function drawPannl() {
        can.beginPath();

        can.restore()
        can.save()

        can.lineWidth = 10;
        can.strokeStyle = 'skyblue';
        can.arc(0, 0, 400, 0, 2 * Math.PI);
        can.stroke();

        for (let i = 0; i < 12; i++) {
            can.beginPath();
            can.lineWidth = 16;
            can.strokeStyle = 'greenyellow';

            can.rotate(2 * Math.PI / 12)

            can.moveTo(0, -395);
            can.lineTo(0, -340);
            can.stroke();
        }

        for (let i = 0; i < 60; i++) {
            can.beginPath();
            can.lineWidth = 10;
            can.strokeStyle = '#fff';

            can.rotate(2 * Math.PI / 60)

            can.moveTo(0, -395);
            can.lineTo(0, -370);
            can.stroke();
        }
    }

    // 畫時針
    function drawHour(h, m, s) {
        can.beginPath();

        can.restore()
        can.save()

        can.lineWidth = 24;
        can.strokeStyle = 'palevioletred';
        can.lineCap = 'round'
        can.rotate(2 * Math.PI / (12 * 60 * 60) * (h * 60 * 60 + m * 60 + s))
        can.moveTo(0, 0);
        can.lineTo(0, -200);
        can.stroke();
    }

    // 畫分針
    function drawMin(m, s) {
        can.beginPath();

        can.restore()
        can.save()

        can.lineWidth = 14;
        can.strokeStyle = '#09f';
        can.lineCap = 'round'
        can.rotate(2 * Math.PI / (60 * 60) * (m * 60 + s))
        can.moveTo(0, 0);
        can.lineTo(0, -260);
        can.stroke();
    }

    // 畫秒針
    function drawSec(s, ms) {
        can.beginPath();

        can.restore()
        can.save()

        can.lineWidth = 8;
        can.strokeStyle = '#f00';
        can.lineCap = 'round'
        can.rotate(2 * Math.PI / (60 * 1000) * (s * 1000 + ms));
        can.moveTo(0, 50);
        can.lineTo(0, -320);
        can.stroke();
    }


    // 畫中心點
    function drawPoint() {
        can.beginPath();

        can.restore()
        can.save()

        can.lineWidth = 10;
        can.fillStyle = 'red';
        can.arc(0, 0, 12, 0, 2 * Math.PI);
        can.fill();
    }
</script>

</html>

圓弧時鐘(案例)

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>clock</title>
    <style type="text/css">
        #can {
            width: 1000px;
            height: 600px;
            background: linear-gradient(45deg, rgb(94, 53, 6), black);
        }
    </style>
</head>

<body>
    <canvas id="can" width="2000" height="1200"></canvas>
</body>

<script type="text/javascript">
    let dom = document.getElementById('can');

    let can = dom.getContext('2d');

    // 把畫布的圓心移動到畫布的中心
    can.translate(dom.width / 2, dom.height / 2);
    // 保存當前的畫布坐標系
    can.save();

    // 圓形指針起始角度
    let startDeg = 2 * Math.PI / 360 * 270;


    run();
    // draw();


    function run() {
        setInterval(function() {
            clearCanvas();
            draw();
        }, 20);
    }

    // 繪圖
    function draw() {
        let time = new Date();
        // let hour = time.getHours();
        let hour = time.getHours() > 10 ? time.getHours() - 12 : time.getHours();
        let min = time.getMinutes();
        let sec = time.getSeconds();
        let minSec = time.getMilliseconds();

        drawPannl();
        drawTime(hour, min, sec, minSec);
        drawHour(hour, min, sec);
        drawMin(min, sec);
        drawSec(sec, minSec);
        drawPoint();
    }

    // 最簡單的方法:由於canvas每當高度或寬度被重設時,畫布內容就會被清空
    function clearCanvas() {
        dom.height = dom.height;
        can.translate(dom.width / 2, dom.height / 2);
        can.save()
    }

    // 畫錶盤
    function drawPannl() {

        can.restore()
        can.save()

        // 設置時錶盤
        can.beginPath();
        can.lineWidth = 50;
        can.strokeStyle = 'rgba(255,23,87,0.2)';
        can.arc(0, 0, 400, 0, 2 * Math.PI);
        can.stroke();
        // 設置分錶盤
        can.beginPath();
        can.strokeStyle = 'rgba(169,242,15,0.2)';
        can.arc(0, 0, 345, 0, 2 * Math.PI);
        can.stroke();
        // 設置秒錶盤
        can.beginPath();
        can.strokeStyle = 'rgba(21,202,230,0.2)';
        can.arc(0, 0, 290, 0, 2 * Math.PI);
        can.stroke();


        // 小時刻度
        // for (let i = 0; i < 12; i++) {
        //     can.beginPath();
        //     can.lineWidth = 16;
        //     can.strokeStyle = 'rgba(0,0,0,0.2)';

        //     can.rotate(2 * Math.PI / 12)

        //     can.moveTo(0, -375);
        //     can.lineTo(0, -425);
        //     can.stroke();
        // }

        // 分針刻度
        // for (let i = 0; i < 60; i++) {
        //     can.beginPath();
        //     can.lineWidth = 10;
        //     can.strokeStyle = '#fff';

        //     can.rotate(2 * Math.PI / 60)

        //     can.moveTo(0, -395);
        //     can.lineTo(0, -370);
        //     can.stroke();
        // }
    }

    // 畫時針
    function drawHour(h, m, s) {

        let rotateDeg = 2 * Math.PI / (12 * 60 * 60) * (h * 60 * 60 + m * 60 + s);

        can.beginPath();
        can.restore()
        can.save()

        // 時針圓弧
        can.lineWidth = 50;
        can.strokeStyle = 'rgb(255,23,87)';
        can.lineCap = 'round';
        can.shadowColor = "rgb(255,23,87)"; // 設置陰影顏色
        can.shadowBlur = 20; // 設置陰影範圍
        can.arc(0, 0, 400, startDeg, startDeg + rotateDeg);
        can.stroke();

        // 時針指針
        can.beginPath();
        can.lineWidth = 24;
        can.strokeStyle = 'rgb(255,23,87)';
        can.lineCap = 'round'
        can.rotate(rotateDeg)
        can.moveTo(0, 0);
        can.lineTo(0, -100);
        can.stroke();



    }

    // 畫分針
    function drawMin(m, s) {

        let rotateDeg = 2 * Math.PI / (60 * 60) * (m * 60 + s);

        can.beginPath();
        can.restore()
        can.save()

        // 分針圓弧
        can.lineWidth = 50;
        can.strokeStyle = 'rgb(169,242,15)';
        can.lineCap = 'round'
        can.shadowColor = "rgb(169,242,15)";
        can.shadowBlur = 20;
        can.arc(0, 0, 345, startDeg, startDeg + rotateDeg);
        can.stroke();

        // 分針指針
        can.beginPath();
        can.lineWidth = 14;
        can.strokeStyle = 'rgb(169,242,15)';
        can.lineCap = 'round'
        can.rotate(rotateDeg)
        can.moveTo(0, 0);
        can.lineTo(0, -160);
        can.stroke();
    }

    // 畫秒針
    function drawSec(s, ms) {

        let rotateDeg = 2 * Math.PI / (60 * 1000) * (s * 1000 + ms);

        can.beginPath();
        can.restore()
        can.save()

        can.lineWidth = 50;
        can.strokeStyle = 'rgb(21,202,230)';
        can.lineCap = 'round'
        can.arc(0, 0, 290, startDeg, startDeg + rotateDeg);
        can.stroke();

        can.beginPath();
        can.lineWidth = 8;
        can.strokeStyle = 'rgb(21,202,230)';
        can.lineCap = 'round'
        can.shadowColor = "rgb(21,202,230)";
        can.shadowBlur = 20;
        can.rotate(rotateDeg);
        can.moveTo(0, 50);
        can.lineTo(0, -220);
        can.stroke();
    }


    // 畫中心點
    function drawPoint() {
        can.beginPath();
        can.restore()
        can.save()

        can.lineWidth = 10;
        can.fillStyle = 'red';
        can.arc(0, 0, 12, 0, 2 * Math.PI);
        can.fill();
    }

    // 显示数字時鐘
    function drawTime(h, m, s, ms) {
        can.font = '60px Calibri';
        can.fillStyle = '#0f0'
        can.shadowColor = "#fff";
        can.shadowBlur = 20;
        can.fillText(`${h}:${m}:${s}.${ms}`, -140, -100);
    }
</script>

</html>

(啾咪 ^.<)

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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