部落格

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

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

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

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

      《》

    一、基礎知識

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

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

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

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

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

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

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

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

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

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

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

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

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

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

     

     

     

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

    二、IL概覽

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

      

     

     

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

    三、Emit編寫

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

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

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

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

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

    using System.Reflection.Emit;

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

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

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

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

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

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

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

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

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

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

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

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

      (8) 定義Name屬性

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

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

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

       (10) 定義並實現PrintName方法

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

      (11) 創建類

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

      (12) 調用

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

    四、應用

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

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

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

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

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

      (5) …

    五、小結

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

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

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

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

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

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

  • 標準庫bufio個人詳解

    標準庫bufio個人詳解

    本文是我有通俗的語言寫的如果有誤請指出。

    先看bufio官方文檔

    https://studygolang.com/pkgdoc文檔地址

     

     主要分三部分Reader、Writer、Scanner

    分別是讀數據、寫數據和掃描器三種數據類型的相關操作 這個掃描後面會詳細說我開始也沒弄明白其實很簡單。

     

    Reader

    func 

    func NewReaderSize(rd ., size ) *

    NewReaderSize創建一個具有最少有size尺寸的緩衝、從r讀取的*Reader。如果參數r已經是一個具有足夠大緩衝的* Reader類型值,會返回r。

     

     

     解釋:看官方解釋這個方法可能不太容易懂,這個意思就是就是你可以給*Reader自定義一個size大小的緩衝區,*Reader每次從底層io.Reader(也就是你那個文件或者流)中預讀size大小的數據到緩衝區中(可能讀不滿),然後你每次讀數據實際是從這個緩衝區中拿數據。

     

     下面是NewReaderSize源碼

    func NewReaderSize(rd io.Reader, size int) *Reader {
        // Is it already a Reader?
        b, ok := rd.(*Reader)
        if ok && len(b.buf) >= size {
            return b
        }
        if size < minReadBufferSize { //minReadBufferSize==16
            size = minReadBufferSize
        }
        r := new(Reader)
        r.reset(make([]byte, size), rd)
        return r
    }
    

      r.reset 初始化了一個*Reader 返回大小是size。

    func 

    func NewReader(rd .) *

    NewReader創建一個具有默認大小緩衝、從r讀取的*Reader。

    解釋:那這個NewReader就很好解釋了 和NewReaderSize基本一樣就是緩衝區大小是默認設置好的

    func (*Reader) 

    func (b *) Peek(n ) ([], )

    解釋:Peek就是返回緩存的一個切片,該切片引用緩存中的前N個字節的數據,如果n大於總大小,則返回能讀到的字節數的數據。

    func (*Reader) 

    func (b *) Read(p []) (n , err )

    Read讀取數據寫入p。本方法返回寫入p的字節數。本方法一次調用最多會調用下層Reader接口一次Read方法,因此返回值n可能小於len(p)。讀取到達結尾時,返回值n將為0而err將為io.EOF。

    解釋:如果緩存不為空則直接從緩存中讀數據不會從底層io.Reader讀,如果緩存為空len(p)>緩存大小,則直接從底層io.Reader讀數據到p。

    如果len(p)<緩存大小,則先從底層io.Reader中讀數據到緩存再到p。

     

    主要就這幾個 還有幾個文檔寫的都很清楚易懂我就不多寫了。

    Writer類型的方法和Reader類型的方法差不多也很易懂主要就一個Flush要注意。

    func (*Writer) 

    func (b *) Flush() 

    Flush方法將緩衝中的數據寫入下層的io.Writer接口。

    和Reader是倒過來的,Writer每次寫數據是先寫入緩衝區的,進程緩衝區填滿后,通過進程緩衝寫入到內核緩衝再寫入到磁盤,使用Flush就不等填滿直接走寫入流程了,保證你的數據及時寫入文件。

     

     

     

     解釋:scanner類型掃描器 官方的說法很複雜,我也沒太看懂找了很多資料,其實就是你在數據傳輸的時候時候使用“分隔符”,scanner類型可以通過分隔符逐個迭代你的數據。

    上面4個函數func Scan……  就是分隔符的判斷函數這4個是給你預設好的,你也可以按照自己的需求改寫。

    怎麼改寫呢,看下面

    func (*Scanner) 

    func (s *) Split(split )

    這個Split方法就是設置你這個scanner的用哪個SplitFunc類型的函數

    在看下面這個SpliFunc類型的函數簽名

    type SplitFunc func(data [], atEOF ) (advance , token [], err )

    照着這個格式寫一個不就得了么,當然具體寫法給出了但是你不會?沒關係咱看一下官方是咋寫的。

    https://github.com/golang/go/blob/master/src/bufio/scan.go?name=release#57官方源碼地址

    func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
    	if atEOF && len(data) == 0 {
    		return 0, nil, nil
    	}
    	if i := bytes.IndexByte(data, '\n'); i >= 0 {
    		// We have a full newline-terminated line.
    		return i + 1, dropCR(data[0:i]), nil
    	}
    	// If we're at EOF, we have a final, non-terminated line. Return it.
    	if atEOF {
    		return len(data), dropCR(data), nil
    	}
    	// Request more data.
    	return 0, nil, nil
    }
    

       

    看bytes.IndexByte(data, ‘\n’);這段不就是在找行尾嘛 比如你想改成以“;”為分隔符的就改成bytes.IndexByte(data, ‘;’);不就得了么

    func main(){
        scanner:=bufio.NewScanner(
            strings.NewReader("abcdefg\nhigklmn"),
        )
        scanner.Split(ScanLines) //這裏可以隨意選擇用哪個函數也可以自定義,可以不指定默認為\n做分隔符
      for scanner.Scan(){
        fmt.Println(scanner.Text())
      }
    }

      

    到此為止拉~

     

     

     

     

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

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

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

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

  • 人臉檢測和人臉識別原理,微調(Fine-tune)原理

    人臉檢測和人臉識別原理,微調(Fine-tune)原理

    一、MTCNN的原理

      搭建人臉識別系統的第一步是人臉檢測,也就是在圖片中找到人臉的位置。在這個過程中,系統的輸入是一張可能含有人臉的圖片,輸出是人臉位置的矩形框,如下圖所示。一般來說,人臉檢測應該可以正確檢測出圖片中存在的所有人臉,不能用遺漏,也不能有錯檢。  

       

      獲得包含人臉的矩形框后,第二步要做的就是人臉對齊(Face Alignment)。原始圖片中人臉的姿態、位置可能較大的區別,為了之後統一處理,要把人臉“擺正”。為此,需要檢測人臉中的關鍵點(Landmark),如眼睛的位置、鼻子的位置、嘴巴的位置、臉的輪廓點等。根據這些關鍵點可以使用仿射變換將人臉統一校準,以盡量消除姿勢不同帶來的誤差,人臉對齊的過程如下圖所示。

       

      這裏介紹一種基於深度卷積神經網絡的人臉檢測和人臉對齊方法—-MTCNN,它是基於卷積神經網絡的一種高精度的實時人臉檢測和對齊技術。MT是英文單詞Multi-task的縮寫,意思就是這種方法可以同時完成人臉檢測的人臉對齊兩項任務。相比於傳統方法,MTCNN的性能更好,可以更精確的定位人臉,此外,MTCNN也可以做到實時的檢測。

      MTCNN由三個神經網絡組成,分別是P-Net、R-Net、O-Net。在使用這些網絡之前,首先要將原始圖片縮放到不同尺度,形成一個“圖像金字塔”,如下圖所示。

       

      接着會對每個尺度的圖片通過神經網絡計算一遍。這樣做的原因在於:原始圖片中的人臉存在不同的尺度,如有的人臉比較大,有的人臉比較小。對於比較小的人臉,可以在放大后的圖片上檢測;對於比較大的人臉,可以在縮小后的圖片上進行檢測。這樣,就可以在統一的尺度下檢測人臉了。

      現在再來討論第一個網絡P-Net的結構,如下圖所示

       

      P-Net的輸入是一個寬和高皆為12像素,同時是3通道的RGB圖像,該網絡要判斷這個12×12的圖像中是否含有人臉,並且給出人臉框和關鍵點的位置。因此對應的輸出應該由3部分組成:

      (1)第一個部分要判斷該圖像是否是人臉(上圖中的face classification),輸出向量的形狀為1x1x2,也就是兩個值,分別為該圖像是人臉的概率,以及該圖像不是人臉的概率。這兩個值加起來應該嚴格等1。之所以使用兩個值來表示,是為了方便定義交叉熵損失。
      (2)第二個部分給出框的精確位置(上圖中的bounding box regression),一般稱之為框回歸。P-Net輸入的12×12的圖像塊可能並不是完美的人臉框的位置,如有的時候人臉並不正好為方形,有的時候12×12的圖像塊可能偏左或偏右,因此需要輸出當前框位置相對於完美的人臉框位置的偏移。這個偏移由四個變量組成。一般地,對於圖像中的框,可以用四個數來表示它的位置:框左上角的橫坐標、框左上角的縱坐標、框的寬度、框的高度。因此,框回歸輸出的值是:框左上角的橫坐標的相對偏移、框左上角的縱坐標的相對偏移、框的寬度的誤差、框的 高度的誤差。輸出向量的形狀就是上圖中的1x1x4。
      (3)第三個部分給出人臉的5個關鍵點的位置。5個關鍵點分別為:左眼的位置、右眼的位置、鼻子的位置、左嘴角的位置、右嘴角的位置。每個關鍵點又需要橫坐標和縱坐標來表示,因此輸出一共是10維(即1x1x10)

      上面的介紹大致就是P-Net的結構了。在實際計算中,通過P-Net中第一層卷積的移動,會對圖像中每一個12×12的區域做一次人臉檢測,得到的結構如下圖所示:

       

      圖中框的大小各有不同,除了框回歸的影響外,主要是因為將圖片金字塔的各個尺度都使用P-Net計算了一遍,因此形成了大小不同的人臉框。P-Net的結果還是比較粗糙的,所以接下來又使用R-Net進一步調優。R-Net的網絡結構如下圖所示。

       

      這個結構與之前的P-Net非常類似,P-Net的輸入是12x12x3的圖像,R-Net是24x24x3的圖像,也就是說,R-Net判斷24x24x3的圖像中是否含有人臉,以及預測關鍵點的位置。R-Net的輸出和P-Net完全一樣,同樣有人臉判別、框回歸、關鍵點位置預測三部分組成。

      在實際應用中,對每個P-Net輸出可能為人臉的區域都放縮到24×24的大小,在輸入到R-Net中,進行進一步的判定。得到的結果如下圖所示:

       

      顯然R-Net消除了P-Net中很多誤判的情況。

      進一步把所有得到的區域縮放成48×48的大小,輸入到最後的O-Net中,O-Net的結構同樣與P-Net類似,不同點在於它的輸入是48x48x3的圖像,網絡的通道數和層數也更多了。O-Net的網絡的結構如下圖所示:

       

      檢測結果如下圖所示:

       

      從P-Net到R-Net,最後再到O-Net,網絡輸入的圖片越來越大,卷積層的通道數越來越多,內部的層數也越來越多,因此它們識別人臉的準確率應該是越來越高的。同時,P-Net的運行速度是最快的,R-Net的速度其次,O-Net的運行速度最慢。之所以要使用三個網絡,是因為如果一開始直接對圖中的每個區域使用O-Net,速度會非常慢慢。實際上P-Net先做了一遍過濾,將過濾后的結果再交給R-Net進行過濾,最後將過濾后的結果交給效果最好但速度較慢的O-Net進行判別。這樣在每一步都提前減少了需要判別的數量,有效降低了處理時間。

      最後介紹MTCNN的損失定義和訓練過程。MTCNN中每個網絡都有三部分輸出,因此損失也由三部分組成。針對人臉判別部分,直接使用交叉熵損失,針對框回歸和關鍵點判定,直接使用L2損失。最後這三部分損失各自乘以自身的權重再加起來,就形成最後的總損失了。在訓練P-Net和R-Net時,更關心框位置的準確性,而較少關注關鍵點判定的損失,因此關鍵點判定損失的權重很小。對於O-Net,關鍵點判定損失的權重較大。

    二、使用深度卷積網絡提取特徵

      經過人臉檢測和人臉對齊兩個步驟,就獲得了包含人臉的區域圖像,接下來就要進行人臉識別了。這一步一般是使用深度卷積網絡,將輸入的人臉圖像轉換為一個向量的表示,也就是所謂的“特徵”。

      如何針對人臉來提取特徵?可以先來回憶VGG16的網絡結構(見),輸入神經網絡的是圖像,經過一系列卷積計算后,全連接分類得到類別概率。

      在通常的圖像應用中,可以去掉全連接層,使用卷積層的最後一層當作圖像的“特徵”。但如果對人臉識別問題同樣採用這種方法,即使用卷積層最後一層做為人臉的“向量表示”,效果其實是不好的。這其中的原因和改進方法是什麼?在後面會談到,這裏先談談希望這種人臉的“向量表示”應該具有哪些性質。

      在理想的狀況下,希望“向量表示”之間的距離可以直接反映人臉的相似度

      對於同一個人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較小。對於不同人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較大。

      例如,設人臉圖像為$x_{1}$,$x_{2}$,對應的特徵為$f(x_{1})$,$f(x_{2})$,當$x_{1}$,$x_{2}$對應是同一個人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很小,而當$x_{1}$,$x_{2}$是不同人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很大。

      在原始的CNN模型中,使用的是Softmax損失。Softmax是類別間的損失,對於人臉來說,每一類就是一個人。儘管使用Softmax損失可以區別出每個人,但其本質上沒有對每一類的向量表示之間的距離做出要求。

      舉個例子,使用CNN對MNIST進行分類,設計一個特殊的卷積網絡,讓其最後一層的向量變為2維,此時可以畫出每一類對應的2維向量(圖中一種顏色對應一種類別),如下圖所示:

       

      上圖是我們直接使用softmax訓練得到的結果,它就不符合我們希望特徵具有的特點:

      (1)我們希望同一類對應的向量表示盡可能接近。但這裏同一類(如紫色),可能具有很大的類間距離;
      (2)我們希望不同類對應的向量應該盡可能遠。但在圖中靠中心的位置,各個類別的距離都很近;

      對於人臉圖像同樣會出現類似的情況,對此,有很改進方法。這裏介紹其中兩種:一種是三元組損失函數(Triplet Loss),一種是中心損失函數。 

    三、三元組損失的定義

      三元組損失函數的原理:既然目標是特徵之間的距離應該具備某些性質,那麼我們就圍繞這個距離來設計損失。具體的,我們每次都在訓練數據中抽出三張人臉圖像,第一張圖像記為$x_{i}^{a}$,第二張圖像記為$x_{i}^{p}$,第三張圖像記為$x_{i}^{n}$。在這樣的一個“三元組”中,$x_{i}^{a}$和$x_{i}^{p}$對應的是同一個人的圖像,而$x_{i}^{n}$是另外一個不同的人的人臉圖像。因此,距離$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}$應該較小,而距離$\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}$應該較大。嚴格來說,三元組損失要求下面的式子成立:

       $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$

      然後計算相同人臉之間與不同人臉之間距離的平方

       $\left [ \left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2} \right ]_{+}$

      上式表達相同人臉間的距離平方至少要比不同人臉間的距離平方小α(取平方主要是為了方便求導),據此,上式實際上就是相當於一個損失函數。這樣的話,當三元組的距離滿足 $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$時,不產生任何損失,此時$L_{i}=0$。當距離不滿足上述等式時,就會有值為$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2}$的損失。此外,在訓練時會固定$\left \| f(x) \right \|_{2}=1$,以保證特徵不會無限地“遠離”。

      三元組損失直接對距離進行優化,因此可以解決人臉的特徵表示問題。但是在訓練過程中,三元組的選擇非常地有技巧性。如果每次都是隨機選擇三元組,雖然模型可以正確的收斂,但是並不能達到最好的性能。如果加入”難例挖掘”,即每次都選擇最難分辨率的三元組進行訓練,模型又往往不能正確的收斂。對此,又提出每次都選擇那些“半難”(Semi-hard)的數據進行訓練,讓模型在可以收斂的同時也保持良好的性能。此外,使用三元組損失訓練人臉模型通常還需要非常大的人臉數據集,才能取得較好的效果。

    四、中心損失的定義

      與三元組損失不同,中心損失(Center Loss)不直接對距離進行優化,它保留了原有的分類模型,但又為每個類(在人臉模型中,一個類就對應一個人)指定了一個類別中心。同一類的圖像對應的特徵都應該盡量靠近自己的類別中心,不同類的類別中心盡量遠離。與三元組損失函數相比,使用中心損失訓練人臉模型不需要使用特別的採樣方法,而且利用較少的圖像就可以達到與單元組損失相似的效果。下面我們一起來學習中心損失的定義:

       還是設輸入的人臉圖像為$x_{i}$,該人臉對應的類別為$y_{i}$,對每個類別都規定一個類別中心,記作$c_{yi}$。希望每個人臉圖像對應的特徵$f(x_{i})$都盡可能接近其中心$c_{yi}$。因此定義中心損失為:

        $L_{i}=\frac{1}{2}\left \| f(x_{i})-c_{yi}\right \|_{2}^{2}$

      多張圖像的中心損失就是將它們的值加在一起:

       $L_{center}=\sum\limits_{i}L_i$

      這是一個非常簡單的定義。不過還有一個問題沒有解決,那就是如何確定每個類別的中心$c_{yi}$呢?從理論上來說,類別$y_{i}$的最佳中心應該是它對應的所有圖片的特徵的平均值。但如果採取這樣的定義,那麼在每一次梯度下降時,都要對所有圖片計算一次$c_{yi}$,計算複雜度就太高了。針對這種情況,不妨近似一處理下,在初始階段,先隨機確定$c_{yi}$,接着在每個batch內,使用$L_i=\|f(x_i)-c_{yi}\|_2^2$對當前batch內的$c_{yi}$ 也計算梯度,並使用該梯度更新$c_{yi}$ 。此外,不能只使用中心損失來訓練分類模型,還需要加入Softmax損失,也就是說,最終的損失由兩部分構成,即$L = L_{softmax}+\lambda L_{center}$,其中$\lambda $是一個超參數。

      最後來總結使用中心損失來訓練人臉模型的過程。首先隨機初始化各个中心$c_{yi}$,接着不斷地取出batch進行訓練,在每個batch中,使用總的損失$L$,除了使用神經網絡模型的參數對模型進行更新外,也對$c_{yi}$進行計算梯度,並更新中心的位置。

      中心損失可以讓訓練處的特徵具有“內聚性”。還是以MNIST的例子來說,在未加入中心損失時,訓練的結果不具有內聚性。再加入中心損失后,得到的特徵如下圖所示。 

       

    從圖中可以看出,當中心損失的權重λ越大時,生成的特徵就會具有越明顯的“內聚性” 。

    五、使用特徵設計應用

    當提取出特徵后,剩下的問題就非常簡單了。因為這種特徵已經具有了相同人對應的向量的距離小,不同人對應的向量距離大的特點,接下來,一般的應用有以下幾類:

    • 人臉驗證(Face Identification)。就是檢測A、B是否屬於同一個人。只需要計算向量之間的距離,設定合適的報警閾值(threshold)即可。
    • 人臉識別(Face Recognition)。這個應用是最多的,給定一張圖片,檢測數據庫中與之最相似的人臉。顯然可以被轉換為一個求距離的最近鄰問題。
    • 人臉聚類(Face Clustering)。在數據庫中對人臉進行聚類,直接用K-means即可。

     

     

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

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

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

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

  • Android DecorView 與 Activity 綁定原理分析

    Android DecorView 與 Activity 綁定原理分析

    一年多以前,曾經以為自己對 View 的添加显示邏輯已經有所了解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的添加显示。

    概論

    Android 中 Activity 是作為應用程序的載體存在,代表着一個完整的用戶界面,提供了一個窗口來繪製各種視圖,當 Activity 啟動時,我們會通過 setContentView 方法來設置一個內容視圖,這個內容視圖就是用戶看到的界面。那麼 View 和 activity 是如何關聯在一起的呢 ?

     上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:

    • Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。

    • PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製窗口。並且,該類內部包含了一個 DecorView 對象,該 DectorView 對象是所有應用窗口的根 View。
    • DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(窗口內容的容器)。

    • ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設置它的子 View 。

    • WindowManager : 是一個接口,裏面常用的方法有:添加View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。
    • WindowManagerService (WMS) : 負責管理各 app 窗口的創建,更新,刪除, 显示順序。運行在 system_server 進程。

    ViewRootImpl :擁有 DecorView 的實例,通過該實例來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通過調用 IWindow 一些方法,通過 Binder 通信的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來調用 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub,每一個應用進程都有一個唯一的 Session 對象與 WMS 通信。

    DecorView 的創建 

    先從 Mainactivity 中的代碼看起,首先是調用了 setContentView;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    該方法是父類 AppCompatActivity 的方法,最終會調用 AppCompatDelegateImpl 的 setContentView 方法:

    // AppCompatDelegateImpl  
    public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }

    ensureSubDecor 從字面理解就是創建 subDecorView,這個是根據主題來創建的,下文也會講到。創建完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 布局添加到裏面。不過大家注意的是,在添加之前先調用 removeAllViews() 方法,確保沒有其他子 View 的干擾。

        private void ensureSubDecor() {
            if (!this.mSubDecorInstalled) {
                this.mSubDecor = this.createSubDecor(); 
                ......
            }
            ......
        }        

     最終會調用 createSubDecor() ,來看看裏面的具體代碼邏輯:

     private ViewGroup createSubDecor() {
            // 1、獲取主題參數,進行一些設置,包括標題,actionbar 等 
            TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
            if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
                a.recycle();
                throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
            } else {
                if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
                    this.requestWindowFeature(1);
                } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
                    this.requestWindowFeature(108);
                }
    
                if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                    this.requestWindowFeature(109);
                }
    
                if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                    this.requestWindowFeature(10);
                }
    
                this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
                a.recycle();
                // 2、確保優先初始化 DecorView
                this.mWindow.getDecorView();
                LayoutInflater inflater = LayoutInflater.from(this.mContext);
                ViewGroup subDecor = null;
                // 3、根據不同的設置來對 subDecor 進行初始化
                if (!this.mWindowNoTitle) {
                    if (this.mIsFloating) {
                        subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
                        this.mHasActionBar = this.mOverlayActionBar = false;
                    } else if (this.mHasActionBar) {
                        TypedValue outValue = new TypedValue();
                        this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
                        Object themedContext;
                        if (outValue.resourceId != 0) {
                            themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
                        } else {
                            themedContext = this.mContext;
                        }
    
                        subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
                        this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                        this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
                        if (this.mOverlayActionBar) {
                            this.mDecorContentParent.initFeature(109);
                        }
    
                        if (this.mFeatureProgress) {
                            this.mDecorContentParent.initFeature(2);
                        }
    
                        if (this.mFeatureIndeterminateProgress) {
                            this.mDecorContentParent.initFeature(5);
                        }
                    }
                } else {
                    if (this.mOverlayActionMode) {
                        subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
                    } else {
                        subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
                    }
    
                    if (VERSION.SDK_INT >= 21) {
                        ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
                            public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                                int top = insets.getSystemWindowInsetTop();
                                int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
                                if (top != newTop) {
                                    insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
                                }
    
                                return ViewCompat.onApplyWindowInsets(v, insets);
                            }
                        });
                    } else {
                        ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
                            public void onFitSystemWindows(Rect insets) {
                                insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
                            }
                        });
                    }
                }
    
                if (subDecor == null) {
                    throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
                } else {
                    if (this.mDecorContentParent == null) {
                        this.mTitleView = (TextView)subDecor.findViewById(id.title);
                    }
    
                    ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                    ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                    ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                    if (windowContentView != null) {
                        while(windowContentView.getChildCount() > 0) {
                            View child = windowContentView.getChildAt(0);
                            windowContentView.removeViewAt(0);
                            contentView.addView(child);
                        }
    
                        windowContentView.setId(-1);
                        contentView.setId(16908290);
                        if (windowContentView instanceof FrameLayout) {
                            ((FrameLayout)windowContentView).setForeground((Drawable)null);
                        }
                    }
                    // 將 subDecor 添加到 DecorView 中
                    this.mWindow.setContentView(subDecor);
                    contentView.setAttachListener(new OnAttachListener() {
                        public void onAttachedFromWindow() {
                        }
    
                        public void onDetachedFromWindow() {
                            AppCompatDelegateImpl.this.dismissPopups();
                        }
                    });
                    return subDecor;
                }
            }
        }
                        

    上面的代碼總結來說就是在做一件事,就是創建 subDecor。攤開來說具體如下:

    1、根據用戶選擇的主題來設置一些显示特性,包括標題,actionbar 等。

    2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。

    3、最後添加到 DecorView中。

    添加的具體代碼如下:此處是通過調用 

     // AppCompatDelegateImpl   this.mWindow.getDecorView();
    
     // phoneWindow    public final View getDecorView() {
            if (mDecor == null || mForceDecorInstall) {
                installDecor();
            }
            return mDecor;
        }
     
    
    private void installDecor() {
            mForceDecorInstall = false;
            if (mDecor == null) {
     // 生成 DecorView             mDecor = generateDecor(-1);
                mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
                mDecor.setIsRootNamespace(true);
                if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
                }
            } else {
     // 這樣 DecorView 就持有了window             mDecor.setWindow(this);
            }
          ......
    }
    
    
       protected DecorView generateDecor(int featureId) {
            // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity.
            Context context;
            if (mUseDecorContext) {
                Context applicationContext = getContext().getApplicationContext();
                if (applicationContext == null) {
                    context = getContext();
                } else {
                    context = new DecorContext(applicationContext, getContext());
                    if (mTheme != -1) {
                        context.setTheme(mTheme);
                    }
                }
            } else {
                context = getContext();
            }
            return new DecorView(context, featureId, this, getAttributes());
       }

    到此,DecorView 的創建就講完了。可是我們似乎並沒有看到 DecorView 是被添加的,什麼時候對用戶可見的。

     WindowManager

    View 創建完以後,那 Decorview 是怎麼添加到屏幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。

    看 ActivityThread 中的 handleResumeActivity 方法:

    // ActivityThread
    public
    void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) {           // 這裏也會調用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }

    上面的代碼主要做了以下幾件事:

    1、獲取到 DecorView,設置不可見,然後通過 wm.addView(decor, l) 將 view 添加到 WindowManager;

    2、在某些情況下,比如此時點擊了輸入框調起了鍵盤,就會調用 wm.updateViewLayout(decor, l) 來更新 View 的布局。

    3、這些做完以後,會調用 activity 的  makeVisible ,讓視圖可見。如果此時 DecorView 沒有添加到 WindowManager,那麼會添加。 

    // Activity
    void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }

     接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:

    // WindowManagerGlobal  
     public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
               // ......
        
                root = new ViewRootImpl(view.getContext(), display);
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
               // do this last because it fires off messages to start doing things
                try {
                    root.setView(view, wparams, panelParentView);
                } catch (RuntimeException e) {
                    // BadTokenException or InvalidDisplayException, clean up.
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                } 
    }

    在這裏,實例化了 ViewRootImpl 。同時調用 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裏還保存了 DecorView ,Params,以及 ViewRootImpl 的實例。

    現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。

     ViewRootImpl

    實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程序窗口的 DecorView 都有一個與之關聯的 ViewRootImpl 對象,這種關聯關係是由 WindowManager 來維護的。

    先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:

       /**
         * We have one child
         */
        public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            synchronized (this) {
                if (mView == null) {
                    mView = view;
                    ......
                   
                    mAdded = true;
                    int res; /* = WindowManagerImpl.ADD_OKAY; */
    
                    // Schedule the first layout -before- adding to the window
                    // manager, to make sure we do the relayout before receiving
                    // any other events from the system.
    
                    requestLayout();
                    ......
                }
            }
        }

    這裏先將 mView 保存了 DecorView 的實例,然後調用 requestLayout() 方法,以完成應用程序用戶界面的初次布局。

     public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true;
                scheduleTraversals();
            }
        }

    因為是 UI 繪製,所以一定要確保是在主線程進行的,checkThread 主要是做一個校驗。接着調用 scheduleTraversals 開始計劃繪製了。

    void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }

    這裏主要關注兩點:

    mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步消息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理異步消息,如果沒有異步消息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設置為異步消息)。

    mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。

    mTraversalRunnable :TraversalRunnable 的實例,是一個Runnable,最終肯定會調用其 run 方法:

    final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }

    doTraversal,如其名,開始繪製了,該方法內部最終會調用 performTraversals 進行繪製。

      void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
    
                performTraversals();
    
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }

    到此,DecorView 與 activity 之間的綁定關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。 

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

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

  • Java虛擬機詳解(十)——類加載過程

    Java虛擬機詳解(十)——類加載過程

      在上一篇文章中,我們詳細的介紹了Java,那麼這些Class文件是如何被加載到內存,由虛擬機來直接使用的呢?這就是本篇博客將要介紹的——類加載過程。

    1、類的生命周期

      類從被加載到虛擬機內存開始,到卸載出內存為止,其聲明周期流程如下:

      

      上圖中紅色的5個部分(加載、驗證、準備、初始化、卸載)順序是確定的,也就是說,類的加載過程必須按照這種順序按部就班的開始。這裏的“開始”不是按部就班的“進行”或者“完成”,因為這些階段通常是互相交叉混合的進行的,通常會在一個階段執行過程中調用另一個階段。

    2、加載

      “加載”階段是“類加載”生命周期的第一個階段。在加載階段,虛擬機要完成下面三件事:

      ①、通過一個類的全限定名來獲取定義此類的二進制字節流。

      ②、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

      ③、在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。

      PS:類的全限定名可以理解為這個類存放的絕對路徑。方法區是JDK1.7以前定義的運行時數據區,而在JDK1.8以後改為元數據區(Metaspace),主要用於存放被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。詳情可以參考這邊該系列的第二篇文章——。

      另外,我們看第一點——通過類的權限定名來獲取定義此類的二進制流,這裏並沒有明確指明要從哪裡獲取以及怎樣獲取,也就是說並沒有明確規定一定要我們從一個 Class 文件中獲取。基於此,在Java的發展過程中,充滿創造力的開發人員在這個舞台上玩出了各種花樣:

      1、從 ZIP 包中讀取。這稱為後面的 JAR、EAR、WAR 格式的基礎。

      2、從網絡中獲取。比較典型的應用就是 Applet。

      3、運行時計算生成。這就是動態代理技術。

      4、由其它文件生成。比如 JSP 應用。

      5、從數據庫中讀取。

      加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,然後在Java堆中實例化一個 java.lang.Class 類的對象,這個對象將作為程序訪問方法區中這些類型數據的外部接口。

      注意,加載階段與連接階段的部分內容(如一部分字節碼文件的格式校驗)是交叉進行的,加載階段尚未完成,連接階段可能已經開始了。

    3、驗證

      驗證是連接階段的第一步,作用是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

      我們說Java語言本身是相對安全,因為編譯器的存在,純粹的Java代碼要訪問數組邊界外的數據、跳轉到不存在的代碼行之類的,是要被編譯器拒絕的。但是前面我們也說過,Class 文件不一定非要從Java源碼編譯過來,可以使用任何途徑,包括你很牛逼,直接用十六進制編輯器來編寫 Class 文件。

      所以,如果虛擬機不檢查輸入的字節流,將會載入有害的字節流而導致系統崩潰。但是虛擬機規範對於檢查哪些方面,何時檢查,怎麼檢查都沒有明確的規定,不同的虛擬機實現方式可能都會有所不同,但是大致都會完成下面四個方面的檢查。

    ①、文件格式驗證

      校驗字節流是否符合Class文件格式的規範,並且能夠被當前版本的虛擬機處理。

      一、是否以魔數 0xCAFEBABE 開頭。

      二、主、次版本號是否是當前虛擬機處理範圍之內。

      三、常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)

      四、指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。

      五、CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據。

      六、Class 文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

      以上是一部分校驗內容,當然遠不止這些。經過這些校驗后,字節流才會進入內存的方法區中存儲,接下來後面的三個階段校驗都是基於方法區的存儲結構進行的。

    ②、元數據驗證

      第二個階段主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範要求。

      一、這個類是否有父類(除了java.lang.Object 類之外,所有的類都應當有父類)。

      二、這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

      三、如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有普通方法。

      四、類中的字段、方法是否與父類產生了矛盾(例如覆蓋了父類的final字段、或者出現不符合規則的重載)

    ③、字節碼驗證

      第三個階段字節碼驗證是整個驗證階段中最複雜的,主要是進行數據流和控制流分析。該階段將對類的方法進行分析,保證被校驗的方法在運行時不會做出危害虛擬機安全的行為。

      一、保證任意時刻操作數棧中的數據類型與指令代碼序列都能配合工作。例如不會出現在操作數棧中放置了一個 int 類型的數據,使用時卻按照 long 類型來加載到本地變量表中。

      二、保證跳轉指令不會跳轉到方法體以外的字節碼指令中。

      三、保證方法體中的類型轉換是有效的。比如把一個子類對象賦值給父類數據類型,這是安全的。但是把父類對象賦值給子類數據類型,甚至賦值給完全不相干的類型,這就是不合法的。

    ④、符號引用驗證

      符號引用驗證主要是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,通常需要校驗如下內容:

      一、符號引用中通過字符串描述的全限定名是否能夠找到相應的類。

      二、在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。

      三、符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可以被當前類訪問。

    4、準備

      準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存是在方法區中進行分配。

      注意:

      一、上面說的是類變量,也就是被 static 修飾的變量,不包括實例變量。實例變量會在對象實例化時隨着對象一起分配在堆中。

      二、初始值,指的是一些數據類型的默認值。基本的數據類型初始值如下(引用類型的初始值為null):

      

     

       比如,定義 public static int value = 123 。那麼在準備階段過後,value 的值是 0 而不是 123,把 value 賦值為123 是在程序被編譯后,存放在類的構造器方法之中,是在初始化階段才會被執行。但是有一種特殊情況,通過final 修飾的屬性,比如 定義 public final static int value = 123,那麼在準備階段過後,value 就被賦值為123了。

    5、解析

      解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程。

      符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。

      直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在內存中存在。

      解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用,分別對應於常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANTS_InterfaceMethodref_info四種類型常量。

    6、初始化

       初始化階段是類加載階段的最後一步,前面過程中,除第一個加載階段可以通過用戶自定義類加載器參与之外,其餘過程都是完全由虛擬機主導和控制。而到了初始化階段,則開始真正執行類中定義的Java程序代碼(或者說是字節碼)。

      在前面介紹的準備階段中,類變量已經被賦值過初始值了,而初始化階段,則根據程序員的編碼去初始化變量和資源。

      換句話來說,初始化階段是執行類構造器<clinit>() 方法的過程

      ①、<clinit>() 方法 是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。

      比如如下代碼會報錯:

      

     

       但是你把第 14 行代碼放到 static 靜態代碼塊的上面就不會報錯了。或者不改變代碼順序,將第 11 行代碼移除,也不會報錯。

      ②、<clinit>() 方法與類的構造函數(或者說是實例構造器<init>()方法)不同,它不需要显示的調用父類構造器,虛擬機會保證在子類的<init>()方法執行之前,父類的<init>()方法已經執行完畢。因此虛擬機中第一個被執行的<init>()方法的類肯定是 java.lang.Object。

      ③、由於父類的<clinit>() 方法先執行,所以父類中定義的靜態語句塊要優先於子類的變量賦值操作。

      ④、<clinit>() 方法對於接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成<clinit>() 方法。

      ⑤、接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>() 方法。但接口與類不同的是,執行接口中的<clinit>() 方法不需要先執行父接口的<clinit>() 方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。

      ⑥、接口的實現類在初始化時也一樣不會執行接口的<clinit>() 方法。

      ⑦、虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖和同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>() 方法,其他的線程都需要阻塞等待,直到活動線程執行<clinit>() 方法完畢。如果在一個類的<clinit>() 方法中有很耗時的操作,那麼可能造成多個進程的阻塞。

      比如對於如下代碼:

    package com.yb.carton.controller;
    
    /**
     * Create by YSOcean
     */
    public class ClassLoadInitTest {
    
    
        static class Hello{
            static {
                if(true){
                    System.out.println(Thread.currentThread().getName() + "init");
                    while(true){}
                }
            }
        }
    
        public static void main(String[] args) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"start");
                Hello h1 = new Hello();
                System.out.println(Thread.currentThread().getName()+"run over");
            }).start();
    
    
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"start");
                Hello h2 = new Hello();
                System.out.println(Thread.currentThread().getName()+"run over");
            }).start();
        }
    
    }

    View Code

      運行結果如下:

      

     

       線程1搶到了執行<clinit>() 方法,但是該方法是一個死循環,線程2將一直阻塞等待。

      知道了類的初始化過程,那麼類的初始化何時被觸發呢?JVM大概規定了如下幾種情況:

      ①、當虛擬機啟動時,初始化用戶指定的類。

      ②、當遇到用以新建目標類實例的 new 指令時,初始化 new 指定的目標類。

      ③、當遇到調用靜態方法的指令時,初始化該靜態方法所在的類。

      ④、當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類。

      ⑤、子類的初始化會觸發父類的初始化。

      ⑥、如果一個接口定義了 default 方法,那麼直接實現或間接實現該接口的類的初始化,會觸發該接口的初始化。

      ⑦、使用反射 API 對某個類進行反射調用時,會初始化這個類。

      ⑧、當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

     

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

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

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

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

  • YAMAHA 與 Gogoro 合作首款電動速克達 EC-05 於 8 月上市,售價 99,800 元

    YAMAHA 與 Gogoro 合作首款電動速克達 EC-05 於 8 月上市,售價 99,800 元

    台灣山葉(YAMAHA)和 Gogoro 在 2018 年宣布展開合作之後,不少消費者都好奇兩家公司會擦出什麼樣的火花。YAMAHA 終於在 6 月 27 日正式發表電動速克達 EC-05,也是雙方合作的第一款機車。

    EC-05 採用 Gogoro 市售車種的平台架構,搭配 YAMAHA 的原創設計,未來也會掛上 YAMAHA 的品牌。EC-05 會以換電為動力來源,支援 Gogoro 旗下的換電站 GoStation,不過車主在購車後需要另行和 Gogoro 簽約購買換電服務。新款的電動速克達由 Gogoro 負責製造,並透過 YAMAHA 的通路進行銷售。

    EC-05 的動力系統是 G2 鋁合金水冷永磁同步馬達(S-Version),配合 MOSFET 水冷馬達控制器。安全極速達到時速 90 公里,靜止加速到時速 50 公里僅需 3.9 秒。空車重量為 106 公斤,加上電池則為 126 公斤,擁有 25L 的置物空間。EC-05 可以連結智慧型手機和 Apple Watch,並使用 NFC 和藍牙進行連結。

    EC-05 的電池位於車身當中,座墊底層結構、置物箱的開口部位與收納空間都和 Gogoro 現有車種相同,不過重塑車身線條的設計風格。里程表則根據 Gogoro 原有的元件進行調整,略為提高並向前方移動,減少騎乘者所需的視線移動。坐墊結構與材質沿用 Gogoro 的現行車種,不過座墊後方略為墊高,提供加速時的止滑和支撐。

    EC-05 的電池位於車身內部,擁有 25L 的置物空間。

    EC-05 的里程表和 Gogoro 的車種類似,不過略為提高並向前方移動。

    EC-05 的頭燈。

    EC-05 的尾燈。

    YAMAHA 表示,EC-05 能夠幫助擴展台灣市場的產品線,不僅回應多樣化顧客的需求,也向電動車領域跨出一步。YAMAHA 指出 EC-05 將為 YAMAHA 未來的電動機車家族打下基礎,似乎暗示未來會推出更多與 Gogoro 合作的車款。YAMAHA 台灣總經理小川真司表示,YAMAHA 日本母公司與其他大廠合作的機車聯盟目前只限於日本國內的市場,因此並不會影響台灣子公司和 Gogoro 的合作。

    小川真司認為雖然 EC-05 內部與 Gogoro 的車種相同,但 YAMAHA 的風格與 Gogoro 不同,而且雙方的消費族群也不一樣,可以給不同生活方式的族群不同的選擇。此外,YAMAHA 在台灣市場已經深耕多年,有更多的經銷商夥伴,無論是銷售或保養都能更貼近消費者。YAMAHA 對 EC-05 頗具信心,喊出了一年 2 萬台的銷售目標。

    EC-05 提供藍灰色、深黑色、深藍灰色和白銀色 4 種顏色讓消費者選擇,定價為台幣 99,800 元,補助最多的桃園市汰換二行程機車換購電動機車補助 33,000 元,因此最低台幣 66,800 元起。預計將在 7 月 1 日開放預購,8 月 1 日正式上市。YAMAHA 將在台北、台中和高雄展開 EC-05 的巡迴賞車活動,活動期間參與的消費者將有機會抽中電動速克達 EC-05。

    EC-05 共推出藍灰色、深黑色、深藍灰色和白銀色四款顏色。

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

    延伸閱讀:

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

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

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

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

  • 豐田衝刺燃料電池車,傳產能將擴增至 10 倍以上

    豐田衝刺燃料電池車,傳產能將擴增至 10 倍以上

    日刊工業新聞 3 日,豐田汽車(Toyota)計劃於 2020 年將燃料電池車(FCV)月產能提高至 3,000 台,將達現行的 10 倍以上水準。豐田計劃在 2020 年下半推出 FCV 車「MIRAI」的次代車款。

    據報導,豐田目前利用元町工廠的專用產線生產「MIRAI」,年產能約 3,000 台,依此換算月產能相當於 250 台左右。

    2018 年 MIRAI 全球銷售量約 2,400 台,而豐田目標在 2020 年以後將 FCV 年銷售量提高至 3 萬台以上水準。

    截至台灣時間 3 日上午 10 點 21 分為止,豐田下跌 0.84%。

    豐田於 2018 年 5 月 24 日宣布,為了因應計劃在 2020 年以後將 FCV 全球年銷售量提高至 3 萬台以上水準的目標,決議將增產 FCV 關鍵零件,計劃在愛知縣豐田市的本社工廠廠區內興建新廠房、增產燃料電池堆(Fuel Cell stack),且也計劃在愛知縣三好市的下山工廠內增設用來儲存氫燃料的高壓氫氣槽專用產線。上述新廠預計於 2020 年左右啟用。

    (本文內容由 授權使用。首圖來源: CC BY 2.0)

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

  • 搶攻 EV 需求,三菱化學傳倍增美國鋰電池關鍵材料產能

    搶攻 EV 需求,三菱化學傳倍增美國鋰電池關鍵材料產能

    日刊工業新聞 3 日報導,為了搶攻電動車(EV)需求,三菱化學(Mitsubishi Chemical)將擴增鋰離子電池關鍵材料「電解液」產能,計畫於 2019 年度內(2020 年 3 月底前)將美國工廠(位於田納西州曼非斯)電解液年產能提高至 2 萬噸、將達現行的 2 倍水準。

    三菱化學計畫於 2020 年度結束前(2021 年 3 月底前)將電解液全球年產能提高至 8.5 萬噸、將較 2017 年度大增 95%,且計畫將另一項鋰電池關鍵材料「負極材」全球年產能提高至 2.9 萬噸(將較 2017 年度增加 61%),目標在 2020 年度將電池材料等新能源部門營收提高至 1,000 億日圓、2025 年度進一步倍增至 2,000 億日圓的規模。

    三菱化學 2018 年 12 月 26 日宣布,日本國內外電動車、插電式油電混合車(PHV)、油電混合車(HV)市場呈現急速擴大,因此將擴增日本「電解液」產能,計畫將四日市事業所的電解液年產能自現行的 1.1 萬噸大幅擴產約 5 成至 1.6 萬噸。

    日韓企業紛紛擴增鋰離子電池關鍵材料產能

    旭化成(Asahi Kasei)3 月 14 日宣布,因鋰離子電池以電動車等車用需求為中心呈現急速增長,故決議投資 300 億日圓對位於日本滋賀縣守山市的守山製造所、以及位於北卡羅萊納州的美國工廠進行增產投資,增產鋰離子電池關鍵材料「分隔膜」,上述增產工程預計於 2021 年度上半年開始商轉,預估 2021 年度旭化成分隔膜全球年產能將擴增至約 15.5 億平方公尺、將較 2018 年度末提高 1 倍。

    住友化學(Sumitomo Chemical)日前也傳出將階段性提高南韓工廠產能,目標在 2021 年度將分隔膜總年產能(合併日本工廠產能計算)提高至 6 億平方公尺、將達現行的近 2 倍水準。

    全球第 2 大鋰離子電池關鍵材料「分隔膜」廠商南韓 SK Innovation 5 月 27 日表示,計畫在 2025 年結束前將分隔膜產能提高至現行的 5 倍,SK Innovation 社長金俊 27 日在首爾舉行的記者會上表示,「目標藉由大規模增產、搶當全球龍頭」。在全球分隔膜市場上,日本旭化成市佔約 2 成、位居首位,SK Innovation 市佔率超過 1 成居次。

    日本民間調查機構矢野經濟研究所公布調查報告指出,因中國自 2019 年起開始實施新能源車規範,加上 2019-2020 年期間日歐車廠將進行車輛電動化,提振車用鋰離子電池材料需求今後將持續擴大,預估 2020 年全球 4 大關鍵材料(正極材、負極材、電解液和分隔膜)市場規模(廠商出貨金額)將擴增至 281.46 億美元、將較 2017 年暴增 9 成(增加 91.3%)。

    (本文內容由 授權使用。首圖來源:)

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

    【其他文章推薦】

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

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

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

  • NEDO、夏普和豐田攜手,高效太陽能電動車 7 月下旬上路測試

    NEDO、夏普和豐田攜手,高效太陽能電動車 7 月下旬上路測試

    豐田的插電式油電混合車 Prius PHV 將再出發。最近夏普、豐田以及日本新能源產業技術總合開發機構(NEDO)攜手合作,在 Prius PHV 裝設 34% 超高效率太陽能板,推出全新規模的太陽能油電混合車,預計將在 7 月下旬上路測試。

    Prius PHV 是從 Prius 衍伸出來的插電式油電車,除了一般的鋰離子充電系統,還可以裝設轉換效率達 22%、容量共 180W 的太陽能板,只不過其在日照充裕時只能增加 6.1公里的行駛距離,著實不夠。

    而現在該團隊決定換一種太陽能光電技術,NEDO 透過磷化銦鎵(InGaP)、砷化鎵(GaAs)、砷化銦鎵(InGaAs)等三五族半導體,研發出轉換效率超越 34% 的超高效率三接面(Triple-junction)太陽能板。

    這些僅有 0.03mm 太陽能板將會裝設在引擎蓋、車頂與後車箱之上,也因為搭載的太陽能板轉換效率大幅提升,發電容量也不可同日而語,已從 180W 躍升至 860W,車輛靜止狀態下可增加 44.5 的續航距離,是過去 Prius PHV 車型的 7 倍左右,且車輛行駛時也能提供電力,將續航距離提升至 56.3 公里。

    其中該計畫是由 NEDO 主導,2016 年 4 月時 NEDO 成立車載太陽戰略委員會,希望能透過太陽能系統,找出緩解交通能源與環境問題的解決方案,而團隊目前盼望能在有限的裝設空間下,利用轉換效率高達 30% 的太陽能板,實現 1KW 的發電潛力。

    目前團隊將會在本月下旬在日本東京、愛知縣豐田市的道路、高速公路上進行測試,測試時間將在 2020 年 2 月底結束。 NEDO、豐田與夏普將能共享實驗測試的全部數據,也將會進一步評估能降低多少二氧化碳排放量、是否真的能降低充電次數以及大眾的接受度等等。

    隨著太陽能與電動車技術日新月異,未來將會有愈來愈多新奇有趣車型出爐,雖然些車輛的外型或是性能,還無法跟傳統燃油車相比,但這些都是綠色能源車的新嘗試。就好比先前荷蘭新創公司 Lightyear 也宣布,首款太陽能電動原型車 Lightyear One 預計在 2021 年開始量產上市,充電一次就能跑 725 公里。

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

    延伸閱讀:

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

    【其他文章推薦】

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

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

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

  • Chrome OS 80 將可以直接構建 Android 應用

    Chrome OS 80 將可以直接構建 Android 應用

      在近期 Android Dev Summit 上,Chrome OS 團隊宣布 Chrome OS 80 將使 Chromebook 可以直接構建 Android 應用。

      這項特性其實是在 Chrome OS 中引入 Android 應用側加載(sideloading),該功能的具體介紹來自一個非公開 bug 記錄以及相應的代碼更改,根據該記錄,Android 應用的側加載被帶到了 Chromebook 上的 Android 容器中。

      根據內部文件,具體開發時的操作是啟動 Crostini 容器時需要一個特殊命令(從 Chromebook 的命令行啟動 Linux 時),需要添加–enable-features = ArcAdbSideloading

      目前開發人員必須通過 USB 線將 Android 設備連接到 Chromebook,然後將其應用推送到設備上進行測試或使用 Chrome OS 開發人員模式,才能構建 Android 應用,但這兩種都不是理想的選擇。

      這項新特性對於使用 Android Studio 在 Chromebook 上構建其應用的 Android 開發人員來說,是極其方便的功能。具體來看,Chrome OS 80 將為 Android 開發人員添加選項,這樣可以直接在 Chrome OS 設備上安裝和測試其應用。

      消息來源:

      https://www.aboutchromebooks.com/news/chrome-os-80-to-bring-arc-sideloading-of-android-apps-to-chromebooks

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

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

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

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

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