Ubuntu Python Firefox 开源 Windows linux 云计算 编程 php 程序员 shell nginx Android mysql wordpress apache 微软 java centos google

ANDROID性能調優

一、性能瓶頸點
整個頁面主要由6個Page的ViewPager,每個Page為一個GridView,GridView一屏大概顯示4*4的item信息(本文最後有附圖)。由於網絡數據獲取較多且隨時需要保持頁面內app下載進度及狀態,所以出現以下性能問題
a.  ViewPager左右滑動明顯卡頓
b.  GridView上下滾動明顯卡頓
c.  其他Activity返回ViewPager Activity較慢
d.  網絡獲取到展現速度較慢


二、性能調試及定位
主要使用Traceview、monkey、monkey runner調試,traceview類似JAVA web調優的visualvm,使用方法如下:
在需要調優的activity onCreate函數中添加

android.os.debug.startMethodTracing("Entertainment");

1

android.os.debug.startMethodTracing("Entertainment");

onDestrory函數中添加

android.os.debug.stopMethodTracing();

1

android.os.debug.stopMethodTracing();

程序退出後會在sd卡根目錄下生成Entertainment.trace這個文件,cmd到android sdk的tools目錄下運行traceview.bat Entertainment.trace即可,截圖如下

從中可以看出各個函數的調用時間、調用次數、平均調用時間、時間占用百分比等從而定位到耗時的操作。monkey、monkey runner更詳細的見後面博客介紹


三、性能調優點
主要包括同步改異步、緩存、Layout優化、數據庫優化、算法優化、延遲執行。
1. 同步改異步
這個就不用多講了,耗時操作放在線程中執行防止占用主線程,一定程度上解決anr。
但需要註意線程和service結合(防止activity被回收後線程也被回收)以及線程的數量
線程池使用可見java的線程池


2. 緩存
java的對象創建需要分配資源較耗費時間,加上創建的對象越多會造成越頻繁的gc影響系統響應。主要使用單例模式、緩存(圖片緩存、線程池、View緩存、IO緩存、消息緩存、通知欄notification緩存)及其他方式減少對象創建。
(1). 單例模式
對於創建開銷較大的類可使用此方法,保證全局一個實例,在程序運行過程中該類不會因新建額外對象產生開銷。示例代碼如下:

單例模式

Java

public class Singleton { private static Object obj = new Object(); private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance() { // if already inited, no need to get lock everytime if (instance == null) { synchronized (obj) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public class Singleton {

 

    private static Object    obj      = new Object();

    private static Singleton instance = null;

 

    private Singleton(){

    }

 

    public static Singleton getInstance() {

        // if already inited, no need to get lock everytime

        if (instance == null) {

            synchronized (obj) {

                if (instance == null) {

                    instance = new Singleton();

                }

            }

        }

 

        return instance;

    }

}

(2). 緩存
程序中用到了圖片緩存、線程池、View緩存、IO緩存、消息緩存、通知欄notification緩存等。
a. 圖片緩存:見ImageCache和ImageSdCache


b. 線程池:使用Java的Executors類,通過newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool提供四種不同類型的線程池


c. View緩存:

可見ListView緩存機制

listView的getView緩存

Java

@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.type_item, null); holder = new ViewHolder(); holder.imageView = (ImageView)convertView.findViewById(R.id.app_icon); holder.textView = (TextView)convertView.findViewById(R.id.app_name); convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } holder.imageView.setImageResource(R.drawable.index_default_image); holder.textView.setText(""); return convertView; } /** * ViewHolder */ static class ViewHolder { ImageView imageView; TextView textView; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@Override

public View getView(int position, View convertView, ViewGroup parent) {

    ViewHolder holder;

    if (convertView == null) {

        convertView = inflater.inflate(R.layout.type_item, null);

        holder = new ViewHolder();

        holder.imageView = (ImageView)convertView.findViewById(R.id.app_icon);

        holder.textView = (TextView)convertView.findViewById(R.id.app_name);

        convertView.setTag(holder);

    } else {

        holder = (ViewHolder)convertView.getTag();

    }

    holder.imageView.setImageResource(R.drawable.index_default_image);

    holder.textView.setText("");

    return convertView;

}

 

/**

* ViewHolder

*/

static class ViewHolder {

 

    ImageView imageView;

    TextView  textView;

}

通過convertView是否為null減少layout inflate次數,通過靜態的ViewHolder減少findViewById的次數,這兩個函數尤其是inflate是相當費時間的


d. IO緩存:
使用具有緩存策略的輸入流,BufferedInputStream替代InputStream,BufferedReader替代Reader,BufferedReader替代BufferedInputStream.對文件、網絡IO皆適用。


e. 消息緩存:通過Handler的obtainMessage回收就的Message對象,減少Message對象的創建開銷
handler.sendMessage(handler.obtainMessage(1));


f. 通知欄notification緩存:下載中需要不斷改變通知欄進度條狀態,如果不斷新建Notification會導致通知欄很卡。這裏我們可以使用最簡單的緩存
Map<String, Notification> notificationMap = new HashMap<String, Notification>();如果notificationMap中不存在,則新建notification並且put into map.


(3). 其他
能創建基類解決問題就不用具體子類:除需要設置優先級的線程使用new Thread創建外,其余線程創建使用new Runnable。因為子類會有自己的屬性創建需要更多開銷。
控制最大並發數量:使用Java的Executors類,通過Executors.newFixedThreadPool(nThreads)控制線程池最大線程並發
對於http請求增加timeout

3. Layout優化
使用抽象布局標簽(include, viewstub, merge)、去除不必要的嵌套和View節點、減少不必要的infalte及其他Layout方面可調優點,順帶提及布局調優相關工具(hierarchy viewer和lint)。具體可見性能優化之布局優化
TextView屬性優化:TextView的android:ellipsize=”marquee”跑馬燈效果極耗性能,具體原因還在深入源碼中

4. 數據庫優化
主要包括索引和事務及針對Sqlite的優化。具體可見性能優化之數據庫優化


5. 算法優化
這個就是個博大精深的話題了,只介紹本應用中使用的。
使用hashMap代替arrayList,時間復雜度降低一個數量級


6. 延遲執行
對於很多耗時邏輯沒必要立即執行,這時候我們可以將其延遲執行。
線程延遲執行 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
消息延遲發送 handler.sendMessageDelayed(handler.obtainMessage(0), 1000);


四、本程序性能調優結果
1. ViewPager左右滑動明顯卡頓
2. GridView上下滾動明顯卡頓
(1). 去掉TextView的android:ellipsize=”marquee”
(2). 修改圖片緩存的最大線程數,增加http timeout
(3). 修改設置app是否已安裝的狀態,具體代碼修改如下:

Java

List<PackageInfo> installedPackageList = getPackageManager().getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES); List<App> installedAppList = function(installedAppList) for (App app : appList) { for (App installedApp : installedAppList) { } }

