Android字體工作原理

        本文簡單介紹了上層如何使用特定字體,android字體工作原理提出要求瀏覽器使用單獨的字體,並且不影響系統默認字體的使用。圖形小組在該需求的基礎上,又提出瞭如果一個ttf文件包含多個字庫,skia是否支持,如果不支持,實現難度多大。本文主要在這兩個方面展開。
1 問題分析
        針對以上需求,主要從以下兩個方面展開:
        (1).android是否提供設置特定字體的標準API供應用使用
        (2).android字體工作原理
        經過調研發現,android本身是支持應用程序設置特定字體,並且不影響其他應用程序的顯示。所以問題主要落在android字體工作原理上面,該問題由於android4.0之後工程不再附帶skia的源碼,使得的問題又多了一層,以下將詳細介紹。
2  Android設置特定字體API
        設置特定字體,一般有如下步驟:
        (1).創建字體
        (2).設置字體
        android.graphics.Typeface提供了3個API供應用程序創建特定字體:
        (1).Typeface createFromAsset(AssetManager mgr, String path);
        (2).Typeface createFromFile(File path);
        (3).Typeface createFromFile(String path);
        以及如下3個API來創建系統字體:
        (1).Typeface create(String familyName, int style);
        (2).Typeface create(Typeface family, int style);
        (3).Typeface defaultFromStyle(int style);
        系統字體放在/system/fonts目錄下,可以通過查看/system/etc/system_fonts.xml來了解系統當前支持的字體文件及其名字對應關係。

        system_fonts.xml部分內容:

<familyset>
    <family>
        <nameset>
            <name>sans-serif</name>
            <name>arial</name>
            <name>helvetica</name>
            <name>tahoma</name>
            <name>verdana</name>
        </nameset>
        <fileset>
            <file>Roboto-Regular.ttf</file>
            <file>Roboto-Bold.ttf</file>
            <file>Roboto-Italic.ttf</file>
            <file>Roboto-BoldItalic.ttf</file>
        </fileset>
    </family>

    <family>
        <nameset>
            <name>sans-serif-light</name>
        </nameset>
        <fileset>
            <file>Roboto-Light.ttf</file>
            <file>Roboto-LightItalic.ttf</file>
        </fileset>
    </family>

    <family>
        <nameset>
            <name>sans-serif-thin</name>
        </nameset>
        <fileset>
            <file>Roboto-Thin.ttf</file>
            <file>Roboto-ThinItalic.ttf</file>
        </fileset>
    </family>
</familyset>

        nameset代表該font family可以有的名字,fileset表示該font family所對應的ttf文件,由上至下分別代表正常、粗體、斜體、粗斜體所對應的ttf文件, android skia會根據這些來初始化相應的變量。

        創建完字體之後,則可以通過android.graphics.Paint的TypefacesetTypeface(Typeface typeface);來設置。
        代碼示例:
        typeface =Typeface.createFromFile("/system/fonts/XXX.ttf");   
        paint.setTypeface(typeface);

3 Android字體工作原理
3.1 Android skia源碼下載
        android 4.0上不再有skia源碼,可以前往如下地址下載源碼:http://code.google.com/p/skia/
        同時如下網址講解了如何在android上下載skia源碼並編譯,在此不再贅述。
        https://sites.google.com/site/skiadocs/user-documentation/quick-start-guides/android
3.2 Android系統字庫加載
        android字體由android2D圖形引擎skia實現,並在Zygote的Preloading classes中對系統字體進行load。
        相關涉及到: 
        android的啓動過程 
        frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中的preloadClasses方法,對/framework/base/preloaded-classes文件裏面的類一一加載Class.forName("android.graphics.Typeface");Class.forName()會加載類到DVM(JVM),同時加載static代碼塊。 

        java中class.forName和classLoader都可用來對類進行加載。前者除了將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊。而classLoader只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance纔會去執行static塊。Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。並且只有調用了newInstance()方法採用調用構造函數,創建類的對象。 

        android.graphics.Typeface Static代碼:

