設計模式之代理模式

什麼是代理模式

代理模式就是為一個對象提供一個代理對象,由這個代理對象控制對該對象的訪問。

理解代理模式,可以對照生活中的一些具體例子,比如房產中介、二手車交易市場、經紀人等。

為什麼要用代理模式

通過使用代理模式,我們避免了直接訪問目標對象時可能帶來的一些問題,比如:遠程調用,需要使用遠程代理來幫我們處理一些網絡傳輸相關的細節邏輯;可能需要基於某種權限控制對目標資源的訪問,可以使用保護代理等。

總的來說,通過是用代理模式,我們可以控制對目標對象的訪問,可以在真實方法被調用前或調用后,通過代理對象加入額外的處理邏輯。

代理模式分類

代理模式分為靜態代理和動態代理。動態代理根據實現不同又可細分為JDK動態代理和cglib動態代理。

靜態代理是由程序員創建或工具生成代理類的源碼,再編譯代理類。所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和委託類的關係在運行前就確定了。

動態代理是在實現階段不用關心代理類,而在運行時動態生成代理類的。

靜態代理

以房哥買房子為例,用代碼實現靜態代理。

1、首先建立一個Seller接口

public interface Seller {
    void sell();
}

2、創建實現類,房哥,有一個方法,就是買房子

public class FangGe implements Seller{
    @Override
    public void sell() {
        System.out.println("房哥要出手一套四合院");
    }
}

3、買房子需要找到買家,達成交易后還要辦理過戶等其他手續,房哥只想賣房收錢就完了。因此,需要找一個代理來幫房哥處理這些雜事。

我們創建一個代理類FangGeProxy,代理類也需要實現Seller接口,行為上要保持和FangGe一樣,都是要賣房子。同時該代理類還需要持有房哥的引用。

public class FangGeProxy implements Seller{
    private FangGe fangGe;

    public FangGeProxy(FangGe fangGe){
        this.fangGe = fangGe;
    }
    @Override
    public void sell() {
        findBuyer();
        fangGe.sell();
        afterSell();
    }
    
    public void findBuyer(){
        System.out.println("代理幫助尋找買主");
    }
    
    public void afterSell(){
        System.out.println("達成交易后,辦理相關手續");
    }
}

可以看到,房哥的代理類通過findBuyer()和afterSell()兩個方法幫助房哥完成了其他一些雜事。

4、測試類

public class StaticProxyTest {
    public static void main(String[] args) {
        Seller seller = new FangGeProxy(new FangGe());
        seller.sell();
    }
}

輸出:

代理幫助尋找買主
房哥要出手一套四合院
達成交易后,辦理相關手續

最後,看下類圖

靜態代理的問題:

1、由於靜態代理類在編譯前已經確定了代理的對象,因此靜態代理只能代理一種類型的類,如果要給大量的類做代理,就需要編寫大量的代理類;

2、如果我們要給Seller,也就是目標對象要增加一些方法,則需要同步修改代理類,不符合開閉原則。

JDK動態代理

JDK的動態代理依賴於jdk給我們提供的類庫實現,是一種基於接口實現的動態代理,在編譯時並不知道要代理哪個類,而是在運行時動態生成代理類。同時也解決了靜態代理中存在的問題。

我們接上上面靜態代理的例子,繼續實現JDK的動態代理。

1、我們建一個方法轉發的處理器類,該類需要實現InvocationHandler接口。

public class SellerInvocationHandler implements InvocationHandler {

    // 要代理的真實對象
    private Object target;

    /**
     * 使用Proxy類靜態方法獲取代理類實例
     */
    public Object getProxyInstance(Object target){
        this.target = target;
        Class<?> clazz = target.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object obj = method.invoke(this.target, args);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法前");
    }
    
    private void after() {
        System.out.println("執行方法后");
    }
}

2、新建JDK動態代理測試類,首先代理房哥賣房子

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        
        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();

    }
}

輸出:

執行方法前
房哥要出手一套四合院
執行方法后

可以看到,完成了代理。

3、接下來我們新建另外一個類,User類,並使用JDK動態代理完成代理User類

public interface IUser {
    void sayHello();

    void work();
}

public class UserImpl implements IUser{
    @Override
    public void sayHello() {
        System.out.println("hello,我是小明");
    }

    @Override
    public void work() {
        System.out.println("我正在寫代碼");
    }
}

修改測試類,

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

/*        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();

        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();*/

        // 代理user類
        IUser user = new UserImpl();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        IUser userProxy = (IUser) sellerInvocationHandler.getProxyInstance(user);
        userProxy.sayHello();
        userProxy.work();

    }
}

輸出:

執行方法前
hello,我是小明
執行方法后
執行方法前
我正在寫代碼
執行方法后

可以看到,我們SellerInvocationHandler 並未做任何改動,它便能為UserImpl類生成代理,並在執行方法的前後增加額外的執行邏輯。

cglib動態代理

JDK動態代理有一個局限就是,被代理的類必須要實現接口。如果被代理的類沒有實現接口,則JDK動態代理就無能為力了。這個時候該cglib動態代理上場了。

CGLIB是一個功能強大,高性能的代碼生成包。它為沒有實現接口的類提供代理,為JDK的動態代理提供了很好的補充。通常可以使用Java的動態代理創建代理,但當要代理的類沒有實現接口或者為了更好的性能,CGLIB是一個好的選擇。

1、新建一個MyCglibInterceptor,實現MethodInterceptor接口。該類類似於JDK動態代理中的InvocationHandler實例,是實現cglib動態代理的主要類。

public class MyCglibInterceptor implements MethodInterceptor {

    public Object getCglibProxyInstance(Object object){
        // 相當於Proxy,創建代理的工具類
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(object.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o, objects);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法之前");
    }

    private void after() {
        System.out.println("執行方法之後");
    }
}

2、新建cglib動態代理的測試類,先代理上面例子中的User類。

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();
    }
}

輸出:

執行方法之前
hello,我是小明
執行方法之後
執行方法之前
我正在寫代碼
執行方法之後

3、新建一個類HelloWorld,不實現任何接口,為該類實現動態代理。

public class HelloWorld {
    public void hello(){
        System.out.println("世界這麼大,我想去看看");
    }
}

測試代理類

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
/*        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();*/

        // 代理未實現任何接口的類
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        HelloWorld helloWorldProxy = (HelloWorld) myCglibInterceptor.getCglibProxyInstance(new HelloWorld());
        helloWorldProxy.hello();
    }
}

輸出:

執行方法之前
世界這麼大,我想去看看
執行方法之後

使用cglib動態代理,我們實現了對普通類的代理。

(完)

設計模式系列文章

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

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

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

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

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