1

2

3

4

5

6

7

List<PackageInfo> installedPackageList = getPackageManager().getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);

List<App> installedAppList = function(installedAppList)

for (App app : appList) {

    for (App installedApp : installedAppList) {

 

    }

}

修改为

Java

for (App app : appList) { Pair<Integer, String> versionInfo = INSTALLED_APP_MAP.get(app.getPackageName()); if (versionInfo != null) { } else { } }

1

2

3

4

5

6

7

8

for (App app : appList) {

    Pair<Integer, String> versionInfo = INSTALLED_APP_MAP.get(app.getPackageName());

    if (versionInfo != null) {

 

    } else {

 

    }

}

從每次獲取List<PackageInfo> installedAppList = getPackageManager().getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);修改為只在有應用安裝或卸載廣播時獲取應用列表,並且用hashMap代替installedAppList減少查詢時間。

將平均執行時間從201ms降低到1ms。


3. 其他Activity返回ViewPager Activity較慢
定位:在onStart函數
解決:使用延遲策略,具體代碼修改如下:

Java

@Override public void onStart() { super.onStart(); appUpdateListAdapter.notifyDataSetChanged(); }

1

2

3

4

5

@Override

public void onStart() {

    super.onStart();

    appUpdateListAdapter.notifyDataSetChanged();

}

改为

优化后代码

Java

public void onStart() { super.onStart(); // delay send message handler.sendMessageDelayed(handler.obtainMessage(MessageConstants.WHAT_NOTIFY_DATA_CHANGED), 100); } private class MyHandler extends Handler { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case MessageConstants.WHAT_NOTIFY_DATA_CHANGED: if (appUpdateListAdapter != null) { appUpdateListAdapter.notifyDataSetChanged(); } break; } } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public void onStart() {

    super.onStart();

    // delay send message

    handler.sendMessageDelayed(handler.obtainMessage(MessageConstants.WHAT_NOTIFY_DATA_CHANGED), 100);

}

 

private class MyHandler extends Handler {

 

    @Override

    public void handleMessage(Message msg) {

        super.handleMessage(msg);

 

        switch (msg.what) {

            case MessageConstants.WHAT_NOTIFY_DATA_CHANGED:

                if (appUpdateListAdapter != null) {

                    appUpdateListAdapter.notifyDataSetChanged();

                }

                break;

        }

    }

}


4. 网络获取到展现速度较慢

定位:在HttpURLConnection.getInputStream()之后的处理
解决:使用BufferedReader替代BufferedInputStream获取时间从100ms降低到3ms,具体代码修改如下:

Java

HttpURLConnection con = (HttpURLConnection)url.openConnection(); InputStream input = con.getInputStream(); while (input.read(buffer, 0, 1024) != -1) { }

1

2

3

4

5

HttpURLConnection con = (HttpURLConnection)url.openConnection();

InputStream input = con.getInputStream();

while (input.read(buffer, 0, 1024) != -1) {

 

}

改为

Java

HttpURLConnection con = (HttpURLConnection)url.openConnection(); BufferedReader input = new BufferedReader(new InputStreamReader(con.getInputStream())); String s; while ((s = input.readLine()) != null) { }

1

2

3

4

5

6

HttpURLConnection con = (HttpURLConnection)url.openConnection();

BufferedReader input = new BufferedReader(new InputStreamReader(con.getInputStream()));

String s;

while ((s = input.readLine()) != null) {

 

}

 

 

 

性能優化系列總篇

本文為性能優化系列的總綱,主要介紹性能調優專題計劃、何為性能問題、性能調優方式及前面介紹的數據庫優化、布局優化、Java(Android)代碼優化具體對應的調優方式。

