浅析Android字体加载原理

前言

   之前在处理系统字体问题的时候,可借鉴的资料很少,遇到了很多坑,不得不了解Android字体加载原理,现抽空写一篇总结,来加深自己对这块的理解。

内容

概述

   Android字体系统是由底层的Android 2D图形引擎Skia来实现的,Android3.0之后逐渐使用了新的硬件绘图模块hwui,在5.0之后正式取代了Skia,因此不同版本的系统其字体加载机制有些差异,按照Google的API Level来看,大体可以分为三个阶段:

  1. Android4.0以下的系统
  2. Android4.0到Android4.4的系统
  3. Android5.0以上的系统

   当然这每个阶段中,可能也存在些许小差异,但大方向是没变化的,本文主要对Android5.0以上的系统的字体加载机制进行描述,围绕系统字体配置文件解析与字体加载相关内容,不涉及系统运行库的实现细节。

注:浏览器及webView中的字体有单独的字体系统

   下面将从Java层面、Native层面、文件配置系统三个部分来阐述Android字体加载原理。

Java层面

   有研究过Android的人大概都有了解,Android的Java层封装了构建应用程序时可能会用到的各种Api。而在字体这部分,起主要作用的是android.graphics.Typeface,其主要负责字体加载以及对上层提供创建字体功能的调用,下面将着重分析该类的调用过程。

   首先,在Android启动的过程中,ZygoteInit类中的main()方法会调用加载方法preload(),对各种类、链接库、资源等进行初始化,具体代码如下:

public static void main(String argv[]) {    ...    registerZygoteSocket(socketName);    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ZygotePreload");    EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,        SystemClock.uptimeMillis());    //调用加载方法    preload();    EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,        SystemClock.uptimeMillis());    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);    ...}//主要用于加载并初始化各种类、链接库、资源等。static void preload() {    Log.d(TAG, "begin preload");    //Systrace开始tag    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "BeginIcuCachePinning");    //开始Icu缓存开销    beginIcuCachePinning();    //Systrace结束tag    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadClasses");    //预加载Classes    preloadClasses();    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadResources");    //预加载resources    preloadResources();    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadOpenGL");    //预加载openGL    preloadOpenGL();    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);    //加载分享库    preloadSharedLibraries();    //加载文本资源    preloadTextResources();    // Ask the WebViewFactory to do any initialization that must run in the zygote process,    // for memory sharing purposes.、    WebViewFactory.prepareWebViewInZygote();    endIcuCachePinning();    warmUpJcaProviders();    Log.d(TAG, "end preload");}

   其中preloadClasses()方法会加载并初始化一些系统常用的API类,这些类都是位于frameworks/base/preloaded-classes文件中,当然也包括Typeface类。