原創|我是如何從零學習開發一款跨平台桌面軟件的(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收購,鏡頭 收購有可能以全新價回收嗎?

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

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

虛擬機提到虛擬機,大家可能第一反應就是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地圖已可更新顯示潭子電動車充電站設置地點!!

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

VSCode, Django, and Anaconda開發環境集成配置[Windows]

  之前一直是在Ubuntu下進行Python和Django開發,最近換了電腦,把在Virtual Box 下跑的Ubuntu開發機挪過來總是頻繁崩潰,索性就嘗試把開發環境挪到Windows主力機了。

不得不說,巨硬家這幾年在多元並包方面真的是走在了世界前列。特別是VSCode,兩年前已經成為了我在Linux下的主力IDE。於是直接Google到了這篇爽文:Django Tutorial in Visual Studio Code, 下面會結合Anaconda的開發環境,翻譯這篇官方指導。

 

0x1 – 安裝清單

– Win10

– Anaconda3

– Vistual Studio Code(VSCode)

 分別下載並安裝好以上三個神器,選的都是最新穩定版。

 

0x2 – Anaconda 管理並配置Python開發環境

  • 打開Anaconda Prompt終端命令行工具(不是Anaconda Navigator),先來練習下conda,類似於pip和virtualenv的結合體。

  • 用conda創建Python開發虛擬環境,注意要在環境名稱(這裡是my_env)后加上python版本。
conda create -n wechat_env python=3.7
  • 移除環境

 conda remove -n wechat_env –all

  • 查看虛擬環境列表, *代表當前工作所在的虛擬環境:
(base) C:\Users\freman.zhang>conda env list
# conda environments:
#
base                  *  C:\Anaconda3
wechat_env               C:\Anaconda3\envs\wechat_env
  •  激活及切換環境:
(base) C:\Users\freman.zhang>conda activate wechat_env
(wechat_env) C:\Users\freman.zhang>conda env list
# conda environments:
#
base                     C:\Anaconda3
wechat_env            *  C:\Anaconda3\envs\wechat_env
(wechat_env) C:\Users\freman.zhang>conda deactivate
(base) C:\Users\freman.zhang>
  • 安裝Django到開發環境,如有需要可以指定版本號。
conda install django
conda install django==2.2.5

 

 

這樣conda就會自動下載並安裝好Python和Django到指定的開發環境,無需再事先或單獨安裝在OS中。

conda詳細的管理命令可以到中詳細了解。

 

0x3 – 在VSCode中配置集成開發環境

 Django是一個為安全,快速和可擴展的web開發所設計的高級Python框架。Django對於URL路由, 頁面模板和數據處理等提供豐富的支持。

在接下來的tutorial中,我們將創建一個簡單的三頁應用,並將會用到一個通用的基礎模板。通過在VSCode中完整的過一遍這個開發過程,我們將可以更好的理解如何使用VSCode的命令終端,編輯器和調試器等來高效便捷地進行Django應用開發。

整個示例項目的完整代碼在Github: .

1. 準備條件

– 在VScode中安裝python插件

– 下載安裝python,在Windows中還需特別注意PATH環境變量的配置

我們的安裝包和開發環境在前面已經都通過conda完成,對比后就可非常明顯的體現出Anaconda在包管理方面的便捷性。

2. 集成虛擬開發環境到VSCode中

  • 在VSCode中按組合鍵ctrl+shift+P,輸入python,先擇Python: Select Interpreter, 這個命令將會展示出一個所有VSCode可用的python解釋器清單。

 

  • 從這個清單中選擇我們上面用conda新建的開發環境 — 以 ./env or .\env開頭

 

 

  •  按組合鍵Ctrl+Shift+`打開一個新的集成命令終端,在VSCode的地步狀態欄,可以看到當前開發環境的標識

 

 

 0x4 – VSCode中創建Django項目

1. 按組合鍵Ctrl+Shift+`進入開發終端,相關解釋器和虛擬開發環境將會自動被激活。然後執行如下命令,如果沒有任何報錯,用瀏覽器打開http://127.0.0.1:8000,我們將會看到Django的默認歡迎頁。

django-admin startproject web_project C:\web_project
cd C:\web_project
python manage.py startapp hello
python manage.py runserver

 

 

 2. 接下來是Django應用的基礎構建

  • hello/views.py
from django.http import HttpResponse

def home(request):
    return HttpResponse("Hello, Django!")
  •  hello/urls.py
from django.urls import path
from hello import views

urlpatterns = [
    path("", views.home, name="home"),
]

 

  • web_project/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("", include("hello.urls")),
]

 

 

 

3. 保存所有文件,然後啟動服務 python manage.py runserver,用瀏覽器訪問應用網址 http://127.0.0.1:8000,將會看到如下:

 

 

 0x5 – VSCode中創建Django debugger launch profile開啟自動調試

到這裏你可能已經在想,是否有更好的方式來調試和運行應用服務,而非每次執行一次python manage.py runserver 呢?必須有的!

VSCode的debugger是支持Django的,我們通過自定義 launch profile就可以實現這一點。

1. 切換左邊的活動欄到Debug, 在Debug視圖的頂部,我們可以看到如下。No Configuratins表示Debugger還未配置任何運行設定(launch.json)。

 

 

 2. 點擊齒輪創建並開啟一個launch.json文件,這個文件裏面已經包含了一些調試設定,每種都是以獨立的JSON對象存在。我們添加如下:

 

{
    "name": "Python: Django",
    "type": "python",
    "request": "launch",
    "program": "${workspaceFolder}/manage.py",
    "console": "integratedTerminal",
    "args": [
        "runserver",
        "--noreload"
    ],
    "django": true
},

 

其中”django”: true告訴VSCode開啟Django頁面模板調試功能。

3. 點擊Debug > Start Debugging按鈕,瀏覽器中打開URL http://127.0.0.1:8000/將可以看到我們的APP順利的跑起來了。

 其實任何時候感覺想要調試一下應用效果時,我們都可以用Debug來啟動服務,此外這個操作還會自動保存所有打開着的文件。

這樣就不用每次都到命令行敲一遍啟動命令,倍爽有木有!!!

4. Debug不僅僅只有啟動和保存功能,我們下面通過具體案例來體驗下高級用法。

  先碼代碼

  • hello/urls.py:添加訪問路由到urlpatterns list中
path("hello/<name>", views.hello_there, name="hello_there"),

 

  • hello/views.py
import re
from datetime import datetime
from django.http import HttpResponse

def home(request):
    return HttpResponse("Hello, Django!")

def hello_there(request, name):
    now = datetime.now()
    formatted_now = now.strftime("%A, %d %B, %Y at %X")

    # Filter the name argument to letters only using regular expressions. URL arguments
    # can contain arbitrary text, so we restrict to safe characters only.
    match_object = re.match("[a-zA-Z]+", name)

    if match_object:
        clean_name = match_object.group(0)
    else:
        clean_name = "Friend"

    content = "Hello there, " + clean_name + "! It's " + formatted_now
    return HttpResponse(content)

5. 在Debug中設定斷點(breakpoints)於now = datetime.now() 所在行。

 

 

 6. 按F5或Debug > Start Debugging 開啟調試,VSCode頂部將會出現一個如下的Debug工具欄。

Pause (or Continue, F5), Step Over (F10), Step Into (F11), Step Out (Shift+F11), Restart (Ctrl+Shift+F5), and Stop (Shift+F5). See  for a description of each command.

 

 

 7. 在下面的終端中也會出現相關的控制信息。通過瀏覽器打開URL http://127.0.0.1:8000/hello/VSCode, 在頁面渲染完成前,VSCode會暫停在設定的斷點處。黃色小箭頭代表其是即將執行到的下一行。

 

 

 點擊 Step Over(F10) 執行 now = datetime.now()所在行。