1、調優專題博客計劃
目前調優專題已完成五部分:

性能優化總綱——性能問題及性能調優方式
性能優化第三篇——Java(Android)代碼優化
性能優化第二篇——布局優化
性能優化第一篇——數據庫性能優化

性能優化實例 

後續計劃性能優化——診斷及工具(目前只有關於TraceView的介紹)、性能優化——內存篇、性能優化——JNI篇,性能優化——電量篇。

2、何為性能問題
在性能測試中存在兩個概念:
(1). 響應時間
指從用戶操作開始到系統給用戶以正確反饋的時間。一般包括系統處理時間 + 網絡傳輸時間 + 展現時間。對於非網絡類應用不包括網絡傳輸時間。響應時間是用戶對性能最直接的感受。

(2). TPS(Transaction Per Second)
TPS為每秒處理的事務數,是系統吞吐量的指標,在搜索系統中也用QPS(Query Per Second)衡量。TPS一般與響應時間反相關。

通常所說的性能問題就是指響應時間過長、系統吞吐量過低。在web性能測試中,也會將高並發下內存泄漏歸為性能問題。

在Android應用程序中由於系統ANR的限制,所以對主線程的響應時間提出了更高的要求。Android ANR的具體要求是指Activity對事件響應不超過5秒,BroadcastReceiver中執行時間不超過10秒。

3、性能調優方式

明白了何為性能問題之後,就能明白性能優化實際就是優化系統的響應時間,提高TPS。優化響應時間,提高TPS的方式包括:
(1) 降低執行時間
這部分包括:a. 緩存(包括對象緩存、IO緩存、網絡緩存), b. 數據存儲類型優化, c. 算法優化, d. JNI, e. 邏輯優化, f. 需求優化

(2) 同步改異步,利用多線程提高TPS

(3) 提前或延遲操作,錯開時間段提高TPS

對於數據庫優化、布局優化、Java代碼部分優化都可以歸納到上面的幾種方式中。具體見:

性能優化第三篇——Java(Android)代碼優化
性能優化第二篇——布局優化
性能優化第一篇——數據庫性能優化

性能優化實例 

性能優化之布局優化

本文為Android性能優化的第二篇——布局優化,主要介紹使用抽象布局標簽(include, viewstub, merge)、去除不必要的嵌套和View節點、減少不必要的infalte及其他Layout方面可調優點,順帶提及布局調優相關工具(hierarchy viewer和lint)。

性能優化專題已完成五部分:

性能優化總綱——性能問題及性能調優方式
性能優化第三篇——Java(Android)代碼優化
性能優化第二篇——布局優化
性能優化第一篇——數據庫性能優化

性能優化實例 

1、抽象布局標簽 

(1) <include>標簽
include標簽常用於將布局中的公共部分提取出來供其他layout共用,以實現布局模塊化,這在布局編寫方便提供了大大的便利。
下面以在一個布局main.xml中用include引入另一個布局foot.xml為例。main.mxl代碼如下:

Java

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@+id/simple_list_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="@dimen/dp_80" /> <include layout="@layout/foot.xml" /> </RelativeLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent" >

 

    <ListView

        android:id="@+id/simple_list_view"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:layout_marginBottom="@dimen/dp_80" />

 

    <include layout="@layout/foot.xml" />

 

</RelativeLayout>

其中include引入的foot.xml为公用的页面底部,代码如下:

Java

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="@dimen/dp_40" android:layout_above="@+id/text"/> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="@dimen/dp_40" android:layout_alignParentBottom="true" android:text="@string/app_name" /> </RelativeLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent" >

 

    <Button

        android:id="@+id/button"

        android:layout_width="match_parent"

        android:layout_height="@dimen/dp_40"

        android:layout_above="@+id/text"/>

 

    <TextView

        android:id="@+id/text"

        android:layout_width="match_parent"

        android:layout_height="@dimen/dp_40"

        android:layout_alignParentBottom="true"

        android:text="@string/app_name" />

 

</RelativeLayout>

<include>標簽唯一需要的屬性是layout屬性,指定需要包含的布局文件。可以定義android:id和android:layout_*屬性來覆蓋被引入布局根節點的對應屬性值。註意重新定義android:id後,子布局的頂結點i就變化了。

(2) <viewstub>標簽
viewstub標簽同include標簽一樣可以用來引入一個外部布局,不同的是,viewstub引入的布局默認不會擴張,即既不會占用顯示也不會占用位置,從而在解析layout時節省cpu和內存。
viewstub常用來引入那些默認不會顯示,只在特殊情況下顯示的布局,如進度布局、網絡失敗顯示的刷新布局、信息出錯出現的提示布局等。
下面以在一個布局main.xml中加入網絡錯誤時的提示頁面network_error.xml為例。main.mxl代碼如下:

Java

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > …… <ViewStub android:id="@+id/network_error_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:layout="@layout/network_error" /> </RelativeLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent" >

 

    ……

    <ViewStub

        android:id="@+id/network_error_layout"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:layout="@layout/network_error" />

 

</RelativeLayout>

其中network_error.xml为只有在网络错误时才需要显示的布局,默认不会被解析,示例代码如下:

