『壹』 android 下拉滾動頁面怎麼實現
以下是我自己花功夫編寫了一種非常簡單的下拉刷新實現方案,現在拿出來和大家分享一下。相信在閱讀完本篇文章之後,大家都可以在自己的項目中一分鍾引入下拉刷新功能 最近項目中需要用到ListView下拉刷新的功能,一開始想圖省事,在網上直接找一個現成的,可是嘗試了網上多個版本的下拉刷新之後發現效果都不 怎麼理想。有些是因為功能不完整或有Bug,有些是因為使用起來太復雜,十全十美的還真沒找到。因此我也是放棄了在網上找現成代碼的想法,自己花功夫編寫 了一種非常簡單的下拉刷新實現方案,現在拿出來和大家分享一下。相信在閱讀完本篇文章之後,大家都可以在自己的項目中一分鍾引入下拉刷新功能。 首先講一下實現原理。這里我們將採取的方案是使用組合View的方式,先自定義一個布局繼承自LinearLayout,然後在這個布局中加入下拉 頭和ListView這兩個子元素,並讓這兩個子元素縱向排列。初始化的時候,讓下拉頭向上偏移出屏幕,這樣我們看到的就只有ListView了。然後對 ListView的touch事件進行監聽,如果當前ListView已經滾動到頂部並且手指還在向下拉的話,那就將下拉頭顯示出來,鬆手後進行刷新操 作,並將下拉頭隱藏。原理示意圖如下: 那我們現在就來動手實現一下,新建一個項目起名叫PullToRefreshTest,先在項目中定義一個下拉頭的布局文件pull_to_refresh/apk/res/android" xmlns:tools="schemas/tools" android:id="@+id/pull_to_refresh_head" android:layout_width="fill_parent" android:layout_height="60dip" > <LinearLayout android:layout_width="200dip" android:layout_height="60dip" android:layout_centerInParent="true" android:orientation="horizontal" > <RelativeLayout android:layout_width="0dip" android:layout_height="60dip" android:layout_weight="3" > <ImageView android:id="@+id/arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="@drawable/arrow" /> <ProgressBar android:id="@+id/progress_bar" android:layout_width="30dip" android:layout_height="30dip" android:layout_centerInParent="true" android:visibility="gone" /> </RelativeLayout> <LinearLayout android:layout_width="0dip" android:layout_height="60dip" android:layout_weight="12" android:orientation="vertical" > <TextView android:id="@+id/description" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:gravity="center_horizontalbottom" android:text="@string/pull_to_refresh" /> <TextView android:id="@+id/updated_at" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:gravity="center_horizontaltop" android:text="@string/updated_at" /> </LinearLayout> </LinearLayout> </RelativeLayout> 在這個布局中,我們包含了一個下拉指示箭頭,一個下拉狀態文字提示,和一個上次更新的時間。當然,還有一個隱藏的旋轉進度條,只有正在刷新的時候我們才會將它顯示出來。 布局中所有引用的字元串我們都放在stringsmit(); new HideHeaderTask()/apk/res/android" xmlns:tools="schemas/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <com.example.pulltorefreshtest.RefreshableView android:id="@+id/refreshable_view" android:layout_width="fill_parent" android:layout_height="fill_parent" > <ListView android:id="@+id/list_view" android:layout_width="fill_parent" android:layout_height="fill_parent" > </ListView> </com.example.pulltorefreshtest.RefreshableView> </RelativeLayout> 可以看到,我們在自定義的RefreshableView中加入了一個ListView,這就意味著給這個ListView加入了下拉刷新的功能,就是這么簡單! 然後我們再來看一下程序的主Activity,打開或新建MainActivity,加入如下代碼: 復制代碼 代碼如下: public class MainActivity extends Activity { RefreshableView refreshableView; ListView listView; ArrayAdapter<String> adapter; String[] items = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); refreshableView = (RefreshableView) findViewById(R.id.refreshable_view); listView = (ListView) findViewById(R.id.list_view); adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items); listView.setAdapter(adapter); refreshableView.setOnRefreshListener(new PullToRefreshListener() { @Override public void onRefresh() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } refreshableView.finishRefreshing(); } }, 0); } } 可 以看到,我們通過調用RefreshableView的setOnRefreshListener方法注冊了一個監聽器,當ListView正在刷新時就 會回調監聽器的onRefresh方法,刷新的具體邏輯就在這里處理。而且這個方法已經自動開啟了線程,可以直接在onRefresh方法中進行耗時操 作,比如向伺服器請求最新數據等,在這里我就簡單讓線程睡眠3秒鍾。另外在onRefresh方法的最後,一定要調用RefreshableView中的 finishRefreshing方法,這個方法是用來通知RefreshableView刷新結束了,不然我們的ListView將一直處於正在刷新的 狀態。 不知道大家有沒有注意到,setOnRefreshListener這個方法其實是有兩個參數的,我們剛剛也是傳入了一個不起眼的 0。那這第二個參數是用來做什麼的呢?由於RefreshableView比較智能,它會自動幫我們記錄上次刷新完成的時間,然後下拉的時候會在下拉頭中 顯示距上次刷新已過了多久。這是一個非常好用的功能,讓我們不用再自己手動去記錄和計算時間了,但是卻存在一個問題。如果當前我們的項目中有三個地方都使 用到了下拉刷新的功能,現在在一處進行了刷新,其它兩處的時間也都會跟著改變!因為刷新完成的時間是記錄在配置文件中的,由於在一處刷新更改了配置文件, 導致在其它兩處讀取到的配置文件時間已經是更改過的了。那解決方案是什麼?就是每個用到下拉刷新的地方,給setOnRefreshListener方法 的第二個參數中傳入不同的id就行了。這樣各處的上次刷新完成時間都是單獨記錄的,相互之間就不會再有影響。 好了,全部的代碼都在這里了,讓我們來運行一下,看看效果吧。 效果看起來還是非常不錯的。我們最後再來總結一下,在項目中引入ListView下拉刷新功能只需三步: 1. 在Activity的布局文件中加入自定義的RefreshableView,並讓ListView包含在其中。 2. 在Activity中調用RefreshableView的setOnRefreshListener方法注冊回調介面。 3. 在onRefresh方法的最後,記得調用RefreshableView的finishRefreshing方法,通知刷新結束。 從此以後,在項目的任何地方,一分鍾引入下拉刷新功能妥妥的。 好了,今天的講解到此結束,有疑問的朋友請在下面留言。 源碼下載,請點擊這里
『貳』 android平台框架原理
Android的系統架構採用了分層架構的思想,如圖1所示。從上層到底層共包括四層,分別是應用程序程序層、應用框架層、系統庫和Android運行時和Linux內核。
每層功能簡要介紹如下:
該層提供一些核心應用程序包,例如電子郵件、簡訊、日歷、地圖、瀏覽器和聯系人管理等。同時,開發者可以利用java語言設計和編寫屬於自己的應用程序,而這些程序與那些核心應用程序彼此平等、友好共處。
該層是Android應用開發的基礎,開發人員大部分情況是在和她打交道。應用程序框架層包括活動管理器、窗口管理器、內容提供者、視圖系統、包管理器、電話管理器、資源管理器、位置管理器、通知管理器和XMPP服務十個部分。在Android平台上,開發人員可以完全訪問核心應用程序所使用的API框架。並且,任何一個應用程序都可以發布自身的功能模塊,而其他應用程序則可以使用這些已發布的功能模塊。基於這樣的重用機制,用戶就可以方便地替換平台本身的各種應用程序組件。
系統庫包括九個子系統,分別是圖層管理、媒體庫、SQLite、OpenGLEState、FreeType、WebKit、SGL、SSL和libc。
包括核心庫和Dalvik虛擬機。
既兼容了大多數Java語言所需要調用的功能函數,又包括了Android的核心庫,比如android.os、android.net、android.media等等。
Dalvik虛擬機是一種基於寄存器的java虛擬機,所支持的位元組碼(ByteCode)是「dex」文件(Dalvik Executable)
Dalvik虛擬機主要是完成對生命周期的管理、堆棧的管理、線程的管理、安全和異常的管理以及垃圾回收等重要功能。
核心系統服務依賴於Linux內核,如安全性、內存管理、進程管理、網路協議棧和驅動模型。Linux內核也是作為硬體與軟體棧的抽象層。驅動:顯示驅動、攝像頭驅動、鍵盤驅動、WiFi驅動、Audio驅動、flash內存驅動、Binder(IPC)驅動、電源管理等。
Android的系統架構採用分層架構的思想,架構清晰,層次分明,協同工作。
Android的系統架構不僅從宏觀上認識了Android系統,同時,也給我們的學習與實踐指明了方向。
找准切入點,對我們的學習和工作,無疑是有非常大的幫助的。
每個開發者估計都糾結過 平台 和 框架 的概念,特別是對新手而言, 平台 和 框架 似乎總是前輩們口頭上慣用的、玄而又玄的名詞。
實際上,我們可以把 平台 理解為舞台,其強調了事物的支持特性,有如舞台具有支撐舞者在其上進行表演的特性。
同樣,Android 平台 具有支持Android應用程序運行的特性,具體表現在運行時(Runtime)環境和介面,API。
框架 可以理解為骨架,其強調了事物的可重用性。眾所周知,人類無論高矮胖瘦、美醜強弱,其骨架都是相似的。反之,使用一個人類的骨架模型,可以塑造出不同的人體模型。
在軟體開發過程中,使用 框架 可以開發出界面各異的、某一類應用程序。例如,輸入法,有搜狗輸入法、國筆輸入法、網路輸入法……等各有異同的應用程序。
框架 的具體表現為一組協同工作的類,如界面組件類、事件處理類、網路通信類等。藉助 框架 ,開發者可以高效地開發出應用程序。
簡而言之, 框架 幫助應用程序的開發, 平台 支持應用程序的運行, 框架 建立在 平台 之上。
首先,理解兩個概念 抽象和衍生
框架里的函數能夠呼叫應用程序之中的函數,通俗的講是前輩呼叫晚輩,框架先於程序誕生,稱之為前輩。程序在框架的基礎上誕生,所以稱為晚輩。前輩呼叫晚輩,會產生下述幾種效果
應用框架的典型雙向溝通情形
從上圖可以看到,框架和應用程序之間,主動權掌握在框架手裡,框架決定如何呼叫應用程序中的函數。
『叄』 Android UI繪制之View繪制的工作原理
這是AndroidUI繪制流程分析的第二篇文章,主要分析界面中View是如何繪制到界面上的具體過程。
ViewRoot 對應於 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View的三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢後,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 建立關聯。
measure 過程決定了 View 的寬/高, Measure 完成以後,可以通過 getMeasuredWidth 和 getMeasuredHeight 方法來獲取 View 測量後的寬/高,在幾乎所有的情況下,它等同於View的最終的寬/高,但是特殊情況除外。 Layout 過程決定了 View 的四個頂點的坐標和實際的寬/高,完成以後,可以通過 getTop、getBottom、getLeft 和 getRight 來拿到View的四個頂點的位置,可以通過 getWidth 和 getHeight 方法拿到View的最終寬/高。 Draw 過程決定了 View 的顯示,只有 draw 方法完成後 View 的內容才能呈現在屏幕上。
DecorView 作為頂級 View ,一般情況下,它內部會包含一個豎直方向的 LinearLayout ,在這個 LinearLayout 裡面有上下兩個部分,上面是標題欄,下面是內容欄。在Activity中,我們通過 setContentView 所設置的布局文件其實就是被加到內容欄中的,而內容欄id為 content 。可以通過下面方法得到 content:ViewGroup content = findViewById(R.android.id.content) 。通過 content.getChildAt(0) 可以得到設置的 view 。 DecorView 其實是一個 FrameLayout , View 層的事件都先經過 DecorView ,然後才傳遞給我們的 View 。
MeasureSpec 代表一個32位的int值,高2位代表 SpecMode ,低30位代表 SpecSize , SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。
SpecMode 有三類,如下所示:
UNSPECIFIED
EXACTLY
AT_MOST
LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬/高。
對於頂級View,即DecorView和普通View來說,MeasureSpec的轉換過程略有不同。對於DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定;
對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同決定;
MeasureSpec一旦確定,onMeasure就可以確定View的測量寬/高。
小結一下
當子 View 的寬高採用 wrap_content 時,不管父容器的模式是精確模式還是最大模式,子 View 的模式總是最大模式+父容器的剩餘空間。
View 的工作流程主要是指 measure 、 layout 、 draw 三大流程,即測量、布局、繪制。其中 measure 確定 View 的測量寬/高, layout 確定 view 的最終寬/高和四個頂點的位置,而 draw 則將 View 繪制在屏幕上。
measure 過程要分情況,如果只是一個原始的 view ,則通過 measure 方法就完成了其測量過程,如果是一個 ViewGroup ,除了完成自己的測量過程外,還會遍歷調用所有子元素的 measure 方法,各個子元素再遞歸去執行這個流程。
如果是一個原始的 View,那麼通過 measure 方法就完成了測量過程,在 measure 方法中會去調用 View 的 onMeasure 方法,View 類裡面定義了 onMeasure 方法的默認實現:
先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源碼:
可以看到, getMinimumWidth 方法獲取的是 Drawable 的原始寬度。如果存在原始寬度(即滿足 intrinsicWidth > 0),那麼直接返回原始寬度即可;如果不存在原始寬度(即不滿足 intrinsicWidth > 0),那麼就返回 0。
接著看最重要的 getDefaultSize 方法:
如果 specMode 為 MeasureSpec.UNSPECIFIED 即未指定模式,那麼返回由方法參數傳遞過來的尺寸作為 View 的測量寬度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精確模式,那麼返回從 measureSpec 中取出的 specSize 作為 View 測量後的寬度和高度。
看一下剛才的表格:
當 specMode 為 EXACTLY 或者 AT_MOST 時,View 的布局參數為 wrap_content 或者 match_parent 時,給 View 的 specSize 都是 parentSize 。這會比建議的最小寬高要大。這是不符合我們的預期的。因為我們給 View 設置 wrap_content 是希望View的大小剛好可以包裹它的內容。
因此:
如果是一個 ViewGroup,除了完成自己的 measure 過程以外,還會遍歷去調用所有子元素的 measure 方法,各個子元素再遞歸去執行 measure 過程。
ViewGroup 並沒有重寫 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 這幾個方法專門用於測量子元素。
如果是 View 的話,那麼在它的 layout 方法中就確定了自身的位置(具體來說是通過 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft , mRight , mTop , mBottom 這四個值), layout 過程就結束了。
如果是 ViewGroup 的話,那麼在它的 layout 方法中只是確定了 ViewGroup 自身的位置,要確定子元素的位置,就需要重寫 onLayout 方法;在 onLayout 方法中,會調用子元素的 layout 方法,子元素在它的 layout 方法中確定自己的位置,這樣一層一層地傳遞下去完成整個 View 樹的 layout 過程。
layout 方法的作用是確定 View 本身的位置,即設定 View 的四個頂點的位置,這樣就確定了 View 在父容器中的位置;
onLayout 方法的作用是父容器確定子元素的位置,這個方法在 View 中是空實現,因為 View 沒有子元素了,在 ViewGroup 中則進行抽象化,它的子類必須實現這個方法。
1.繪制背景( background.draw(canvas); );
2.繪制自己( onDraw );
3.繪制 children( dispatchDraw(canvas) );
4.繪制裝飾( onDrawScrollBars )。
dispatchDraw 方法的調用是在 onDraw 方法之後,也就是說,總是先繪制自己再繪制子 View 。
對於 View 類來說, dispatchDraw 方法是空實現的,對於 ViewGroup 類來說, dispatchDraw 方法是有具體實現的。
通過 dispatchDraw 來傳遞的。 dispatchDraw 會遍歷調用子元素的 draw 方法,如此 draw 事件就一層一層傳遞了下去。dispatchDraw 在 View 類中是空實現的,在 ViewGroup 類中是真正實現的。
如果一個 View 不需要繪制任何內容,那麼就設置這個標記為 true,系統會進行進一步的優化。
當創建的自定義控制項繼承於 ViewGroup 並且不具備繪制功能時,就可以開啟這個標記,便於系統進行後續的優化;當明確知道一個 ViewGroup 需要通過 onDraw 繪制內容時,需要關閉這個標記。
參考:《Android開發藝術探索》
『肆』 安卓運行機制是什麼 安卓手機的工作原理是什麼
android基於Linux內核,很多系統也都基於Linux內核。但是android的特別之處除了開發上的特點以外,還有一個就是程序在運行時的行為和以往我接觸到的程序運行機制有很大不同。在傳統PC機或者其他一些手機上,用戶對應用程序有絕對的掌控權,在應用程序的系統菜單上選擇「退出」或者「關閉」之類的選項會直接殺死進程,而在android系統中不是這樣的。在android中,應用程序的生命周期並不是由應用程序自身直接控制的,而是由系統,當系統需要釋放內存來運行新進程或者保證某些後台進程和前端進程順利執行的時候才會釋放相應應用程序的資源,這個釋放過程有一個重要性的層次。
android中進程的層次如下(重要性由高到低):
1、前端進程。顧名思義,前端進程就是目前顯示在屏幕上和用戶交互的進程,在系統中前端進程數量很少,而這種進程是對用戶體驗的影響最大,只有系統的內存稀少到不足以維持和用戶的基本交互時才會銷毀前端進程。因此這種進程重要性是最高的。
2、可見進程。可見進程也擁有一個可視化的界面,只是目前不是最上層界面(最上層界面在前端進程裡面),可見進程一般調用了OnPause(),可見進程比前端進程重要性低,但是在交互方面影響還是很大,因為用戶可能隨時切換過去,所以系統不會輕易銷毀它。
3、服務進程。一個服務進程就是一個Service,它調用了startService,就是UNIX中說的守護進程,對用戶不可見,但是保證了一些重要的事件被監聽或者維持著某些狀態,比如網路數據傳輸、後台音樂播放,這類進程在內存不足且為了保證前端交互的順利進行的時候被銷毀。
4、後台進程。這里叫後台進程可能會和一般意義上的後台進程混淆,要說明的是,android里的後台進程是調用了OnStop()的,可以理解成用戶暫時沒有和這個進程交互的願望,所以這里後台進程有點「待銷毀」的意思。
5、空進程。這是一種系統緩存機制,其實就是個進程的外殼,當有新進程創建的時候,這個空進程可以加快進程創建速度,當系統內存不足的時候,首先銷毀空進程。
android中進程重要性層次
『伍』 Android N 四大組件的工作原理
本文側重講解android N 系統中四大組件的工作原理,不同系統原理略有差別。通過分析四大組件的工作流程加深對Android Framework的理解,也為插件化開發打下基礎。
Activity
展示一個界面並和用戶交互,它扮演的是一個前台界面的角色。
Service
計算型組件,用於後台執行一系列計算任務,工作在主線程,耗時操作需要另起線程, 分為啟動狀態和綁定狀態。
BroadcastReceiver
消息型組件,主要用於不同組件或者不同應用之間的消息傳遞,它工作在系統內部,不適合執行耗時操作,操作超過5s,會出現ANR。
ContentProvider
數據共享型組件,用於向其他組件或者應用共享數據,主要執行CURD操作。
我們啟動一個activity有兩種方法,
第一種(Activity直接啟動方式):
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
第二種(Context啟動方式)
Intent intent = new Intent(this, MainActivity.class);
getApplicationContext().startActivity(intent);
不同的啟動方式Activity的工作流程有點差別。
兩種啟動都會調用到Instrumentation類中的execStartActivity的方法,系統最終是通過ActivityThread中的performLaunchActivity完成Activity的創建和啟動。
performLaunchActivity方法主要完成以下工作:
1、通過ActivityClientRecord對象獲取啟動activity的組件信息
2、通過mInstrumentation對象的newActivity方法調用classloader完成activity的創建
3、通過r.packageInfo(LoadedApk 對象)的makeApplication方法嘗試創建Application對象
4、創建ContextImpl對象並調用Activity的attach方法完成一些數據的初始化
5、調用Activity的onCreate方法
在Activity啟動的過程中,App進程會頻繁地與AMS進程進行通信:
App進程會委託AMS進程完成Activity生命周期的管理以及任務棧的管理;這個通信過程AMS是Server端,App進程通過持有AMS的client代理IActivityManager完成通信過程;
AMS進程完成生命周期管理以及任務棧管理後,會把控制權交給App進程,讓App進程完成Activity類對象的創建,以及生命周期回調;這個通信過程也是通過Binder完成的,App所在server端的Binder對象存在於ActivityThread的內部類ApplicationThread;AMS所在client通過持有IApplicationThread的代理對象完成對於App進程的通信。
Service有兩種啟動方式,startService()和bindService(),兩種狀態可以並存:
startService流程
bindService流程
BroadcastReceiver的工作過程主要包括廣播的注冊、發送和接收:
動態注冊過程:
發送過程
靜態注冊是由PackageManagerService(PMS)在應用安裝的時候完成整個注冊過程的,除廣播以外,其他三大組件也都是在應用安裝時由PMS解析並注冊的。
每個進程的入口都是ActivityThead.main(),App的啟動流程如下:
從源碼中可以看出:
應用啟動的入口為ActivityThread的main方法,main方法會創建ActivityThread實例並創建主線程消息隊列。
attach方法中遠程調用AMS的attachApplication方法,並提供ApplicationThread用於和AMS的通信。
attachApplication方法會通過bindApplication方法和H來調回ActivityThread的handleBindApplication,這個方法會先創建Application,再載入ContentProvider,然後才會回調Application的onCreate方法。
由上圖可以看出,在ContentProvider的啟動過程中伴隨著app進程的啟動。
ContentProvider的其他CURD操作如insert,delete,update跟query的流程類似。
『陸』 Android圖形渲染原理上
對於Android開發者來說,我們或多或少有了解過Android圖像顯示的知識點,剛剛學習Android開發的人會知道,在Actvity的onCreate方法中設置我們的View後,再經過onMeasure,onLayout,onDraw的流程,界面就顯示出來了;對Android比較熟悉的開發者會知道,onDraw流程分為軟體繪制和硬體繪制兩種模式,軟繪是通過調用Skia來操作,硬繪是通過調用Opengl ES來操作;對Android非常熟悉的開發者會知道繪制出來的圖形數據最終都通過GraphiBuffer內共享內存傳遞給SurfaceFlinger去做圖層混合,圖層混合完成後將圖形數據送到幀緩沖區,於是,圖形就在我們的屏幕顯示出來了。
但我們所知道的Activity或者是應用App界面的顯示,只屬於Android圖形顯示的一部分。同樣可以在Android系統上展示圖像的WebView,Flutter,或者是通過Unity開發的3D游戲,他們的界面又是如何被繪制和顯現出來的呢?他們和我們所熟悉的Acitvity的界面顯示又有什麼異同點呢?我們可以不藉助Activity的setView或者InflateView機制來實現在屏幕上顯示出我們想要的界面嗎?Android系統顯示界面的方式又和IOS,或者Windows等系統有什麼區別呢?……
去探究這些問題,比僅僅知道Acitvity的界面是如何顯示出來更加的有價值,因為想要回答這些問題,就需要我們真正的掌握Android圖像顯示的底層原理,當我們掌握了底層的顯示原理後,我們會發現WebView,Flutter或者未來會出現的各種新的圖形顯示技術,原來都是大同小異。
我會花三篇文章的篇幅,去深入的講解Android圖形顯示的原理,OpenGL ES和Skia的繪制圖像的方式,他們如何使用,以及他們在Android中的使用場景,如開機動畫,Activity界面的軟體繪制和硬體繪制,以及Flutter的界面繪制。那麼,我們開始對Android圖像顯示原理的探索吧。
在講解Android圖像的顯示之前,我會先講一下屏幕圖像的顯示原理,畢竟我們圖像,最終都是在手機屏幕上顯示出來的,了解這一塊的知識會讓我們更容易的理解Android在圖像顯示上的機制。
圖像顯示的完整過程,分為下面幾個階段:
圖像數據→CPU→顯卡驅動→顯卡(GPU)→顯存(幀緩沖)→顯示器
我詳細介紹一下這幾個階段:
實際上顯卡驅動,顯卡和顯存,包括數模轉換模塊都是屬於顯卡的模塊。但為了能能詳細的講解經歷的步驟,這里做了拆分。
當顯存中有數據後,顯示器又是怎麼根據顯存裡面的數據來進行界面的顯示的呢?這里以LCD液晶屏為例,顯卡會將顯存里的數據,按照從左至右,從上到下的順序同步到屏幕上的每一個像素晶體管,一個像素晶體管就代表了一個像素。
如果我們的屏幕解析度是1080x1920像素,就表示有1080x1920個像素像素晶體管,每個橡素點的顏色越豐富,描述這個像素的數據就越大,比如單色,每個像素只需要1bit,16色時,只需要4bit,256色時,就需要一個位元組。那麼1080x1920的解析度的屏幕下,如果要以256色顯示,顯卡至少需要1080x1920個位元組,也就是2M的大小。
剛剛說了,屏幕上的像素數據是從左到右,從上到下進行同步的,當這個過程完成了,就表示一幀繪制完成了,於是會開始下一幀的繪制,大部分的顯示屏都是以60HZ的頻率在屏幕上繪制完一幀,也就是16ms,並且每次繪制新的一幀時,都會發出一個垂直同步信號(VSync)。我們已經知道,圖像數據都是放在幀緩沖中的,如果幀緩沖的緩沖區只有一個,那麼屏幕在繪制這一幀的時候,圖像數據便沒法放入幀緩沖中了,只能等待這一幀繪制完成,在這種情況下,會有很大了效率問題。所以為了解決這一問題,幀緩沖引入兩個緩沖區,即 雙緩沖機制 。雙緩沖雖然能解決效率問題,但會引入一個新的問題。當屏幕這一幀還沒繪制完成時,即屏幕內容剛顯示一半時,GPU 將新的一幀內容提交到幀緩沖區並把兩個緩沖區進行交換後,顯卡的像素同步模塊就會把新的一幀數據的下半段顯示到屏幕上,造成畫面撕裂現象。
為了解決撕裂問題,就需要在收到垂直同步的時候才將幀緩沖中的兩個緩沖區進行交換。Android4.1黃油計劃中有一個優化點,就是CPU和GPU都只有收到垂直同步的信號時,才會開始進行圖像的繪制操作,以及緩沖區的交換工作。
我們已經了解了屏幕圖像顯示的原理了,那麼接著開始對Android圖像顯示的學習。
從上一章已經知道,計算機渲染界面必須要有GPU和幀緩沖。對於Linux系統來說,用戶進程是沒法直接操作幀緩沖的,但我們想要顯示圖像就必須要操作幀緩沖,所以Linux系統設計了一個虛擬設備文件,來作為對幀緩沖的映射,通過對該文件的I/O讀寫,我們就可以實現讀寫屏操作。幀緩沖對應的設備文件於/dev/fb* ,*表示對多個顯示設備的支持, 設備號從0到31,如/dev/fb0就表示第一塊顯示屏,/dev/fb1就表示第二塊顯示屏。對於Android系統來說,默認使用/dev/fb0這一個設幀緩沖作為主屏幕,也就是我們的手機屏幕。我們Android手機屏幕上顯示的圖像數據,都是存儲在/dev/fb0里,早期AndroidStuio中的DDMS工具實現截屏的原理就是直接讀取/dev/fb0設備文件。
我們知道了手機屏幕上的圖形數據都存儲在幀緩沖中,所以Android手機圖像界面的原理就是將我們的圖像數據寫入到幀緩沖內。那麼,寫入到幀緩沖的圖像數據是怎麼生成的,又是怎樣加工的呢?圖形數據是怎樣送到幀緩沖去的,中間經歷了哪些步驟和過程呢?了解了這幾個問題,我們就了解了Android圖形渲染的原理,那麼帶著這幾個疑問,接著往下看。
想要知道圖像數據是怎麼產生的,我們需要知道 圖像生產者 有哪些,他們分別是如何生成圖像的,想要知道圖像數據是怎麼被消費的,我們需要知道 圖像消費者 有哪些,他們又分別是如何消費圖像的,想要知道中間經歷的步驟和過程,我們需要知道 圖像緩沖區 有哪些,他們是如何被創建,如何分配存儲空間,又是如何將數據從生產者傳遞到消費者的,圖像顯示是一個很經典的消費者生產者的模型,只有對這個模型各個模塊的擊破,了解他們之間的流動關系,我們才能找到一條更容易的路徑去掌握Android圖形顯示原理。我們看看谷歌提供的官方的架構圖是怎樣描述這一模型的模塊及關系的。
如圖, 圖像的生產者 主要有MediaPlayer,CameraPrevier,NDK,OpenGl ES。MediaPlayer和Camera Previer是通過直接讀取圖像源來生成圖像數據,NDK(Skia),OpenGL ES是通過自身的繪制能力生產的圖像數據; 圖像的消費者 有SurfaceFlinger,OpenGL ES Apps,以及HAL中的Hardware Composer。OpenGl ES既可以是圖像的生產者,也可以是圖像的消費者,所以它也放在了圖像消費模塊中; 圖像緩沖區 主要有Surface以及前面提到幀緩沖。
Android圖像顯示的原理,會僅僅圍繞 圖像的生產者 , 圖像的消費者 , 圖像緩沖區 來展開,在這一篇文章中,我們先看看Android系統中的圖像消費者。
SurfaceFlinger是Android系統中最重要的一個圖像消費者,Activity繪制的界面圖像,都會傳遞到SurfaceFlinger來,SurfaceFlinger的作用主要是接收圖像緩沖區數據,然後交給HWComposer或者OpenGL做合成,合成完成後,SurfaceFlinger會把最終的數據提交給幀緩沖。
那麼SurfaceFlinger是如何接收圖像緩沖區的數據的呢?我們需要先了解一下Layer(層)的概念,一個Layer包含了一個Surface,一個Surface對應了一塊圖形緩沖區,而一個界面是由多個Surface組成的,所以他們會一一對應到SurfaceFlinger的Layer中。SurfaceFlinger通過讀取Layer中的緩沖數據,就相當於讀取界面上Surface的圖像數據。Layer本質上是 Surface和SurfaceControl的組合 ,Surface是圖形生產者和圖像消費之間傳遞數據的緩沖區,SurfaceControl是Surface的控制類。
前面在屏幕圖像顯示原理中講到,為了防止圖像的撕裂,Android系統會在收到VSync垂直同步時才會開始處理圖像的繪制和合成工作,而Surfaceflinger作為一個圖像的消費者,同樣也是遵守這一規則,所以我們通過源碼來看看SurfaceFlinger是如何在這一規則下,消費圖像數據的。
SurfaceFlinger專門創建了一個EventThread線程用來接收VSync。EventThread通過Socket將VSync信號同步到EventQueue中,而EventQueue又通過回調的方式,將VSync信號同步到SurfaceFlinger內。我們看一下源碼實現。
上面主要是SurfaceFlinger初始化接收VSYNC垂直同步信號的操作,主要有這幾個過程:
經過上面幾個步驟,我們接收VSync的初始化工作都准備好了,EventThread也開始運轉了,接著看一下EventThread的運轉函數threadLoop做的事情。
threadLoop主要是兩件事情
mConditon又是怎麼接收VSync的呢?我們來看一下
可以看到,mCondition的VSync信號實際是DispSyncSource通過onVSyncEvent回調傳入的,但是DispSyncSource的VSync又是怎麼接收的呢?在上面講到的SurfaceFlinger的init函數,在創建EventThread的實現中,我們可以發現答案—— mPrimaryDispSync 。
DispSyncSource的構造方法傳入了mPrimaryDispSync,mPrimaryDispSync實際是一個DispSyncThread線程,我們看看這個線程的threadLoop方法
DispSyncThread的threadLoop會通過mPeriod來判斷是否進行阻塞或者進行VSync回調,那麼mPeriod又是哪兒被設置的呢?這里又回到SurfaceFlinger了,我們可以發現在SurfaceFlinger的 resyncToHardwareVsync 函數中有對mPeriod的賦值。
可以看到,這里最終通過HWComposer,也就是硬體層拿到了period。終於追蹤到了VSync的最終來源了, 它從HWCompser產生,回調至DispSync線程,然後DispSync線程回調到DispSyncSource,DispSyncSource又回調到EventThread,EventThread再通過Socket分發到MessageQueue中 。
我們已經知道了VSync信號來自於HWCompser,但SurfaceFlinger並不會一直監聽VSync信號,監聽VSync的線程大部分時間都是休眠狀態,只有需要做合成工作時,才會監聽VSync,這樣即保證圖像合成的操作能和VSync保持一致,也節省了性能。SurfaceFlinger提供了一些主動注冊監聽VSync的操作函數。
可以看到,只有當SurfaceFlinger調用 signalTransaction 或者 signalLayerUpdate 函數時,才會注冊監聽VSync信號。那麼signalTransaction或者signalLayerUpdate什麼時候被調用呢?它可以由圖像的生產者通知調用,也可以由SurfaceFlinger根據自己的邏輯來判斷是否調用。
現在假設App層已經生成了我們界面的圖像數據,並調用了 signalTransaction 通知SurfaceFlinger注冊監聽VSync,於是VSync信號便會傳遞到了MessageQueue中了,我們接著看看MessageQueue又是怎麼處理VSync的吧。
MessageQueue收到VSync信號後,最終回調到了SurfaceFlinger的 onMessageReceived 中,當SurfaceFlinger接收到VSync後,便開始以一個圖像消費者的角色來處理圖像數據了。我們接著看SurfaceFlinger是以什麼樣的方式消費圖像數據的。
VSync信號最終被SurfaceFlinger的onMessageReceived函數中的INVALIDATE模塊處理。
INVALIDATE的流程如下:
handleMessageTransaction的處理比較長,處理的事情也比較多,它主要做的事情有這些
handleMessageRefresh函數,便是SurfaceFlinger真正處理圖層合成的地方,它主要下面五個步驟。
我會詳細介紹每一個步驟的具體操作
合成前預處理會判斷Layer是否發生變化,當Layer中有新的待處理的Buffer幀(mQueuedFrames>0),或者mSidebandStreamChanged發生了變化, 都表示Layer發生了變化,如果變化了,就調用signalLayerUpdate,注冊下一次的VSync信號。如果Layer沒有發生變化,便只會做這一次的合成工作,不會注冊下一次VSync了。
重建Layer棧會遍歷Layer,計算和存儲每個Layer的臟區, 然後和當前的顯示設備進行比較,看Layer的臟區域是否在顯示設備的顯示區域內,如果在顯示區域內的話說明該layer是需要繪制的,則更新到顯示設備的VisibleLayersSortedByZ列表中,等待被合成
rebuildLayerStacks中最重要的一步是 computeVisibleRegions ,也就是對Layer的變化區域和非透明區域的計算,為什麼要對變化區域做計算呢?我們先看看SurfaceFlinger對界面顯示區域的分類:
還是以這張圖做例子,可以看到我們的狀態欄是半透明的,所以它是一個opaqueRegion區域,微信界面和虛擬按鍵是完全不透明的,他是一個visibleRegion,除了這三個Layer外,還有一個我們看不到的Layer——壁紙,它被上方visibleRegion遮擋了,所以是coveredRegion
對這幾個區域的概念清楚了,我們就可以去了解computeVisibleRegions中做的事情了,它主要是這幾步操作:
『柒』 android的自定義View的實現原理哪位能給我個思路呢。謝謝。
如果說要按類型來劃分的話,自定義View的實現方式大概可以分為三種,自繪控制項、組合控制項、以及繼承控制項。那麼下面我們就來依次學習一下,每種方式分別是如何自定義View的。
一、自繪控制項
自繪控制項的意思就是,這個View上所展現的內容全部都是我們自己繪制出來的。繪制的代碼是寫在onDraw()方法中的,而這部分內容我們已經在Android視圖繪制流程完全解析,帶你一步步深入了解View(二)中學習過了。
下面我們准備來自定義一個計數器View,這個View可以響應用戶的點擊事件,並自動記錄一共點擊了多少次。新建一個CounterView繼承自View,代碼如下所示:
<?xmlversion="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ffcb05">
<Button
android:id="@+id/button_left"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:background="@drawable/back_button"
android:text="Back"
android:textColor="#fff"/>
<TextView
android:id="@+id/title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="ThisisTitle"
android:textColor="#fff"
android:textSize="20sp"/>
</RelativeLayout>
在這個布局文件中,我們首先定義了一個RelativeLayout作為背景布局,然後在這個布局裡定義了一個Button和一個TextView,Button就是標題欄中的返回按鈕,TextView就是標題欄中的顯示的文字。
接下來創建一個TitleView繼承自FrameLayout,代碼如下所示:
{
privateButtonleftButton;
privateTextViewtitleText;
publicTitleView(Contextcontext,AttributeSetattrs){
super(context,attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
titleText=(TextView)findViewById(R.id.title_text);
leftButton=(Button)findViewById(R.id.button_left);
leftButton.setOnClickListener(newOnClickListener(){
@Override
publicvoidonClick(Viewv){
((Activity)getContext()).finish();
}
});
}
publicvoidsetTitleText(Stringtext){
titleText.setText(text);
}
publicvoidsetLeftButtonText(Stringtext){
leftButton.setText(text);
}
(OnClickListenerl){
leftButton.setOnClickListener(l);
}
}
TitleView中的代碼非常簡單,在TitleView的構建方法中,我們調用了LayoutInflater的inflate()方法來載入剛剛定義的title.xml布局,這部分內容我們已經在Android LayoutInflater原理分析,帶你一步步深入了解View(一)這篇文章中學習過了。
接下來調用findViewById()方法獲取到了返回按鈕的實例,然後在它的onClick事件中調用finish()方法來關閉當前的Activity,也就相當於實現返回功能了。
另外,為了讓TitleView有更強地擴展性,我們還提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分別用於設置標題欄上的文字、返回按鈕上的文字、以及返回按鈕的點擊事件。
到了這里,一個自定義的標題欄就完成了,那麼下面又到了如何引用這個自定義View的部分,其實方法基本都是相同的,在布局文件中添加如下代碼:
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.customview.TitleView
android:id="@+id/title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.example.customview.TitleView>
</RelativeLayout>
這樣就成功將一個標題欄控制項引入到布局文件中了,運行一下程序。
現在點擊一下Back按鈕,就可以關閉當前的Activity了。如果你想要修改標題欄上顯示的內容,或者返回按鈕的默認事件,只需要在Activity中通過findViewById()方法得到TitleView的實例,然後調用setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法進行設置就OK了。
『捌』 Android屏幕適配-應用篇
Android屏幕適配-基礎篇
Android屏幕適配-應用篇
從兩個大方面闡述一下Android的屏幕適配:
Android推薦使用dp作為尺寸單位來適配UI ,通過dp加上自適應布局和weight比例布局可以基本解決不同手機上適配的問題,這基本是最原始的Android適配方案。
缺點 :
(1)這種方案只能保證我們寫出來的界面適配絕大部分手機,部分手機仍然需要單獨適配,但dpi的不同,還是會存在差異。
(2)一般的設計稿都是以px為單位的,所以我們在寫layout文件的時候需要將px轉為dp,影響開發效率。
為了高效的實現UI開發,出現了新的適配方案,我把它稱作寬高限定符適配。簡單說,就是窮舉市面上所有的Android手機的寬高像素值,設定一個基準的解析度,其他解析度都根據這個基準解析度來計算,在不同的尺寸文件夾內部,根據該尺寸編寫對應的dimens文件:
鴻洋大神的作品 ,使用也超級簡單,核心功能就是在繪制的時候在onMeasure裡面做變換,重新計算px。
缺點 :我們自定義的控制項可能會被影響或限制,可能有些特定的控制項(框架沒有做適配的控制項),需要單獨適配。
小結:上述幾種適配方案都是實際開發中用過的方案,但隨著技術不斷的更新,出現了更好的適配方案。
實現原理 :Android會識別屏幕可用高度和寬度的最小尺寸的dp值( 其實就是手機的寬度值 ),然後根據識別到的結果去資源文件中尋找對應限定符的文件夾下的資源文件。
sw限定符適配 和 寬高限定符適配 類似,區別在於,前者有很好的容錯機制,如果沒有value-sw360dp文件夾,系統會向下尋找,比如離360dp最近的只有value-sw350dp,那麼Android就會選擇value-sw350dp文件夾下面的資源文件。這個特性就完美的解決了上文提到的寬高限定符的容錯問題。
優點: 1.非常穩定,極低概率出現意外
2.不會有任何性能的損耗
3.適配范圍可自由控制,不會影響其他三方庫
缺點 :就是多個dimens文件可能導致apk變大,幾百k。
附件: 生成sw文件的工具
實現原理 : 修改系統的density值 (核心)
今日頭條適配是以設計圖的寬或高進行適配的,適配最終是改變系統density實現的。
過程:
AndroidAutoSize 是基於今日頭條適配方案,該開源庫已經很大程度上解決了今日頭條適配方案的兩個缺點,可以對activity,fragment進行取消適配。也是目前我的項目中所使用的適配方案。
使用也非常簡單只需兩步:
(1)引入:
(2)在 AndroidManifest 中填寫全局設計圖尺寸 (單位 dp),如果使用副單位,則可以直接填寫像素尺寸,不需要再將像素轉化為 dp,詳情請查看 demo-subunits