在左邊Debug菜單欄我們將會看到很多實時輸入信息,包含運行時的變量值等等。我們可以在這裏檢查各個賦值或相關信息是否符合設計目標。

 

 

 程序暫停在斷點位置時,我們可以回到代碼中更改相關語句,調試器中的相關輸入信息也會實時做狀態更新。我們可以嘗試將formatted_now的賦值做如下更改,用來直觀地比較查看下調試器狀態更新。

now.strftime("%a, %d %B, %Y at %X")
'Fri, 07 September, 2018 at 07:46:32'
now.strftime("%a, %d %b, %Y at %X")
'Fri, 07 Sep, 2018 at 07:46:32'
now.strftime("%a, %d %b, %y at %X")
'Fri, 07 Sep, 18 at 07:46:32'

 

 

我們可以按F5逐行執行接下來的語句,並觀察調試器輸出信息,直到最終應用頁面完全渲染完成,點選Debug > Stop Debugging 或 command (Shift+F5)關閉調試。

 

  0x5 – Go to Definition

VSCode也支持查看函數和類的定義查看:

  • Go to Definition jumps from your code into the code that defines an object. For example, in views.py, right-click on HttpResponse in the home function and select Go to Definition (or use F12), which navigates to the class definition in the Django library.

  • Peek Definition (Alt+F12, also on the right-click context menu), is similar, but displays the class definition directly in the editor (making space in the editor window to avoid obscuring any code). Press Escape to close the Peek window or use the x in the upper right corner.

 

 

 

0x6 – Template, Static, Models編程

接下來可以在模板,靜態文件和數據處理的功能編程實現上實踐上面介紹的這些功能,練習整個集成開發環境的操作熟練度。

其實如果有一定基礎的話,我相信一天你就將會從入門到精通。

詳細的代碼個實現步驟在這裏就不在繼續往下貼了。詳細教程大家可按這個鏈接中的內容參照實現。

https://code.visualstudio.com/docs/python/tutorial-django#_create-multiple-templates-that-extend-a-base-template

 

0x7 – 問題分享

整個過程中只有遇到的問題:

1. VSCode無法原生支持Django Models相關對象的關聯檢查。