Java

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:id="@+id/network_setting" android:layout_width="@dimen/dp_160" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:text="@string/network_setting" /> <Button android:id="@+id/network_refresh" android:layout_width="@dimen/dp_160" android:layout_height="wrap_content" android:layout_below="@+id/network_setting" android:layout_centerHorizontal="true" android:layout_marginTop="@dimen/dp_10" android:text="@string/network_refresh" /> </RelativeLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent" >

 

    <Button

        android:id="@+id/network_setting"

        android:layout_width="@dimen/dp_160"

        android:layout_height="wrap_content"

        android:layout_centerHorizontal="true"

        android:text="@string/network_setting" />

 

    <Button

        android:id="@+id/network_refresh"

        android:layout_width="@dimen/dp_160"

        android:layout_height="wrap_content"

        android:layout_below="@+id/network_setting"

        android:layout_centerHorizontal="true"

        android:layout_marginTop="@dimen/dp_10"

        android:text="@string/network_refresh" />

 

</RelativeLayout>

在java中通过(ViewStub)findViewById(id)找到ViewStub,通过stub.inflate()展开ViewStub,然后得到子View,如下:

Java

private View networkErrorView; private void showNetError() { // not repeated infalte if (networkErrorView != null) { networkErrorView.setVisibility(View.VISIBLE); return; } ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout); networkErrorView = stub.inflate(); Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting); Button refresh = (Button)findViewById(R.id.network_refresh); } private void showNormal() { if (networkErrorView != null) { networkErrorView.setVisibility(View.GONE); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

private View networkErrorView;

 

private void showNetError() {

    // not repeated infalte

    if (networkErrorView != null) {

        networkErrorView.setVisibility(View.VISIBLE);

        return;

    }

 

    ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);

    networkErrorView = stub.inflate();

    Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting);

    Button refresh = (Button)findViewById(R.id.network_refresh);

}

 

private void showNormal() {

    if (networkErrorView != null) {

        networkErrorView.setVisibility(View.GONE);

    }

}

在上面showNetError()中展開了ViewStub,同時我們對networkErrorView進行了保存,這樣下次不用繼續inflate。這就是後面第三部分提到的減少不必要的infalte。

viewstub標簽大部分屬性同include標簽類似。

上面展開ViewStub部分代碼

Java

ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout); networkErrorView = stub.inflate();

1

2

ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);

networkErrorView = stub.inflate();

也可以写成下面的形式

Java

View viewStub = findViewById(R.id.network_error_layout); viewStub.setVisibility(View.VISIBLE); // ViewStub被展开后的布局所替换 networkErrorView = findViewById(R.id.network_error_layout); // 获取展开后的布局

1

2

3

View viewStub = findViewById(R.id.network_error_layout);

viewStub.setVisibility(View.VISIBLE);   // ViewStub被展开后的布局所替换

networkErrorView =  findViewById(R.id.network_error_layout); // 获取展开后的布局

效果一致,只是不用顯示的轉換為ViewStub。通過viewstub的原理我們可以知道將一個view設置為GONE不會被解析,從而提高layout解析速度,而VISIBLE和INVISIBLE這兩個可見性屬性會被正常解析。

(3) <merge>標簽
在使用了include後可能導致布局嵌套過多,多余不必要的layout節點,從而導致解析變慢,不必要的節點和嵌套可通過hierarchy viewer(下面布局調優工具中有具體介紹)或設置->開發者選項->顯示布局邊界查看。

merge標簽可用於兩種典型情況:
a.  布局頂結點是FrameLayout且不需要設置background或padding等屬性,可以用merge代替,因為Activity內容試圖的parent view就是個FrameLayout,所以可以用merge消除只剩一個。
b.  某布局作為子布局被其他布局include時,使用merge當作該布局的頂節點,這樣在被引入時頂結點會自動被忽略,而將其子節點全部合並到主布局中。

以(1) <include>標簽的示例為例,用hierarchy viewer查看main.xml布局如下圖:


可以发现多了一层没必要的RelativeLayout,将foot.xml中RelativeLayout改为merge,如下:

Java

<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="@dimen/dp_40" android:layout_above="@+id/text"/> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="@dimen/dp_40" android:layout_alignParentBottom="true" android:text="@string/app_name" /> </merge>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent" >

 

    <Button

        android:id="@+id/button"

        android:layout_width="match_parent"

        android:layout_height="@dimen/dp_40"

        android:layout_above="@+id/text"/>

 

    <TextView

        android:id="@+id/text"

        android:layout_width="match_parent"

        android:layout_height="@dimen/dp_40"

        android:layout_alignParentBottom="true"

        android:text="@string/app_name" />

 

</merge>

运行后再次用hierarchy viewer查看main.xml布局如下图:

這樣就不會有多余的RelativeLayout節點了。

2、去除不必要的嵌套和View節點
(1) 首次不需要使用的節點設置為GONE或使用viewstub
(2) 使用RelativeLayout代替LinearLayout
大約在Android4.0之前,新建工程的默認main.xml中頂節點是LinearLayout,而在之後已經改為RelativeLayout,因為RelativeLayout性能更優,且可以簡單實現LinearLayout嵌套才能實現的布局。
4.0及以上Android版本可通過設置->開發者選項->顯示布局邊界打開頁面布局顯示,看看是否有不必要的節點和嵌套。4.0以下版本可通過hierarchy viewer查看。