static { 
    DEFAULT = create((String)null,0); 
    DEFAULT_BOLD = create((String)null, Typeface.BOLD); 
    SANS_SERIF = create("sans-serif", 0); 
    SERIF =create("serif", 0); 
    MONOSPACE = create("monospace", 0); 
    sDefaults = newTypeface[] { 
    DEFAULT, 
    DEFAULT_BOLD, 
    create((String)null, Typeface.ITALIC), 
    create((String)null, Typeface.BOLD_ITALIC), 
}; 
public static Typeface create(String familyName, int style) { 
    return newTypeface(nativeCreate(familyName, style)); 
} 
        nativeCreate()是jni方法,其實現在Typeface.cpp和skFontHost_android.cpp,其中後者是skia針對android平臺字體實現的port。以下是Typeface.cpp native方法的註冊。
由此可以知道,在JAVA層默認會創建sans-serif,serif,monospace三種字體,並且通過create第一個參數爲null,來創建默認字體的四種style:normal,bold,italic,bolditalic。
        Typeface_create又進一步調用face = SkTypeface::CreateFromName(str.c_str(),style);來完成。
SkTypeface*SkTypeface::CreateFromName(const char name[], Style style) {
    return SkFontHost::CreateTypeface(NULL,name, style);
}
         最終會調用到SkFontHost_android.cpp中的createTypeface。該函數做了兩件事:
        (1),首先調用load_system_fonts()加載系統字庫,該函數會首先判斷相關的系統字庫變量沒有被初始化,沒有則加載,否則什麼都不做,load_system_fonts()加載的字庫是由/system/etc/system_fonts.xml來進行配置的。 

        (2),在當前的系統字庫裏面查找與所要求最接近的字體,並返回。

        由此JAVA層與C層聯繫起來,而創建系統字庫,對與JAVA層來說也只是返回C層的一個ref。可以用圖2來表示android系統字庫加載過程:

        Zygote preloadClasses() -> Class.forName("android.graphics.Typeface")–> 執行Typeface.javastatic塊 -> create創建默認字體 -> Typeface.cpp Typeface_create–            >SkTypeface.cpp CreateFromName() -> SkFontHost.cpp load_system_fonts()加載系統字體。
3.3 SkFontHost_android.cpp重要結構體及方法
        重要結構體介紹:

struct FontFamily {
    SkTDArray<constchar*>  fNames;
    SkTDArray<const char*> fFileNames;
    int order;
};
        解析system_fonts.xml得到的結構體,fNames保存文件名稱信息,fFileNames保存相應的文件名。
struct FontInitRec {
    constchar*         fFileName;
    const char*const*  fNames;     // null-terminated list
};
        FontFamily轉換後的結構體,同一個fontfamily第一個ttf文件保存所有的名稱,其餘的fNames爲NULL。

struct FamilyRec {
    FamilyRec*  fNext;
    SkTypeface* fFaces[4];
    FamilyRec()
    {
        fNext = gFamilyHead;
        memset(fFaces, 0,sizeof(fFaces));
        gFamilyHead = this;
    }
};
        保存同一個font family的節點,每個font family分配了四個face,分別對應爲normal,bold,italic,bolditalic。
struct NameFamilyPair {
    const char* fName;      // we own this
    FamilyRec*  fFamily;   // we don't own this, we just reference it
    void construct(const charname[], FamilyRec* family) {
        fName = strdup(name);
        fFamily = family;   // we don't own this, so just record thereferene
    }
    void destruct() {
        free((char*)fName);
        // we don't own family, sojust ignore our reference
    }
};
        記錄name跟font family的對應關係。
        重要方法流程:
        SkFontHost::CreateTypeface(constSkTypeface* familyFace, constchar familyName[], SkTypeface::Style style)流程:
        (1) 調用init_system_fonts初始化系統字體
        (2)在當前全局列表中查找最接近的typeface並返回
        init_system_fonts流程:
        (1)調用load_font_info初始化相關變量
        (2)調用get_name_and_style獲取字體的屬性,name style
        (3)通過這些屬性創建FileTypeface,並把這些字體信息保存到全局列表中
        (4)將family 及 name信息添加進NameFamilyPairList中
        其中步驟2 3 4是循環執行,直到所有的信息都加入進去。
        load_font_info流程:
        (1)調用getFontFamilies(fontFamilies);解析/system/etc/system_fonts.xml、/system/etc/fallback_fonts.xml、/vendor/etc/fallback_fonts.xml文件,並把相應信息保存在fontFamilies。fallback_fonts.xml是當相應的字庫找不到時,會繼續找的字體,vendor一般爲第三方廠商提供。fontFamilies保存了ttf文件的文件名字及名稱。
        (2)將fontFamily結構轉換成FontInitRec結構,主要作用是:同一個fontfamily第一個出現的字體保存所有的名稱,後續字體的名稱均設置爲NULL,以標緻是同一個font family。
        (3)將轉換後的結果保存在gSystemFonts,並用gNumSystemFonts記錄當前系統字體個數。
        由此可知,通過名稱或者familyface來創建Typeface的API,一般用於系統字體的創建,因爲系統字體是一定會在列表中的,自定義字體則不一定會在列表中,主要看該字體之前是否被打開過,且沒有被刪除,而如果沒有在列表中,則會選一個跟所需要的字體比較接近的字體來返回。
        SkFontHost::CreateTypefaceFromFile(const char path[])流程:
        (1)調用SkMMAPStream函數將指定文件映射到內存空間,以進程共享讀的方式
        (2)調用SkFontHost::CreateTypefaceFromStream(stream);創建字體
        SkFontHost::CreateTypefaceFromStream(SkStream* stream)流程:
        (1)調用find_name_and_attributes獲取字體style
        (2)調用init_system_fonts初始化系統字體
        (3)調用StreamTypeface構造函數創建Typeface,並把信息保存到相應的鏈表中。
        這裏有一個疑問,本身SkMMAPStream是以進程共享的方式映射的,爲什麼在將相應信息保存到鏈表中的時候,不去查詢鏈表中是否已經存在該typeface,如果多次打開同一個文件,則會導致鏈表中同一typeface具有多個節點。
3.4同一個ttf包含多個fontfamily是否支持
        skia目前只提供了2.3.2中的5個API供應用程序調用,到skia的so庫中就統一成三個API,也就是上一節介紹的CreateTypeface、CreateTypefaceFromStream、CreateTypefaceFromFile,不管是哪個方法都首先會調用find_name_and_attributes獲取字體style及相關信息,之後再在這些信息的基礎上構建typeface相關變量,而find_name_and_attributes是通過FT_Open_Face(library, &args,0, &face)來構建font face,從而獲取font face的信息,注意第三個參數font_index是寫死爲0的,這也意味着skia在android上目前是不支持ttc文件及ttf文件裏包含多個style的用法的。這一點也可以在SkFontHost::GetFileName註釋中得到佐證(該API是通過fontId來獲取ttf文件名及該font在ttf裏面的Index):如果要支持該功能,則需擴展相應的結構體,獲取ttf內部的總的face num,並保存相應的偏移信息。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章