我們需要額外做點工作:

  • ctrl+shift+`(ESC下面那個鍵)打開命令終端,不用手工敲任何命令,終端會自動切換和激活到對應的虛擬開發環境。

安裝pylint-django

 

  •  然後進入VSCode setting裏面設定pylint參數,具體如下:

 

就這樣,問題解決!

 

 

==========================================================

由於還有繁重的日常工作要忙,這篇文章歷時了幾天時間斷斷續續整理出來,也精簡了不少官方指導中的文字描述。可能會對各位閱讀和操作來帶一些困擾,所以還是建議各位直接去讀官方文檔。我們這裏主要是集中整理了下Anaconda和VSCode的集成開發環境配置,以備未來不時之需,若能順便幫到任何人,將倍感欣慰。各位若有任何問題,歡迎提出,我將會彙整日後自己或其他來源收集到的問題陸續補充到0x7。

 

最後,希望大家能多多動手

多多敲代碼

多多點贊

多多分享

 

回家遛女兒去咯

Over~~

 

 

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

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

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

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

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

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

Clean Code 筆記 之 第四章 如何應用註釋

繼上一篇筆記之後,今天我們討論一下 代碼中是存在註釋是否是一件好的事情。

 

在我們開發的過程中講究“名副其實,見名識意”,這也往往是很多公司的要求,但是有了這些要求是不是我們的代碼中如果存在註釋是不是意味着我們的 函數,變量,以及類 的命名就不符合了“名副其實,見名識意”。

我們先區分一下註釋的類別,註釋一般分為以下幾種:

  • 1, 單行註釋
  • 2, 多行註釋
  • 3, 文檔註釋
  • 4, #region 摺疊註釋,可以將 代碼摺疊

 註釋的類別

1, 單行註釋:

在以 “//” 開頭,用以說明一行代碼的作用放置位置 看習慣或者公司要求合理就行。常用於函數內部,在很多的開源代碼中文件的頭部我同樣見到很多人使用單行註釋進行說明,靈活就好。
體現形式如下:

 public List<string> getVipUserNameByUserType()
          {
            // Vip user name list
            var vipUserNames = new List<string>();

            foreach (var user in Users)
            {

                if (user.Type = "VIP")

                    vipUserNames.Add(user.Name);
            }
            return vipUserNames;

          }

View Code

2, 多行註釋:

以“/*”開頭 “*/” 結尾 常用於描述說明一段代碼以及類註釋或者說代碼塊常用於文件的頂部。說明作者信息,時間如果是開源的這包含開源的更多說明。
通常使用如下:

/*
    * 作者:〈版權〉
    * 描述:〈描述〉
    * 創建時間:YYYY-MM-DD
    * 作用:
*/

View Code

3, 文檔註釋:

也就是常用的XML 註釋:它的說明性更加的強烈,多用於類以及函數上,當然屬性上同樣可以使用:
如下所示:

        /// <summary>
        /// MyClass
        /// </summary>
        public class MyClass
        {
            /// <summary>
            /// MyProperty
            /// </summary>
            public int MyProperty { get; set; }
            /// <summary>
            /// MyMethod
            /// </summary>
            public void MyMethod(){  }
        }

View Code

以下是官方建議的文檔標記 點擊標籤會制動跳轉

 

4, #region : 摺疊註釋,常用於描述多個函數的基本作用

書中最喜歡的話

好的註釋不能美化糟糕的代碼,真正好的註釋是你想盡辦法不去謝的註釋。懷註釋都是糟糕代碼的支撐或借口,或者是對錯誤決策的修正。

下面看一個例子

       //Check to see if the employee is eligible for full benefits1)If((employee.flags & HOURLY_FLAG)&& (employee.age>65))

(2)If(employee.isEligibleForFullBenefits()))

  這兩個你更喜歡哪個

View Code

好的註釋的特徵:

1:表示法律信息(這樣的註釋一般出現在文檔頂部說明作用以及協議)

// Copyright (c) .NET Foundation. All rights reserved
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

View Code

2:提供信息的註釋(指無法通過命名提供信息如要 註釋輔助的)

public void ConfigureServices(IServiceCollection services)
{

// These two middleware are registered via an IStartupFilter in UseIISIntegration but you can configure them here.

services.Configure<IISOptions>(options =>
{});

}

View Code

3:對意圖的解釋

4: 警示:告知其他人會出現某種後果的註釋

5: TODO註釋: 主要描述應該做的目前還沒有做的事情。

6: 放大:提示不合理之物的重要性。

應避免的註釋

應該避免以下幾點:

1: 誤導性註釋

2: 日誌式註釋

3: 廢話註釋

4: 標記位置的註釋

5: 括號后的註釋

6: 歸屬與簽名

7: 註釋掉的代碼

8: Html 註釋

以上沒有一一舉例的原因是我的PPT是一份演示的PPT,裏面很多公司的代碼不便貼出,抱歉。

不足之處還請指出

 

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

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

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

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

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

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

[UWP]用Win2D和CompositionAPI實現文字的發光效果,並製作動畫

1. 成果

獻祭了周末的晚上,成功召喚出了上面的番茄鍾。正當我在感慨“不愧是Shadow大人,這難道就是傳說中的五彩斑斕的黑?”

“那才不是什麼陰影效果,那是發光效果。”被路過的老婆吐槽了。

系系系,老婆說的都系對的。我還以為我在做陰影動畫,現在只好改博客標題了?

要實現上面的動畫效果,首先使用CompositionDrawingSurface,在它上面用DrawTextLayout畫出文字,然後用GaussianBlurEffect模仿成陰影,然後用CanvasActiveLayer裁剪文字的輪廓,然後用這個CompositionDrawingSurface創建出CompositionSurfaceBrush,然後創建一個CompositionMaskBrush,將CompositionSurfaceBrush作為它的Mask,然後用CompositionLinearGradientBrush創建出漸變,再用BlendEffect將它變成四向漸變,再用ColorKeyFrameAnimation和ScalarKeyFrameAnimation在它上面做動畫並把它作為CompositionMaskBrush的Source,然後創建SpriteVisual將CompositionMaskBrush應用上去,然後使用兩個PointLight分別從左到右和從右到左照射這個SpriteVisual,再創建一個AmbientLight模仿呼吸燈。

仔細想想……好吧,老婆說得對,我還真的沒有用到任何Shadow的Api,這裏和Shadow大人半毛錢關係都沒有。

這個番茄鍾源碼可以在這裏查看:

也可以安裝我的番茄鍾應用試玩一下,安裝地址:

這篇文章將介紹其中幾個關鍵技術。

2. 使用GaussianBlurEffect模仿陰影

上一篇文章已經介紹過怎麼在CompositionDrawingSurface上寫字,這裏就不再重複。為了可以為文字添加陰影,需要用到CanvasRenderTargetGaussianBlurEffect

CanvasRenderTarget是一個可以用來畫圖的渲染目標。實現文字陰影的步驟如下:將文字畫到CanvasRenderTarget,然後用它作為GaussianBlurEffect.Source產生一張高斯模糊的圖片,這樣看上去就和文字的陰影一樣。然後再在這張模糊的圖片的前面畫上原本的文字。

代碼如下所示:

using (var session = CanvasComposition.CreateDrawingSession(drawingSurface))
{
    session.Clear(Colors.Transparent);
    using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
    {
        var bitmap = new CanvasRenderTarget(session, width, height);
        using (var bitmapSession = bitmap.CreateDrawingSession())
        {
            bitmapSession.DrawTextLayout(textLayout, 0, 0, FontColor);
        }
        var blur = new GaussianBlurEffect
        {
            BlurAmount = (float)BlurAmount,
            Source = bitmap,
            BorderMode = EffectBorderMode.Hard
        };

        session.DrawImage(blur, 0, 0);
        session.DrawTextLayout(textLayout, 0, 0, FontColor);
    }
}

效果如下(因為我用了白色字體,這時候已經不怎麼像陰影了):

關於CavasRenderTaget,死魚的有詳細介紹。他的這個專欄的文章都很有趣。

3. 使用CanvasActiveLayer裁剪文字

關於裁剪文字,有幾件事需要做。

首先獲取需要裁剪的文字的輪廓,這使用上一篇文章介紹過的CanvasGeometry.CreateText就可以了,這個函數的返回值是一個。然後使用CanvasGeometry.CreateRectangle獲取整個畫布的CanvasGeometry,將他們用相減得出文字以外的部分,具體代碼如下:

var fullSizeGeometry = CanvasGeometry.CreateRectangle(session, 0, 0, width, height);
var textGeometry = CanvasGeometry.CreateText(textLayout);
var finalGeometry = fullSizeGeometry.CombineWith(textGeometry, Matrix3x2.Identity, CanvasGeometryCombine.Exclude);

這裏之所以不直接使用textGeometry,是因為我們並不是真的裁剪出文字的部分,而是像WPF的那樣用透明度控制显示的部分。就是用來實現這個功能。CanvasDrawingSession.CreateLayer函數使用透明度和CanvasGeometry創建一個CanvasActiveLayer,在創建Layer后CanvasDrawingSession的操作都會應用這個透明度,直到Layer關閉。

using (var layer = session.CreateLayer(1, finalGeometry))
{
    //DrawSth
}

最後效果如下:

關於CanvasActiveLayer的更多用法, 可以參考Lindexi的。

4. 製作有複雜顏色的陰影

如上圖所示,UWP中的DropShadow的Color只能有一種顏色,所以DropShadow不能使用複雜的顏色。這時候就要用到,CompositionMaskBrush有兩個主要屬性:Mask和Source。其中Mask是一個CompositionBrush類型的屬性,它指定不透明的蒙板源。簡單來說,CompositionMaskBrush的形狀就是它的Mask的形狀。而Source屬性則是它的顏色,這個屬性可以是 CompositionColorBrush、CompositionLinearGradientBrush、CompositionSurfaceBrush、CompositionEffectBrush 或 CompositionNineGridBrush 類型的任何 CompositionBrush。可以使用前面創建的CompositionDrawingSurface創建出CompositionSurfaceBrush,最後創建一個CompositionMaskBrush,將CompositionSurfaceBrush作為它的Mask。

var maskBrush = Compositor.CreateMaskBrush();
maskBrush.Mask = Compositor.CreateSurfaceBrush(DrawingSurface);
maskBrush.Source = Compositor.CreateLinearGradientBrush();

本來還想做到大紫大紅的,但被吐槽和本來低調內斂的目的不符合,所以復用了以前的配色,CompositionLinearGradientBrush加BlendEffect做成了有些複雜的配色(但實際上太暗了看不出來):

這時候效果如下:

5. 使用PointLight和AmbientLight製作動畫

我在這篇文章里介紹了PointLight的用法及基本動畫,這次豪華些,同時有從左到右的紅光以及從右到左的藍光,這兩個PointLight的動畫效果大致是這樣:

因為PointLight最多只能疊加兩個,所以再使用AmbientLight並對它的Intensity屬性做動畫,這樣動畫就會變得複雜些,最終實現了文章開頭的動畫。

var compositor = Window.Current.Compositor;
var ambientLight = compositor.CreateAmbientLight();
ambientLight.Intensity = 0;
ambientLight.Color = Colors.White;

var intensityAnimation = compositor.CreateScalarKeyFrameAnimation();
intensityAnimation.InsertKeyFrame(0.2f, 0, compositor.CreateLinearEasingFunction());
intensityAnimation.InsertKeyFrame(0.5f, 0.20f, compositor.CreateLinearEasingFunction());
intensityAnimation.InsertKeyFrame(0.8f, 0, compositor.CreateLinearEasingFunction());
intensityAnimation.Duration = TimeSpan.FromSeconds(10);
intensityAnimation.IterationBehavior = AnimationIterationBehavior.Forever;

ambientLight.StartAnimation(nameof(AmbientLight.Intensity), intensityAnimation);

6. 參考

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

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

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

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

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

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

看好台灣電動車發展,日本住友商事繼投資 Gogoro 後再入股電動巴士廠商華德動能

因應世界環保趨勢,期望達到未來零排放的標準,日商住友商事看上台灣電動車產業發展,繼之前投資電動機車大廠 Gogoro 之後,再次宣布投資台灣電動巴士大廠華德動能,為台日雙方在電動車方面的合作奠下基礎。

上櫃企業車王電子公司華德動能於 12 日上午召開臨時董事會,通過私募普通股定價相關事宜,應募人為日本住友商事株式會社 (以下簡稱日本住友商事),本次參與私募價格每股新台幣 25 元,總投資金額為日幣 4.5 億元 (約新台幣 1.27 億元),佔華德動能持股比率約 7%。華德動能表示,目前該項投資案仍須送投審會審議,預計最快將在 2020 年第 1 季可以得知審議結果。

華德動能指出,簽約儀式由日本住友商事由本部長岩波與華德動能董事長蔡裕慶代表雙方簽約。而日本住友商事為初次參與華德動能私募,著眼於雙方合作電動巴士製造及服務,未來將持續在智慧移動載具、汰役電池二次應用業務合作發展。基於鋰電池成本將逐年大幅降低,全球電動巴士及智慧載具市場在未來數年內將有突破性增長,華德動能將藉由日本住友商事之全球行銷服務據點協助華德動能拓展全球市場。

華德動能董事長蔡裕慶強調,多年來雙方一直簽有相關備忘錄,並進行進一步的合作。也就是過去華德動能透過住友商事向日本取得相關電動車電池使用於其所生產的電動巴士上,而華德動能也透過與住友商事的合作,藉由旗下全球 60 多個據點與子公司進行市場銷售。目前在東南亞及南美洲都有相關計畫發展,預計短期內就有成果。而住友商事由本部長岩波則是指出,雖然目前雙方的合作在於電動巴士的銷售,但未來仍會在售後服務與能源管理上合作,而且投資華德動能的持股比率還希望增加到 20%。

蔡裕慶進一步指出,華德動能是台灣唯一獲得交通部車輛安全審驗中心電動巴士「自主設計能力」資格審核通過的公司。其國產附加價值率超過 60%,所生産的電動巴士擁有優異的電池管理技術,採用零電池事故之日產 Leaf 同款電池及自主開發之專利電池主動平衡技術,大幅提升電池安全及壽命。另外,華德動能也是全球唯一採用六段電子自動變速箱之電動巴士廠商,並結合芬蘭設計東元電機生產之超高效能馬達,大幅降低能耗並保有超高爬坡力及高速行駛能力,華德電動巴士領先業界採用 10.1 吋智慧化觸控面板及雲端後台管理系統,並藉由大數據達到車輛異常偵測及預防保養,及時掌握車輛運行及異常資訊。

而為了因應目前市場對電動巴士的需求,華德動能 11 日也宣布在中港加工區投資新台幣 25 億元打造中港新廠。其規劃為 4 層樓建築,建物面積約 12,600 坪,預計 2021 年初完工,2021 年 7 月正式投產的新廠區,1 樓供華德動能生產電動巴士及底盤三電之用,2 至 4 樓則分別規劃為車王電子生產線、倉儲等用途。而新廠區的產能為年產整車 1,700 部,與 6,000 部底盤加三電系統,未來將以大多數供應外銷為主。

(合作媒體:。首圖來源:攝)

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

【其他文章推薦】

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

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

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

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

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

設計模式之美學習(八):為何說要多用組合少用繼承?如何決定該用組合還是繼承?

在面向對象編程中,有一條非常經典的設計原則,那就是:組合優於繼承,多用組合少用繼承。為什麼不推薦使用繼承?組合相比繼承有哪些優勢?如何判斷該用組合還是繼承?

為什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。所以,對於是否應該在項目中使用繼承,網上有很多爭議。很多人覺得繼承是一種反模式,應該盡量少用,甚至不用。為什麼會有這樣的爭議?

假設我們要設計一個關於鳥的類。我們將“鳥類”這樣一個抽象的事物概念,定義為一個抽象類 AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。

我們知道,大部分鳥都會飛,那我們可不可以在 AbstractBird 抽象類中,定義一個 fly() 方法呢?答案是否定的。儘管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現實世界中事物的認識。當然,你可能會說,在鴕鳥這個子類中重寫(overridefly() 方法,讓它拋出 UnSupportedMethodException 異常不就可以了嗎?具體的代碼實現如下所示:

public class AbstractBird {
  //...省略其他屬性和方法...
  public void fly() { //... }
}

public class Ostrich extends AbstractBird { //鴕鳥
  //...省略其他屬性和方法...
  public void fly() {
    throw new UnSupportedMethodException("I can't fly.'");
  }
}

這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對於這些不會飛的鳥來說,都需要重寫 fly() 方法,拋出異常。這樣的設計,一方面,徒增了編碼的工作量;另一方面,也違背了最小知識原則(Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的概率。

那再通過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類,不就可以了嗎?具體的繼承關係如下圖所示:

從圖中可以看出,繼承關係變成了三層。不過,整體上來講,目前的繼承關係還比較簡單,層次比較淺,也算是一種可以接受的設計思路。再繼續加點難度。在剛剛這個場景中,我們只關注“鳥會不會飛”,但如果我們還關注“鳥會不會叫”,那這個時候,又該如何設計類之間的繼承關係呢?

是否會飛?是否會叫?兩個行為搭配起來會產生四種情況:會飛會叫、不會飛會叫、會飛不會叫、不會飛不會叫。如果我們繼續沿用剛才的設計思路,那就需要再定義四個抽象類(AbstractFlyableTweetableBirdAbstractFlyableUnTweetableBirdAbstractUnFlyableTweetableBirdAbstractUnFlyableUnTweetableBird)。

如果還需要考慮“是否會下蛋”這樣一個行為,那估計就要組合爆炸了。類的繼承層次會越來越深、繼承關係會越來越複雜。而這種層次很深、很複雜的繼承關係,一方面,會導致代碼的可讀性變差。因為我們要搞清楚某個類具有哪些方法、屬性,必須閱讀父類的代碼、父類的父類的代碼……一直追溯到最頂層父類的代碼。另一方面,這也破壞了類的封裝特性,將父類的實現細節暴露給了子類。子類的實現依賴父類的實現,兩者高度耦合,一旦父類代碼修改,就會影響所有子類的邏輯。

總之,繼承最大的問題就在於:繼承層次過深、繼承關係過於複雜會影響到代碼的可讀性和可維護性。這也是為什麼不推薦使用繼承。那剛剛例子中繼承存在的問題,又該如何來解決呢?

組合相比繼承有哪些優勢?

實際上,可以利用組合(composition)、接口、委託(delegation)三個技術手段,一塊兒來解決剛剛繼承存在的問題。

前面講到接口的時候說過,接口表示具有某種行為特性。針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 接口,只讓會飛的鳥去實現這個接口。對於會叫、會下蛋這些行為特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口。將這個設計思路翻譯成 Java 代碼的話,就是下面這個樣子:

public interface Flyable {
  void fly();
}
public interface Tweetable {
  void tweet();
}
public interface EggLayable {
  void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  //... 省略其他屬性和方法...
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀
  //... 省略其他屬性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}

不過,接口只聲明方法,不定義實現。也就是說,每個會下蛋的鳥都要實現一遍 layEgg() 方法,並且實現邏輯是一樣的,這就會導致代碼重複的問題。那這個問題又該如何解決呢?

可以針對三個接口再定義三個實現類,它們分別是:實現了 fly() 方法的 FlyAbility 類、實現了 tweet() 方法的 TweetAbility 類、實現了 layEgg() 方法的 EggLayAbility 類。然後,通過組合和委託技術來消除代碼重複。具體的代碼實現如下所示:

public interface Flyable {
  void fly();
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  private TweetAbility tweetAbility = new TweetAbility(); //組合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
  //... 省略其他屬性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委託
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委託
  }
}

繼承主要有三個作用:表示 is-a 關係,支持多態特性,代碼復用。而這三個作用都可以通過其他技術手段來達成。比如 is-a 關係,我們可以通過組合和接口的 has-a 關係來替代;多態特性我們可以利用接口來實現;代碼復用我們可以通過組合和委託來實現。所以,從理論上講,通過組合、接口、委託三個技術手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關係,特別是一些複雜的繼承關係。

如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。從上面的例子來看,繼承改寫成組合意味着要做更細粒度的類的拆分。這也就意味着,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的複雜程度和維護成本。所以,在實際的項目開發中,我們還是要根據具體的情況,來具體選擇該用繼承還是組合。

如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關係),繼承關係不複雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關係複雜,我們就盡量使用組合來替代繼承。

除此之外,還有一些設計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關係,而模板模式(template pattern)使用了繼承關係。

前面講到繼承可以實現代碼復用。利用繼承特性,我們把相同的屬性和方法,抽取出來,定義到父類中。子類復用父類中的屬性和方法,達到代碼復用的目的。但是,有的時候,從業務含義上,A 類和 B 類並不一定具有繼承關係。比如,Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但並不具有繼承關係(既不是父子關係,也不是兄弟關係)。僅僅為了代碼復用,生硬地抽象出一個父類出來,會影響到代碼的可讀性。如果不熟悉背後設計思路的同事,發現 Crawler 類和 PageAnalyzer 類繼承同一個父類,而父類中定義的卻只是 URL 相關的操作,會覺得這個代碼寫得莫名其妙,理解不了。這個時候,使用組合就更加合理、更加靈活。具體的代碼實現如下所示:

public class Url {
  //...省略屬性和方法
}

public class Crawler {
  private Url url; // 組合
  public Crawler() {
    this.url = new Url();
  }
  //...
}

public class PageAnalyzer {
  private Url url; // 組合
  public PageAnalyzer() {
    this.url = new Url();
  }
  //..
}

還有一些特殊的場景要求我們必須使用繼承。如果你不能改變一個函數的入參類型,而入參又非接口,為了支持多態,只能採用繼承來實現。比如下面這樣一段代碼,其中 FeignClient 是一個外部類,我們沒有權限去修改這部分代碼,但是我們希望能重寫這個類在運行時執行的 encode() 函數。這個時候,我們只能採用繼承來實現了。

public class FeignClient { // feign client框架代碼
  //...省略其他代碼...
  public void encode(String url) { //... }
}

public void demofunction(FeignClient feignClient) {
  //...
  feignClient.encode(url);
  //...
}

public class CustomizedFeignClient extends FeignClient {
  @Override
  public void encode(String url) { //...重寫encode的實現...}
}

// 調用
FeignClient client = new CustomizedFeignClient();
demofunction(client);

儘管有些人說,要杜絕繼承,100% 用組合代替繼承,但是這裏的觀點沒那麼極端!之所以“多用組合少用繼承”這個口號喊得這麼響,只是因為,長期以來,過度使用繼承。還是那句話,組合併不完美,繼承也不是一無是處。只要我們控制好它們的副作用、發揮它們各自的優勢,在不同的場合下,恰當地選擇使用繼承還是組合,這才是我們所追求的境界。

重點回顧

1. 為什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。在這種情況下,我們應該盡量少用,甚至不用繼承。

2. 組合相比繼承有哪些優勢?

繼承主要有三個作用:表示 is-a 關係,支持多態特性,代碼復用。而這三個作用都可以通過組合、接口、委託三個技術手段來達成。除此之外,利用組合還能解決層次過深、過複雜的繼承關係影響代碼可維護性的問題。

3. 如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。在實際的項目開發中,我們還是要根據具體的情況,來選擇該用繼承還是組合。如果類之間的繼承結構穩定,層次比較淺,關係不複雜,我們就可以大膽地使用繼承。反之,我們就盡量使用組合來替代繼承。除此之外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。

思考

  • 在基於 MVC 架構開發 Web 應用的時候,經常會在數據庫層定義 Entity,在 Service 業務層定義 BOBusiness Object),在 Controller 接口層定義 VOView Object)。大部分情況下,EntityBOVO 三者之間的代碼有很大重複,但又不完全相同。該如何處理 EntityBOVO 代碼重複的問題呢?

參考:

本文由博客一文多發平台 發布!

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

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

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

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

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

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

程序員需要了解的硬核知識之彙編語言(一)

之前的系列文章從 CPU 和內存方面簡單介紹了一下彙編語言,但是還沒有系統的了解一下彙編語言,彙編語言作為第二代計算機語言,會用一些容易理解和記憶的字母,單詞來代替一個特定的指令,作為高級編程語言的基礎,有必要系統的了解一下彙編語言,那麼本篇文章希望大家跟我一起來了解一下彙編語言。

彙編語言和本地代碼

我們在之前的文章中探討過,計算機 CPU 只能運行本地代碼(機器語言)程序,用 C 語言等高級語言編寫的代碼,需要經過編譯器編譯后,轉換為本地代碼才能夠被 CPU 解釋執行。

但是本地代碼的可讀性非常差,所以需要使用一種能夠直接讀懂的語言來替換本地代碼,那就是在各本地代碼中,附帶上表示其功能的英文縮寫,比如在加法運算的本地代碼加上add(addition) 的縮寫、在比較運算符的本地代碼中加上cmp(compare)的縮寫等,這些通過縮寫來表示具體本地代碼指令的標誌稱為 助記符,使用助記符的語言稱為彙編語言。這樣,通過閱讀彙編語言,也能夠了解本地代碼的含義了。

不過,即使是使用彙編語言編寫的源代碼,最終也必須要轉換為本地代碼才能夠運行,負責做這項工作的程序稱為編譯器,轉換的這個過程稱為彙編。在將源代碼轉換為本地代碼這個功能方面,彙編器和編譯器是同樣的。

用彙編語言編寫的源代碼和本地代碼是一一對應的。因而,本地代碼也可以反過來轉換成彙編語言編寫的代碼。把本地代碼轉換為彙編代碼的這一過程稱為反彙編,執行反彙編的程序稱為反彙編程序

哪怕是 C 語言編寫的源代碼,編譯后也會轉換成特定 CPU 用的本地代碼。而將其反彙編的話,就可以得到彙編語言的源代碼,並對其內容進行調查。不過,本地代碼變成 C 語言源代碼的反編譯,要比本地代碼轉換成彙編代碼的反彙編要困難,這是因為,C 語言代碼和本地代碼不是一一對應的關係。

通過編譯器輸出彙編語言的源代碼

我們上面提到本地代碼可以經過反彙編轉換成為彙編代碼,但是只有這一種轉換方式嗎?顯然不是,C 語言編寫的源代碼也能夠通過編譯器編譯稱為彙編代碼,下面就來嘗試一下。

首先需要先做一些準備,需要先下載 Borland C++ 5.5 編譯器,為了方便,我這邊直接下載好了讀者直接從我的百度網盤提取即可 (鏈接:https://pan.baidu.com/s/19LqVICpn5GcV88thD2AnlA 密碼:hz1u)

下載完畢,需要進行配置,下面是配置說明 (https://wenku.baidu.com/view/22e2f418650e52ea551898ad.html),教程很完整跟着配置就可以,下面開始我們的編譯過程

首先用 Windows 記事本等文本編輯器編寫如下代碼

// 返回兩個參數值之和的函數
int AddNum(int a,int b){
  return a + b;
}

// 調用 AddNum 函數的函數
void MyFunc(){
  int c;
  c = AddNum(123,456);
}

編寫完成后將其文件名保存為 Sample4.c ,C 語言源文件的擴展名,通常用.c 來表示,上面程序是提供兩個輸入參數並返回它們之和。

在 Windows 操作系統下打開 命令提示符,切換到保存 Sample4.c 的文件夾下,然後在命令提示符中輸入

bcc32 -c -S Sample4.c

bcc32 是啟動 Borland C++ 的命令,-c 的選項是指僅進行編譯而不進行鏈接,-S 選項被用來指定生成彙編語言的源代碼

作為編譯的結果,當前目錄下會生成一個名為Sample4.asm 的彙編語言源代碼。彙編語言源文件的擴展名,通常用.asm 來表示,下面就讓我們用編輯器打開看一下 Sample4.asm 中的內容

    .386p
    ifdef ??version
    if    ??version GT 500H
    .mmx
    endif
    endif
    model flat
    ifndef  ??version
    ?debug  macro
    endm
    endif
    ?debug  S "Sample4.c"
    ?debug  T "Sample4.c"
_TEXT   segment dword public use32 'CODE'
_TEXT   ends
_DATA   segment dword public use32 'DATA'
_DATA   ends
_BSS    segment dword public use32 'BSS'
_BSS    ends
DGROUP  group   _BSS,_DATA
_TEXT   segment dword public use32 'CODE'
_AddNum proc    near
?live1@0:
   ;    
   ;    int AddNum(int a,int b){
   ;    
    push      ebp
    mov       ebp,esp
   ;    
   ;    
   ;        return a + b;
   ;    
@1:
    mov       eax,dword ptr [ebp+8]
    add       eax,dword ptr [ebp+12]
   ;    
   ;    }
   ;    
@3:
@2:
    pop       ebp
    ret 
_AddNum endp
_MyFunc proc    near
?live1@48:
   ;    
   ;    void MyFunc(){
   ;    
    push      ebp
    mov       ebp,esp
   ;    
   ;        int c;
   ;        c = AddNum(123,456);
   ;    
@4:
    push      456
    push      123
    call      _AddNum
    add       esp,8
   ;    
   ;    }
   ;    
@5:
    pop       ebp
    ret 
_MyFunc endp
_TEXT   ends
    public  _AddNum
    public  _MyFunc
    ?debug  D "Sample4.c" 20343 45835
    end

這樣,編譯器就成功的把 C 語言轉換成為了彙編代碼了。

不會轉換成本地代碼的偽指令

第一次看到彙編代碼的讀者可能感覺起來比較難,不過實際上其實比較簡單,而且可能比 C 語言還要簡單,為了便於閱讀彙編代碼的源代碼,需要注意幾個要點

彙編語言的源代碼,是由轉換成本地代碼的指令(後面講述的操作碼)和針對彙編器的偽指令構成的。偽指令負責把程序的構造以及彙編的方法指示給彙編器(轉換程序)。不過偽指令是無法彙編轉換成為本地代碼的。下面是上面程序截取的偽指令

_TEXT   segment dword public use32 'CODE'
_TEXT   ends
_DATA   segment dword public use32 'DATA'
_DATA   ends
_BSS    segment dword public use32 'BSS'
_BSS    ends
DGROUP  group   _BSS,_DATA

_AddNum proc    near
_AddNum endp

_MyFunc proc    near
_MyFunc endp

_TEXT   ends
    end

由偽指令 segmentends 圍起來的部分,是給構成程序的命令和數據的集合體上加一個名字而得到的,稱為段定義。段定義的英文表達具有區域的意思,在這個程序中,段定義指的是命令和數據等程序的集合體的意思,一個程序由多個段定義構成。

上面代碼的開始位置,定義了3個名稱分別為 _TEXT、_DATA、_BSS 的段定義,_TEXT 是指定的段定義,_DATA 是被初始化(有初始值)的數據的段定義,_BSS 是尚未初始化的數據的段定義。這種定義的名稱是由 Borland C++ 定義的,是由 Borland C++ 編譯器自動分配的,所以程序段定義的順序就成為了 _TEXT、_DATA、_BSS ,這樣也確保了內存的連續性

_TEXT   segment dword public use32 'CODE'
_TEXT   ends
_DATA   segment dword public use32 'DATA'
_DATA   ends
_BSS    segment dword public use32 'BSS'
_BSS    ends

段定義( segment ) 是用來區分或者劃分範圍區域的意思。彙編語言的 segment 偽指令表示段定義的起始,ends 偽指令表示段定義的結束。段定義是一段連續的內存空間

group 這個偽指令表示的是將 _BSS和_DATA 這兩個段定義匯總名為 DGROUP 的組

DGROUP  group   _BSS,_DATA

圍起 _AddNum_MyFun_TEXT segment 和 _TEXT ends ,表示_AddNum_MyFun 是屬於 _TEXT 這一段定義的。

_TEXT   segment dword public use32 'CODE'
_TEXT   ends

因此,即使在源代碼中指令和數據是混雜編寫的,經過編譯和彙編后,也會轉換成為規整的本地代碼。

_AddNum proc_AddNum endp 圍起來的部分,以及_MyFunc proc_MyFunc endp 圍起來的部分,分別表示 AddNum 函數和 MyFunc 函數的範圍。

_AddNum proc    near
_AddNum endp

_MyFunc proc    near
_MyFunc endp

編譯后在函數名前附帶上下劃線_ ,是 Borland C++ 的規定。在 C 語言中編寫的 AddNum 函數,在內部是以 _AddNum 這個名稱處理的。偽指令 proc 和 endp 圍起來的部分,表示的是 過程(procedure) 的範圍。在彙編語言中,這種相當於 C 語言的函數的形式稱為過程。

末尾的 end 偽指令,表示的是源代碼的結束。

## 彙編語言的語法是 操作碼 + 操作數

在彙編語言中,一行表示一對 CPU 的一個指令。彙編語言指令的語法結構是 操作碼 + 操作數,也存在只有操作碼沒有操作數的指令。

操作碼錶示的是指令動作,操作數表示的是指令對象。操作碼和操作數一起使用就是一個英文指令。比如從英語語法來分析的話,操作碼是動詞,操作數是賓語。比如這個句子 Give me money這個英文指令的話,Give 就是操作碼,me 和 money 就是操作數。彙編語言中存在多個操作數的情況,要用逗號把它們分割,就像是 Give me,money 這樣。

能夠使用何種形式的操作碼,是由 CPU 的種類決定的,下面對操作碼的功能進行了整理。

本地代碼需要加載到內存后才能運行,內存中存儲着構成本地代碼的指令和數據。程序運行時,CPU會從內存中把數據和指令讀出來,然後放在 CPU 內部的寄存器中進行處理。

如果 CPU 和內存的關係你還不是很了解的話,請閱讀作者的另一篇文章 詳細了解。

寄存器是 CPU 中的存儲區域,寄存器除了具有臨時存儲和計算的功能之外,還具有運算功能,x86 系列的主要種類和角色如下圖所示

指令解析

下面就對 CPU 中的指令進行分析

最常用的 mov 指令

指令中最常使用的是對寄存器和內存進行數據存儲的 mov 指令,mov 指令的兩個操作數,分別用來指定數據的存儲地和讀出源。操作數中可以指定寄存器、常數、標籤(附加在地址前),以及用方括號([]) 圍起來的這些內容。如果指定了沒有用([]) 方括號圍起來的內容,就表示對該值進行處理;如果指定了用方括號圍起來的內容,方括號的值則會被解釋為內存地址,然後就會對該內存地址對應的值進行讀寫操作。讓我們對上面的代碼片段進行說明

    mov       ebp,esp
    mov       eax,dword ptr [ebp+8]

mov ebp,esp 中,esp 寄存器中的值被直接存儲在了 ebp 中,也就是說,如果 esp 寄存器的值是100的話那麼 ebp 寄存器的值也是 100。

而在 mov eax,dword ptr [ebp+8] 這條指令中,ebp 寄存器的值 + 8 後會被解析稱為內存地址。如果 ebp

寄存器的值是100的話,那麼 eax 寄存器的值就是 100 + 8 的地址的值。dword ptr 也叫做 double word pointer 簡單解釋一下就是從指定的內存地址中讀出4字節的數據

對棧進行 push 和 pop

程序運行時,會在內存上申請分配一個稱為棧的數據空間。棧(stack)的特性是后入先出,數據在存儲時是從內存的下層(大的地址編號)逐漸往上層(小的地址編號)累積,讀出時則是按照從上往下進行讀取的。

棧是存儲臨時數據的區域,它的特點是通過 push 指令和 pop 指令進行數據的存儲和讀出。向棧中存儲數據稱為 入棧 ,從棧中讀出數據稱為 出棧,32位 x86 系列的 CPU 中,進行1次 push 或者 pop,即可處理 32 位(4字節)的數據。

函數的調用機制

下面我們一起來分析一下函數的調用機制,我們以上面的 C 語言編寫的代碼為例。首先,讓我們從MyFunc 函數調用AddNum 函數的彙編語言部分開始,來對函數的調用機制進行說明。棧在函數的調用中發揮了巨大的作用,下面是經過處理后的 MyFunc 函數的彙編處理內容

_MyFunc      proc    near
    push            ebp       ; 將 ebp 寄存器的值存入棧中              (1) 
    mov             ebp,esp ; 將 esp 寄存器的值存入 ebp 寄存器中        (2)
    push            456         ; 將 456 入棧                                                (3)
    push            123         ; 將 123 入棧                                                (4)
    call            _AddNum ; 調用 AddNum 函數                                       (5)
    add             esp,8       ; esp 寄存器的值 + 8                                     (6)
    pop             ebp         ; 讀出棧中的數值存入 esp 寄存器中                 (7)
    ret                             ; 結束 MyFunc 函數,返回到調用源                   (8)
_MyFunc         endp

代碼解釋中的(1)、(2)、(7)、(8)的處理適用於 C 語言中的所有函數,我們會在後面展示 AddNum 函數處理內容時進行說明。這裏希望大家先關注(3) – (6) 這一部分,這對了解函數調用機制至關重要。

(3) 和 (4) 表示的是將傳遞給 AddNum 函數的參數通過 push 入棧。在 C 語言源代碼中,雖然記述為函數 AddNum(123,456),但入棧時則會先按照 456,123 這樣的順序。也就是位於後面的數值先入棧。這是 C 語言的規定。(5) 表示的 call 指令,會把程序流程跳轉到 AddNum 函數指令的地址處。在彙編語言中,函數名表示的就是函數所在的內存地址。AddNum 函數處理完畢后,程序流程必須要返回到編號(6) 這一行。call 指令運行后,call 指令的下一行(也就指的是 (6) 這一行)的內存地址(調用函數完畢后要返回的內存地址)會自動的 push 入棧。該值會在 AddNum 函數處理的最後通過 ret 指令 pop 出棧,然後程序會返回到 (6) 這一行。

(6) 部分會把棧中存儲的兩個參數 (456 和 123) 進行銷毀處理。雖然通過兩次的 pop 指令也可以實現,不過採用 esp 寄存器 + 8 的方式會更有效率(處理 1 次即可)。對棧進行數值的輸入和輸出時,數值的單位是4字節。因此,通過在負責棧地址管理的 esp 寄存器中加上4的2倍8,就可以達到和運行兩次 pop 命令同樣的效果。雖然內存中的數據實際上還殘留着,但只要把 esp 寄存器的值更新為數據存儲地址前面的數據位置,該數據也就相當於銷毀了。

我在編譯 Sample4.c 文件時,出現了下圖的這條消息

圖中的意思是指 c 的值在 MyFunc 定義了但是一直未被使用,這其實是一項編譯器優化的功能,由於存儲着 AddNum 函數返回值的變量 c 在後面沒有被用到,因此編譯器就認為 該變量沒有意義,進而也就沒有生成與之對應的彙編語言代碼

下圖是調用 AddNum 這一函數前後棧內存的變化

函數的內部處理

上面我們用彙編代碼分析了一下 Sample4.c 整個過程的代碼,現在我們着重分析一下 AddNum 函數的源代碼部分,分析一下參數的接收、返回值和返回等機制

_AddNum         proc        near
    push            ebp                        -----------(1)
    mov             ebp,esp                -----------(2)
    mov             eax,dword ptr[ebp+8]   -----------(3)
    add             eax,dword ptr[ebp+12]  -----------(4)
    pop             ebp                                      -----------(5)
    ret             ----------------------------------(6)
_AddNum         endp

ebp 寄存器的值在(1)中入棧,在(5)中出棧,這主要是為了把函數中用到的 ebp 寄存器的內容,恢復到函數調用前的狀態。

(2) 中把負責管理棧地址的 esp 寄存器的值賦值到了 ebp 寄存器中。這是因為,在 mov 指令中方括號內的參數,是不允許指定 esp 寄存器的。因此,這裏就採用了不直接通過 esp,而是用 ebp 寄存器來讀寫棧內容的方法。

(3) 使用[ebp + 8] 指定棧中存儲的第1個參數123,並將其讀出到 eax 寄存器中。像這樣,不使用 pop 指令,也可以參照棧的內容。而之所以從多個寄存器中選擇了 eax 寄存器,是因為 eax 是負責運算的累加寄存器。

通過(4) 的 add 指令,把當前 eax 寄存器的值同第2個參數相加后的結果存儲在 eax 寄存器中。[ebp + 12] 是用來指定第2個參數456的。在 C 語言中,函數的返回值必須通過 eax 寄存器返回,這也是規定。也就是 函數的參數是通過棧來傳遞,返回值是通過寄存器返回的

(6) 中 ret 指令運行后,函數返回目的地內存地址會自動出棧,據此,程序流程就會跳轉返回到(6) (Call _AddNum) 的下一行。這時,AddNum 函數入口和出口處棧的狀態變化,就如下圖所示

這是程序員需要了解的硬核知識之彙編語言(一) 第一篇文章,下一篇文章我們會着重討論局部變量和全局變量以及循環控制語句的彙編語言,防止斷更,請關注我

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

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

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

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

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

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