3、減少不必要的infalte
(1) 對於inflate的布局可以直接緩存,用全部變量代替局部變量,避免下次需再次inflate
如上面ViewStub示例中的

Java

if (networkErrorView != null) { networkErrorView.setVisibility(View.VISIBLE); return; }

1

2

3

4

if (networkErrorView != null) {

    networkErrorView.setVisibility(View.VISIBLE);

    return;

}

 

(2) ListView提供了item缓存,adapter getView的标准写法,如下:

Java

@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, null); holder = new ViewHolder(); …… convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } } /** * ViewHolder * * @author trinea@trinea.cn 2013-08-01 */ private static class ViewHolder { ImageView appIcon; TextView appName; TextView appInfo; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

@Override

public View getView(int position, View convertView, ViewGroup parent) {

    ViewHolder holder;

    if (convertView == null) {

        convertView = inflater.inflate(R.layout.list_item, null);

        holder = new ViewHolder();

        ……

        convertView.setTag(holder);

    } else {

        holder = (ViewHolder)convertView.getTag();

    }

}

 

/**

* ViewHolder

*

* @author trinea@trinea.cn 2013-08-01

*/

private static class ViewHolder {

 

    ImageView appIcon;

    TextView  appName;

    TextView  appInfo;

}

关于ListView缓存原理可见Android ListView缓存机制

 

4、其他點
(1) 用SurfaceView或TextureView代替普通View
SurfaceView或TextureView可以通過將繪圖操作移動到另一個單獨線程上提高性能。
普通View的繪制過程都是在主線程(UI線程)中完成,如果某些繪圖操作影響性能就不好優化了,這時我們可以考慮使用SurfaceView和TextureView,他們的繪圖操作發生在UI線程之外的另一個線程上。
因為SurfaceView在常規視圖系統之外,所以無法像常規試圖一樣移動、縮放或旋轉一個SurfaceView。TextureView是Android4.0引入的,除了與SurfaceView一樣在單獨線程繪制外,還可以像常規視圖一樣被改變。

(2) 使用RenderJavascript
RenderScript是Adnroid3.0引進的用來在Android上寫高性能代碼的一種語言,語法給予C語言的C99標準,他的結構是獨立的,所以不需要為不同的CPU或者GPU定制代碼代碼。

(3) 使用OpenGL繪圖
Android支持使用OpenGL API的高性能繪圖,這是Android可用的最高級的繪圖機制,在遊戲類對性能要求較高的應用中得到廣泛使用。
Android 4.3最大的改變,就是支持OpenGL ES 3.0。相比2.0,3.0有更多的緩沖區對象、增加了新的著色語言、增加多紋理支持等等,將為Android遊戲帶來更出色的視覺體驗。

(4) 盡量為所有分辨率創建資源

減少不必要的硬件縮放,這會降低UI的繪制速度,可借助Android asset studio

5、布局調優工具
(1) hierarchy viewer
hierarchy viewer可以方便的查看Activity的布局,各個View的屬性、measure、layout、draw的時間,如果耗時較多會用紅色標記,否則顯示綠色。
hierarchy viewer.bat位於<sdk>/tools/目錄下。使用可見:Using Hierarchy Viewer , 示例圖如下:

 

(2) layoutopt

layoutopt是一個可以提供layout及其層級優化提示的命令行,在sdk16以後已經被lint取代,在Windows->Show View->Other->Android->Lint Warnings查看lint優化提示,lint具體介紹可見Improving Your Code with lint。

性能優化之Java(Android)代碼優化

本文為Android性能優化的第三篇——Java(Android)代碼優化。主要介紹Java代碼中性能優化方式及網絡優化,包括緩存、異步、延遲、數據存儲、算法、JNI、邏輯等優化方式。(時間倉促,後面還會繼續完善^_*)

性能優化專題已完成五部分:

性能優化總綱——性能問題及性能調優方式
性能優化第三篇——Java(Android)代碼優化
性能優化第二篇——布局優化
性能優化第一篇——數據庫性能優化

性能優化實例 
1、降低執行時間
這部分包括:緩存、數據存儲優化、算法優化、JNI、邏輯優化、需求優化幾種優化方式。
(1). 緩存
緩存主要包括對象緩存、IO緩存、網絡緩存、DB緩存,對象緩存能減少內存的分配,IO緩存減少磁盤的讀寫次數,網絡緩存減少網絡傳輸,DB緩存較少Database的訪問次數。
在內存、文件、數據庫、網絡的讀寫速度中,內存都是最優的,且速度數量級差別,所以盡量將需要頻繁訪問或訪問一次消耗較大的數據存儲在緩存中。

Android中常使用緩存:
a.  線程池
b.  Android圖片緩存,Android圖片Sdcard緩存,數據預取緩存
c. 消息緩存
通過handler.obtainMessage復用之前的message,如下:

handler.sendMessage(handler.obtainMessage(0, object));

1

handler.sendMessage(handler.obtainMessage(0, object));

d. ListView缓存

e. 網絡緩存
數據庫緩存http response,根據http頭信息中的Cache-Control域確定緩存過期時間。
f. 文件IO緩存
使用具有緩存策略的輸入流,BufferedInputStream替代InputStream,BufferedReader替代Reader,BufferedReader替代BufferedInputStream.對文件、網絡IO皆適用。
g. layout緩存
h. 其他需要頻繁訪問或訪問一次消耗較大的數據緩存

(2). 數據存儲優化
包括數據類型、數據結構的選擇。
a. 數據類型選擇
字符串拼接用StringBuilder代替String,在非並發情況下用StringBuilder代替StringBuffer。如果你對字符串的長度有大致了解,如100字符左右,可以直接new StringBuilder(128)指定初始大小,減少空間不夠時的再次分配。
64位類型如long double的處理比32位如int慢
使用SoftReference、WeakReference相對正常的強應用來說更有利於系統垃圾回收
final類型存儲在常量區中讀取效率更高
LocalBroadcastManager代替普通BroadcastReceiver,效率和安全性都更高

b. 數據結構選擇
常見的數據結構選擇如:
ArrayList和LinkedList的選擇,ArrayList根據index取值更快,LinkedList更占內存、隨機插入刪除更快速、擴容效率更高。一般推薦ArrayList。
ArrayList、HashMap、LinkedHashMap、HashSet的選擇,hash系列數據結構查詢速度更優,ArrayList存儲有序元素,HashMap為鍵值對數據結構,LinkedHashMap可以記住加入次序的hashMap,HashSet不允許重復元素。
HashMap、WeakHashMap選擇,WeakHashMap中元素可在適當時候被系統垃圾回收器自動回收,所以適合在內存緊張型中使用。
Collections.synchronizedMap和ConcurrentHashMap的選擇,ConcurrentHashMap為細分鎖,鎖粒度更小,並發性能更優。Collections.synchronizedMap為對象鎖,自己添加函數進行鎖控制更方便。

Android也提供了一些性能更優的數據類型,如SparseArray、SparseBooleanArray、SparseIntArray、Pair。
Sparse系列的數據結構是為key為int情況的特殊處理,采用二分查找及簡單的數組存儲,加上不需要泛型轉換的開銷,相對Map來說性能更優。不過我不太明白為啥默認的容量大小是10,是做過數據統計嗎,還是說現在的內存優化不需要考慮這些東西,寫16會死嗎,還是建議大家根據自己可能的容量設置初始值。

(3). 算法優化
這個主題比較大,需要具體問題具體分析,盡量不用O(n*n)時間復雜度以上的算法,必要時候可用空間換時間。
查詢考慮hash和二分,盡量不用遞歸。可以從結構之法 算法之道或微軟、Google等面試題學習。

(4). JNI
Android應用程序大都通過Java開發,需要Dalvik的JIT編譯器將Java字節碼轉換成本地代碼運行,而本地代碼可以直接由設備管理器直接執行,節省了中間步驟,所以執行速度更快。不過需要註意從Java空間切換到本地空間需要開銷,同時JIT編譯器也能生成優化的本地代碼,所以糟糕的本地代碼不一定性能更優。
這個優化點會在後面單獨用一片博客介紹。

(5). 邏輯優化
這個不同於算法,主要是理清程序邏輯,減少不必要的操作。

(6). 需求優化
這個就不說了,對於sb的需求可能帶來的性能問題,只能說做為一個合格的程序員不能只是執行者,要學會說NO。不過不能拿這種接口敷衍產品經理哦。

2、異步,利用多線程提高TPS
充分利用多核Cpu優勢,利用線程解決密集型計算、IO、網絡等操作。
關於多線程可參考:
在Android應用程序中由於系統ANR的限制,將可能造成主線程超時操作放入另外的工作線程中。在工作線程中可以通過handler和主線程交互。

3、提前或延遲操作,錯開時間段提高TPS
(1) 延遲操作
不在Activity、Service、BroadcastReceiver的生命周期等對響應時間敏感函數中執行耗時操作,可適當delay。
Java中延遲操作可使用ScheduledExecutorService,不推薦使用Timer.schedule;
Android中除了支持ScheduledExecutorService之外,還有一些delay操作,如
handler.postDelayed,handler.postAtTime,handler.sendMessageDelayed,View.postDelayed,AlarmManager定時等。

(2) 提前操作
對於第一次調用較耗時操作,可統一放到初始化中,將耗時提前。如得到壁紙wallpaperManager.getDrawable();

4、網絡優化
以下是網絡優化中一些客戶端和服務器端需要盡量遵守的準則:
a. 圖片必須緩存,最好根據機型做圖片做圖片適配
b. 所有http請求必須添加httptimeout

c. 開啟gzip壓縮
d. api接口數據以json格式返回,而不是xml或html
e. 根據http頭信息中的Cache-Control及expires域確定是否緩存請求結果。

f. 確定網絡請求的connection是否keep-alive
g. 減少網絡請求次數,服務器端適當做請求合並。
h. 減少重定向次數
i. api接口服務器端響應時間不超過100ms
google正在做將移動端網頁速度降至1秒的項目,關註中https://developers.google.com/speed/docs/insights/mobile

性能優化之數據庫優化

本文為性能優化的第一篇——數據庫性能優化,原理適用於大部分數據庫包括Sqlite、mysql、Oracle、Sql server,詳細介紹了索引(優缺點、分類、場景、規則)和事務,最後介紹了部分單獨針對Sqlite的優化。

性能優化專題已完成五部分:

性能優化總綱——性能問題及性能調優方式
性能優化第三篇——Java(Android)代碼優化
性能優化第二篇——布局優化
性能優化第一篇——數據庫性能優化

性能優化實例 
1、索引
簡單的說,索引就像書本的目錄,目錄可以快速找到所在頁數,數據庫中索引可以幫助快速找到數據,而不用全表掃描,合適的索引可以大大提高數據庫查詢的效率。
(1). 優點
大大加快了數據庫檢索的速度,包括對單表查詢、連表查詢、分組查詢、排序查詢。經常是一到兩個數量級的性能提升,且隨著數據數量級增長。

(2). 缺點
索引的創建和維護存在消耗,索引會占用物理空間,且隨著數據量的增加而增加。
在對數據庫進行增刪改時需要維護索引,所以會對增刪改的性能存在影響。

(3). 分類
a. 直接創建索引和間接創建索引
直接創建: 使用sql語句創建,Android中可以在SQLiteOpenHelper的onCreate或是onUpgrade中直接excuSql創建語句,語句如

CREATE INDEX mycolumn_index ON mytable (myclumn)

1

CREATE INDEX mycolumn_index ON mytable (myclumn)

间接创建: 定义主键约束或者唯一性键约束,可以间接创建索引,主键默认为唯一索引。

 

b. 普通索引和唯一性索引
普通索引:

CREATE INDEX mycolumn_index ON mytable (myclumn)

1

CREATE INDEX mycolumn_index ON mytable (myclumn)

唯一性索引:保证在索引列中的全部数据是唯一的,对聚簇索引和非聚簇索引都可以使用,语句为

CREATE UNIQUE COUSTERED INDEX myclumn_cindex ON mytable(mycolumn)

1

CREATE UNIQUE COUSTERED INDEX myclumn_cindex ON mytable(mycolumn)

 

c. 单个索引和复合索引

单个索引:索引建立语句中仅包含单个字段,如上面的普通索引和唯一性索引创建示例。
复合索引:又叫组合索引,在索引建立语句中同时包含多个字段,语句如:

CREATE INDEX name_index ON username(firstname, lastname)

1

CREATE INDEX name_index ON username(firstname, lastname)

其中firstname为前导列。

 

d. 聚簇索引和非聚簇索引(聚集索引,群集索引)
聚簇索引:物理索引,与基表的物理顺序相同,数据值的顺序总是按照顺序排列,语句为:

CREATE CLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn) WITH ALLOW_DUP_ROW