/** * Performs Zygote process initialization. Loads and initializes * commonly used classes. * * Most classes only cause a few hundred bytes to be allocated, but * a few will allocate a dozen Kbytes (in one case, 500+K). */private static void preloadClasses() {    ...        InputStream is;    try {        is = new FileInputStream(PRELOADED_CLASSES);    } catch (FileNotFoundException e) {        Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");        return;    }    ...    try {    BufferedReader br        = new BufferedReader(new InputStreamReader(is), 256);    int count = 0;    String line;    while ((line = br.readLine()) != null) {        // Skip comments and blank lines.        line = line.trim();        if (line.startsWith("#") || line.equals("")) {            continue;        }        Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadClass " + line);        try {            if (false) {                Log.v(TAG, "Preloading " + line + "...");            }            // Load and explicitly initialize the given class. Use            // Class.forName(String, boolean, ClassLoader) to avoid repeated stack lookups            // (to derive the caller's class-loader). Use true to force initialization, and            // null for the boot classpath class-loader (could as well cache the            // class-loader of this class in a variable).            Class.forName(line, true, null);            count++;    ...}

   从上面的代码可以看到,Android通过反射机制Class.forName(“android.graphics.Typeface”)加载了Typeface类,在加载的同时,会调用类中的static方法块。如下:

static {    //初始化系统字体    init();    // Set up defaults and typefaces exposed in public API    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 = new Typeface[] {        DEFAULT,        DEFAULT_BOLD,        create((String) null, Typeface.ITALIC),        create((String) null, Typeface.BOLD_ITALIC),    };}public static Typeface create(String familyName, int style) {    if (sSystemFontMap != null) {        return create(sSystemFontMap.get(familyName), style);    }    return null;}public static Typeface create(Typeface family, int style) {    ...    typeface = new Typeface(nativeCreateFromTypeface(ni, style));    ...    return typeface;}

   在上面的static方法块中,最终通过调用Native层方法nativeCreateFromTypeface(),来初始化系统字体并且设置默认的系统字体以及字体样式,可以从上面的方法看出系统默认创建sans-serif(无衬线字体),serif(衬线字体),monospace(等宽字体)三种字体,并且通过create第一个参数为null,来创建默认字体的四种style:normal,bold,italic,bolditalic。

注:这里需要注意的是,Android4.x版本的系统与Android5.0以上的版本所调用的API基本一致,但是native层确有很大的变,这是由于5.0以上的系统添加了一个新的方法init(),其主要实现了解析系统字体配置文件,并据此加载系统字体。而Android4.x版本是在native层实现的。

   因为现在Android阵营已经基本上都是5.0以上的系统了,所以5.0以下版本的加载不在解释。下面我们来看init()方法的具体逻辑:

/* * (non-Javadoc) * * This should only be called once, from the static class initializer block. */private static void init() {    // Load font config and initialize Minikin state    //获取系统字体配置文件位置放置于system/etc目录下    File systemFontConfigLocation = getSystemFontConfigLocation();    //获取配置文件fonts.xml    File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG);    //以下代码是对fonts.xml的解析,即是对系统字体的解析    try {        FileInputStream fontsIn = new FileInputStream(configFilename);        FontListParser.Config fontConfig = FontListParser.parse(fontsIn);        Map bufferForPath = new HashMap();        //用来承载fonts.xml中的每个family节点        List familyList = new ArrayList();        // Note that the default typeface is always present in the fallback list;        // this is an enhancement from pre-Minikin behavior.        //从每个family节点中解析字体样式,这里解析系统默认字体        for (int i = 0; i < fontConfig.families.size(); i++) {            FontListParser.Family f = fontConfig.families.get(i);            if (i == 0 || f.name == null) {                familyList.add(makeFamilyFromParsed(f, bufferForPath));            }        }        //系统默认字体集合        sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]);        //设置默认系统字体        setDefault(Typeface.createFromFamilies(sFallbackFonts));        //这里加载系统字体,包括默认字体        Map systemFonts = new HashMap();        for (int i = 0; i < fontConfig.families.size(); i++) {            Typeface typeface;            FontListParser.Family f = fontConfig.families.get(i);            if (f.name != null) {                if (i == 0) {                    // The first entry is the default typeface; no sense in                    // duplicating the corresponding FontFamily.                    typeface = sDefaultTypeface;                } else {                    //从每个family节点中解析字体                    FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath);                    FontFamily[] families = { fontFamily };                    typeface = Typeface.createFromFamiliesWithDefault(families);                }                //解析的字体添加到系统字体中                systemFonts.put(f.name, typeface);            }        }        //通过权重别号解析字体,别名必须与字体对应        for (FontListParser.Alias alias : fontConfig.aliases) {            Typeface base = systemFonts.get(alias.toName);            Typeface newFace = base;            int weight = alias.weight;            if (weight != 400) {                newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));            }            systemFonts.put(alias.name, newFace);        }        //系统字体集合        sSystemFontMap = systemFonts;    } catch (RuntimeException e) {        Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);        // TODO: normal in non-Minikin case, remove or make error when Minikin-only    } catch (FileNotFoundException e) {        Log.e(TAG, "Error opening " + configFilename, e);    } catch (IOException e) {        Log.e(TAG, "Error reading " + configFilename, e);    } catch (XmlPullParserException e) {        Log.e(TAG, "XML parse exception for " + configFilename, e);    }}

   通过以上代码,可以看出,系统解析过程中,一共有三种字体模式。一种的是系统默认字体;一种是系统字体,所有字体,包括自己添加的字体;一种是设置别名的字体,字体的衍生。而这三种字体都会在init()中被加载,而它们加载主要涉及以下方法。

//通过family节点解析FontFamilyprivate static FontFamily makeFamilyFromParsed(FontListParser.Family family,        Map bufferForPath) {    //这里的lang表示国家缩写,variant表示字体的排列格式一般有compact与elegant两种    FontFamily fontFamily = new FontFamily(family.lang, family.variant);    for (FontListParser.Font font : family.fonts) {        ByteBuffer fontBuffer = bufferForPath.get(font.fontName);        if (fontBuffer == null) {            try (FileInputStream file = new FileInputStream(font.fontName)) {                FileChannel fileChannel = file.getChannel();                long fontSize = fileChannel.size();                fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);                bufferForPath.put(font.fontName, fontBuffer);            } catch (IOException e) {                Log.e(TAG, "Error mapping font file " + font.fontName);                continue;            }        }        if (!fontFamily.addFontWeightStyle(fontBuffer, font.ttcIndex, font.axes,                font.weight, font.isItalic)) {            Log.e(TAG, "Error creating font " + font.fontName + "#" + font.ttcIndex);        }    }    return fontFamily;}/*以下是通过不同的格式解析出不同的family*/public FontFamily() {    mNativePtr = nCreateFamily(null, 0);    if (mNativePtr == 0) {        throw new IllegalStateException("error creating native FontFamily");    }}public FontFamily(String lang, String variant) {    int varEnum = 0;    if ("compact".equals(variant)) {        varEnum = 1;    } else if ("elegant".equals(variant)) {        varEnum = 2;    }    mNativePtr = nCreateFamily(lang, varEnum);    if (mNativePtr == 0) {        throw new IllegalStateException("error creating native FontFamily");    }}@Overrideprotected void finalize() throws Throwable {    try {        nUnrefFamily(mNativePtr);    } finally {        super.finalize();    }}public boolean addFont(String path, int ttcIndex) {    try (FileInputStream file = new FileInputStream(path)) {        FileChannel fileChannel = file.getChannel();        long fontSize = fileChannel.size();        ByteBuffer fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);        return nAddFont(mNativePtr, fontBuffer, ttcIndex);    } catch (IOException e) {        Log.e(TAG, "Error mapping font file " + path);        return false;    }}public boolean addFontWeightStyle(ByteBuffer font, int ttcIndex, List axes,        int weight, boolean style) {    return nAddFontWeightStyle(mNativePtr, font, ttcIndex, axes, weight, style);}public boolean addFontFromAsset(AssetManager mgr, String path) {    return nAddFontFromAsset(mNativePtr, mgr, path);}private static native long nCreateFamily(String lang, int variant);private static native void nUnrefFamily(long nativePtr);private static native boolean nAddFont(long nativeFamily, ByteBuffer font, int ttcIndex);private static native boolean nAddFontWeightStyle(long nativeFamily, ByteBuffer font,        int ttcIndex, List listOfAxis,        int weight, boolean isItalic);private static native boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr,        String path);
/** * Create a new typeface from an array of font families. * * @param families array of font families * @hide *///通过FontFamily解析创建字体public static Typeface createFromFamilies(FontFamily[] families) {    long[] ptrArray = new long[families.length];    for (int i = 0; i < families.length; i++) {        ptrArray[i] = families[i].mNativePtr;    }    return new Typeface(nativeCreateFromArray(ptrArray));}/** * Create a new typeface from an array of font families, including * also the font families in the fallback list. * * @param families array of font families * @hide *///通过FontFamily解析创建字体public static Typeface createFromFamiliesWithDefault(FontFamily[] families) {    long[] ptrArray = new long[families.length + sFallbackFonts.length];    for (int i = 0; i < families.length; i++) {        ptrArray[i] = families[i].mNativePtr;    }    for (int i = 0; i < sFallbackFonts.length; i++) {        ptrArray[i + families.length] = sFallbackFonts[i].mNativePtr;    }    return new Typeface(nativeCreateFromArray(ptrArray));}

   从上面的代码可看到,系统通过解析/system/etc/fonts.xml(字体配置文件),然后接收Native层方法回调上来的值,来创建指定的Typeface即字体,保存在sSystemFontMap中。而相关native方法列表以及注册(在frameworks/base/core/jni/android/graphics/Typeface.cpp中注册)如下:

private static native long nativeCreateFromTypeface(long native_instance, int style);private static native long nativeCreateWeightAlias(long native_instance, int weight);private static native void nativeUnref(long native_instance);private static native int  nativeGetStyle(long native_instance);private static native long nativeCreateFromArray(long[] familyArray);private static native void nativeSetDefault(long native_instance);///////////////////////////////////////////////////////////////////////////////static const JNINativeMethod gTypefaceMethods[] = {    { "nativeCreateFromTypeface", "(JI)J", (void*)Typeface_createFromTypeface },    { "nativeCreateWeightAlias",  "(JI)J", (void*)Typeface_createWeightAlias },    { "nativeUnref",              "(J)V",  (void*)Typeface_unref },    { "nativeGetStyle",           "(J)I",  (void*)Typeface_getStyle },    { "nativeCreateFromArray",    "([J)J",                                           (void*)Typeface_createFromArray },    { "nativeSetDefault",         "(J)V",   (void*)Typeface_setDefault },};int register_android_graphics_Typeface(JNIEnv* env){    return RegisterMethodsOrDie(env, "android/graphics/Typeface", gTypefaceMethods,                                NELEM(gTypefaceMethods));}

   最终,通过这一层的关系,调用到Native层的方法。

   到此,字体加载Java层面就结束了,下面将调用Native层的方法。

Native层面

   Native层主要是skia图形引擎的Android移植版,项目源码位于external\skia目录下。

   在Android4.X版本中主要是用skia来进行软件绘制,所以解析配置文件并加载字体是在skia中完成,这里不在描述过程,可以参看相关博客中的描述。而由于绘制性能等问题,Android5.0之后使用了新的硬件绘图模块hwui,hwui主要则是使用opengles来进行gpu硬件绘图,提升整个系统的绘制性能。

   在上述Java层调用过程后,字体加载指向了Native层。在Native层调用首先进入jni/android/graphics/Typeface.cpp,调用对应的方法,然后进入hwui/Typeface.h和hwui/Typeface.cpp中定制的函数,从而解析配置文件并加载字体。

//jni/android/graphics/Typeface.cpp#include "jni.h"#include "core_jni_helpers.h"#include "GraphicsJNI.h"#include "ScopedPrimitiveArray.h"#include "SkTypeface.h"#include #include #include using namespace android;static jlong Typeface_createFromTypeface(JNIEnv* env, jobject, jlong familyHandle, jint style) {    Typeface* family = reinterpret_cast(familyHandle);    Typeface* face = Typeface::createFromTypeface(family, (SkTypeface::Style)style);    // TODO: the following logic shouldn't be necessary, the above should always succeed.    // Try to find the closest matching font, using the standard heuristic    if (NULL == face) {        face = Typeface::createFromTypeface(family, (SkTypeface::Style)(style ^ SkTypeface::kItalic));    }    for (int i = 0; NULL == face && i < 4; i++) {        face = Typeface::createFromTypeface(family, (SkTypeface::Style)i);    }    return reinterpret_cast(face);}static jlong Typeface_createWeightAlias(JNIEnv* env, jobject, jlong familyHandle, jint weight) {    Typeface* family = reinterpret_cast(familyHandle);    Typeface* face = Typeface::createWeightAlias(family, weight);    return reinterpret_cast(face);}static void Typeface_unref(JNIEnv* env, jobject obj, jlong faceHandle) {    Typeface* face = reinterpret_cast(faceHandle);    if (face != NULL) {        face->unref();    }}static jint Typeface_getStyle(JNIEnv* env, jobject obj, jlong faceHandle) {    Typeface* face = reinterpret_cast(faceHandle);    return face->fSkiaStyle;}static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray) {    ScopedLongArrayRO families(env, familyArray);    std::vector familyVec;    for (size_t i = 0; i < families.size(); i++) {        FontFamily* family = reinterpret_cast(families[i]);        familyVec.push_back(family);    }    return reinterpret_cast(Typeface::createFromFamilies(familyVec));}static void Typeface_setDefault(JNIEnv *env, jobject, jlong faceHandle) {    Typeface* face = reinterpret_cast(faceHandle);    return Typeface::setDefault(face);}
//hwui/Typeface.h#ifndef _ANDROID_GRAPHICS_TYPEFACE_IMPL_H_#define _ANDROID_GRAPHICS_TYPEFACE_IMPL_H_#include "SkTypeface.h"#include #include #include namespace android {struct ANDROID_API Typeface {    FontCollection *fFontCollection;    // style used for constructing and querying Typeface objects    SkTypeface::Style fSkiaStyle;    // base weight in CSS-style units, 100..900    int fBaseWeight;    // resolved style actually used for rendering    FontStyle fStyle;    void unref();    static Typeface* resolveDefault(Typeface* src);    static Typeface* createFromTypeface(Typeface* src, SkTypeface::Style style);    static Typeface* createWeightAlias(Typeface* src, int baseweight);    static Typeface* createFromFamilies(const std::vector& families);    static void setDefault(Typeface* face);};}#endif  // _ANDROID_GRAPHICS_TYPEFACE_IMPL_H_

   Native层的c/c++方法调用比较复杂,通过一系列的调用,返回值给Java层,这里就不在阐述,有兴趣的人可以自己下个源码深入理解下,到这里Android的字体加载原理基本完成了,不得不感叹Google工程师的丰功伟绩。

文件配置系统

   前面介绍的是加载的原理,现在简单的描述下字体加载过程中所用到的字体加载文件。

   在4.x版本的系统字体配置文件位于system/etc/system_fonts.xml,备用字体配置文件位于system/etc/fallback_fonts.xml和vendor/etc/fallback_fonts.xml。而5.0以上的版本的系统字体及备用字体配置均位于system/etc/fonts.xml文件中,下面展示部分fonts.xml内容。

<familyset version="22">        <family name="sans-serif">        <font weight="100" style="normal">Roboto-Thin.ttffont>        <font weight="100" style="italic">Roboto-ThinItalic.ttffont>        <font weight="300" style="normal">Roboto-Light.ttffont>        <font weight="300" style="italic">Roboto-LightItalic.ttffont>        <font weight="400" style="normal">Roboto-Regular.ttffont>        <font weight="400" style="italic">Roboto-Italic.ttffont>        <font weight="500" style="normal">Roboto-Medium.ttffont>        <font weight="500" style="italic">Roboto-MediumItalic.ttffont>        <font weight="900" style="normal">Roboto-Black.ttffont>        <font weight="900" style="italic">Roboto-BlackItalic.ttffont>        <font weight="700" style="normal">Roboto-Bold.ttffont>        <font weight="700" style="italic">Roboto-BoldItalic.ttffont>    family>        <alias name="sans-serif-thin" to="sans-serif" weight="100" />    <alias name="sans-serif-light" to="sans-serif" weight="300" />    <alias name="sans-serif-medium" to="sans-serif" weight="500" />    <alias name="sans-serif-black" to="sans-serif" weight="900" />    <alias name="arial" to="sans-serif" />    <alias name="helvetica" to="sans-serif" />    <alias name="tahoma" to="sans-serif" />    <alias name="verdana" to="sans-serif" />...        <family lang="und-Arab" variant="elegant">        <font weight="400" style="normal">NotoNaskhArabic-Regular.ttffont>        <font weight="700" style="normal">NotoNaskhArabic-Bold.ttffont>    family>    <family lang="und-Arab" variant="compact">        <font weight="400" style="normal">NotoNaskhArabicUI-Regular.ttffont>        <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttffont>    family>    <family lang="und-Ethi">        <font weight="400" style="normal">NotoSansEthiopic-Regular.ttffont>        <font weight="700" style="normal">NotoSansEthiopic-Bold.ttffont>    family>        <family lang="zh-Hans">        <font weight="400" style="normal">NotoSansSC-Regular.otffont>    family>        <family lang="zh-Hant">        <font weight="400" style="normal">NotoSansTC-Regular.otffont>    family>

   如上所示,第一个family节点为系统默认字体。nameset节点的各个name子节点定义可用的字体名称,fileset节点的file子节点分别对应normal、bold、italic、bold-italic四种字体样式,如果file节点个数少于四个,相应字体样式会对应已有兄弟file节点的字体文件。family属性中lang代表国家的缩写,系统在切换语言的时候会从加载的字体中匹配国家的缩写,从而调出对于的系统字体、variant属性指的是字体的排列格式通常有compact(紧凑型)以及(简洁型)。

   fallback_fonts配置了系统备用字体。只有在系统内置字体中找不到相应字符时,才会到备用字体中去寻找,family节点的顺序对应搜索顺序,搜索匹配规则采用BCP47的定义。按照这个规则,如下图,系统语言为非缅甸状态下,当系统配置文件如上方所示时,系统会默认加载最上方的字体,即缅甸官方字体;当系统配置文件如下方所示时,系统会默认加载民间字体,这也就是为什么,修改配置后,其他语言下缅文乱码可以得以解决,而正如上面所说,系统在切换语言的时候会从加载的字体中匹配国家的缩写,即国际化适配,所以缅文状态下,一直没有乱码问题的存在。

浅析Android字体加载原理_第1张图片

   5.0以后的字体配置文件与之前版本的相比,最大的一个改进是将之前字体样式中的单一bold样式改为各种不同过的weight,这样可以更加细粒度的控制字重。

总结

   通过以上的加载流程,我们可以用以下流程图来总结一种字体的加载过程。

浅析Android字体加载原理_第2张图片

为系统添加新的字体

   现在的手机产商都对Android系统进行了定制,当然也会加上属于自己的字体,下面简单描述下添加新字体的流程,以缅甸字体为例。

   1.在frameworks/base/data/fonts/fonts.xml中添加字体节点

<family lang="my">    <font weight="400" style="normal">ZawgyiOne.ttffont>family>

   2.在frameworks/base/data/fonts/fonts.mk的最后加入新加的字体文件

PRODUCT_COPY_FILES := \    frameworks/base/data/fonts/fonts.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/fonts.xmlPRODUCT_PACKAGES := \    DroidSansFallback.ttf \    DroidSansMono.ttf \    AndroidClock.ttf \    DINPro-Black.otf \    DINPro-Bold.otf \    DINPro-Light.otf \    DINPro-Medium.otf \    DINPro-Regular.otf \    Flyme-Light.ttf \    ZawgyiOne.ttf

   3.在frameworks/base/data/fonts/Android.mk的font_src_files最后加入新加的字体文件

font_src_files := \    AndroidClock.ttf \    Flyme-Light.ttf \    ZawgyiOne.ttf

   4.将下载的字体放入frameworks/base/data/fonts下

   其中第2、第3步是为了让字体能够编译进入系统中。

参考博客

   knight

   flyeek

更多相关文章

  1. Android 是Google开发的基于Linux平台的开源手机操作系统
  2. 爬架提升控制系统
  3. android 加载图片的三种方式
  4. Android 源码 图形系统之硬件渲染器绘制

随机推荐

  1. android用户界面之菜单(Menu)教程实例汇
  2. android adb和串口调试.
  3. Android设备的识别
  4. Android(安卓)个人手机通讯录开发
  5. 从零开始学习Android一
  6. 升级Android(安卓)ADT 和SDK
  7. This app has been built with an incorr
  8. Android(安卓)Listview 隐藏滚动条
  9. 开发技术前线 第七期 周报
  10. android利用handler线程间的通信