1

CREATE CLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn) WITH ALLOW_DUP_ROW

其中WITH ALLOW_DUP_ROW表示允许有重复记录的聚簇索引

非聚簇索引:

CREATE UNCLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn)

1

CREATE UNCLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn)

索引默认为非聚簇索引

 

(4). 使用場景
在上面講到了優缺點,那麽肯定會對何時使用索引既有點明白又有點糊塗吧,那麽下面總結下:
a.  當某字段數據更新頻率較低,查詢頻率較高,經常有範圍查詢(>, <, =, >=, <=)或order by、group by發生時建議使用索引。並且選擇度越大,建索引越有優勢,這裏選擇度指一個字段中唯一值的數量/總的數量。
b.  經常同時存取多列,且每列都含有重復值可考慮建立復合索引

(5). 索引使用規則
a.  對於復合索引,把使用最頻繁的列做為前導列(索引中第一個字段)。如果查詢時前導列不在查詢條件中則該復合索引不會被使用。
如create unique index PK_GRADE_CLASS on student (grade, class)
select * from student where class = 2未使用到索引
select * from dept where grade = 3使用到了索引

b.  避免對索引列進行計算,對where子句列的任何計算如果不能被編譯優化,都會導致查詢時索引失效
select * from student where tochar(grade)=’2′
c.  比較值避免使用NULL
d.  多表查詢時要註意是選擇合適的表做為內表。連接條件要充份考慮帶有索引的表、行數多的表,內外表的選擇可由公式:外層表中的匹配行數*內層表中每一次查找的次數確定,乘積最小為最佳方案。實際多表操作在被實際執行前,查詢優化器會根據連接條件,列出幾組可能的連接方案並從中找出系統開銷最小的最佳方案。

e.  查詢列與索引列次序一致
f.  用多表連接代替EXISTS子句
g.  把過濾記錄數最多的條件放在最前面
h.  善於使用存儲過程,它使sql變得更加靈活和高效(Sqlite不支持存儲過程::>_<:: )

(6)索引檢驗

建立了索引,對於某條sql語句是否使用到了索引可以通過執行計劃查看是否用到了索引。

2、使用事務
使用事務的兩大好處是原子提交和更優性能。
(1) 原子提交
原則提交意味著同一事務內的所有修改要麽都完成要麽都不做,如果某個修改失敗,會自動回滾使得所有修改不生效。

(2) 更優性能
Sqlite默認會為每個插入、更新操作創建一個事務,並且在每次插入、更新後立即提交。

這樣如果連續插入100次數據實際是創建事務->執行語句->提交這個過程被重復執行了100次。如果我們顯示的創建事務->執行100條語句->提交會使得這個創建事務和提交這個過程只做一次,通過這種一次性事務可以使得性能大幅提升。尤其當數據庫位於sd卡時,時間上能節省兩個數量級左右。

Sqlte顯示使用事務,示例代碼如下:

Java

public void insertWithOneTransaction() { SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase(); // Begins a transaction db.beginTransaction(); try { // your sqls for (int i = 0; i < 100; i++) { db.insert(yourTableName, null, value); } // marks the current transaction as successful db.setTransactionSuccessful(); } catch (Exception e) { // process it e.printStackTrace(); } finally { // end a transaction db.endTransaction(); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public void insertWithOneTransaction() {

    SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();

    // Begins a transaction

    db.beginTransaction();

    try {

        // your sqls

        for (int i = 0; i < 100; i++) {

            db.insert(yourTableName, null, value);

        }

 

        // marks the current transaction as successful

        db.setTransactionSuccessful();

    } catch (Exception e) {

        // process it

        e.printStackTrace();

    } finally {

        // end a transaction

        db.endTransaction();

    }

}

其中sqliteOpenHelper.getWritableDatabase()表示得到写表权限。

 

3、其他優化
(1) 語句的拼接使用StringBuilder代替String
這個就不多說了,簡單的string相加會導致創建多個臨時對象消耗性能。StringBuilder的空間預分配性能好得多。如果你對字符串的長度有大致了解,如100字符左右,可以直接new StringBuilder(128)指定初始大小,減少空間不夠時的再次分配。

(2) 讀寫表

在寫表時調用sqliteOpenHelper..getWritableDatabase(),在讀表時候調用sqliteOpenHelper..getReadableDatabase(),getReadableDatabase性能更優。

(3) 查詢時返回更少的結果集及更少的字段。
查詢時只取需要的字段和結果集,更多的結果集會消耗更多的時間及內存,更多的字段會導致更多的內存消耗。

(4) 少用cursor.getColumnIndex

根據性能調優過程中的觀察cursor.getColumnIndex的時間消耗跟cursor.getInt相差無幾。可以在建表的時候用static變量記住某列的index,直接調用相應index而不是每次查詢。

Java

public static final String HTTP_RESPONSE_TABLE_ID = android.provider.BaseColumns._ID; public static final String HTTP_RESPONSE_TABLE_RESPONSE = "response"; public List<Object> getData() { …… cursor.getString(cursor.getColumnIndex(HTTP_RESPONSE_TABLE_RESPONSE)); …… }

1

2

3

4

5

6

7

public static final String       HTTP_RESPONSE_TABLE_ID                  = android.provider.BaseColumns._ID;

public static final String       HTTP_RESPONSE_TABLE_RESPONSE            = "response";

public List<Object> getData() {

    ……

    cursor.getString(cursor.getColumnIndex(HTTP_RESPONSE_TABLE_RESPONSE));

    ……

}

优化为

Java

public static final String HTTP_RESPONSE_TABLE_ID = android.provider.BaseColumns._ID; public static final String HTTP_RESPONSE_TABLE_RESPONSE = "response"; public static final int HTTP_RESPONSE_TABLE_ID_INDEX = 0; public static final int HTTP_RESPONSE_TABLE_URL_INDEX = 1; public List<Object> getData() { …… cursor.getString(HTTP_RESPONSE_TABLE_RESPONSE_INDEX); …… }

1

2

3

4

5

6

7

8

9

public static final String       HTTP_RESPONSE_TABLE_ID                  = android.provider.BaseColumns._ID;

public static final String       HTTP_RESPONSE_TABLE_RESPONSE            = "response";

public static final int          HTTP_RESPONSE_TABLE_ID_INDEX            = 0;

public static final int          HTTP_RESPONSE_TABLE_URL_INDEX           = 1;

public List<Object> getData() {

    ……

    cursor.getString(HTTP_RESPONSE_TABLE_RESPONSE_INDEX);

    ……

}

 

4、異步線程
Sqlite是常用於嵌入式開發中的關系型數據庫,完全開源。
與Web常用的數據庫Mysql、Oracle db、sql server不同,Sqlite是一個內嵌式的數據庫,數據庫服務器就在你的程序中,無需網絡配置和管理,數據庫服務器端和客戶端運行在同一進程內,減少了網絡訪問的消耗,簡化了數據庫管理。不過Sqlite在並發、數據庫大小、網絡方面存在局限性,並且為表級鎖,所以也沒必要多線程操作。

Android中數據不多時表查詢可能耗時不多,不會導致anr,不過大於100ms時同樣會讓用戶感覺到延時和卡頓,可以放在線程中運行,但sqlite在並發方面存在局限,多線程控制較麻煩,這時候可使用單線程池,在任務中執行db操作,通過handler返回結果和ui線程交互,既不會影響UI線程,同時也能防止並發帶來的異常。

可使用Android提供的AsyncQueryHandler(感謝@內網無法登陸賬號 反饋)或類似如下代碼完成:

Java

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); singleThreadExecutor.execute(new Runnable() { @Override public void run() { // db operetions, u can use handler to send message after db.insert(yourTableName, null, value); handler.sendEmptyMessage(xx); } });

1

2

3

4

5

6

7

8

9

10

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

singleThreadExecutor.execute(new Runnable() {

 

    @Override

    public void run() {

        // db operetions, u can use handler to send message after

        db.insert(yourTableName, null, value);

        handler.sendEmptyMessage(xx);

    }

});

延伸阅读

    评论