Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
者:Mino
原因:html代碼下載到WebView后,webkit開(kāi)始解析網(wǎng)頁(yè)各個(gè)節(jié)點(diǎn),發(fā)現(xiàn)有外部樣式文件或者外部腳本文件時(shí),會(huì)異步發(fā)起網(wǎng)絡(luò)請(qǐng)求下載文件,但如果在這之前也有解析到image節(jié)點(diǎn),那勢(shì)必也會(huì)發(fā)起網(wǎng)絡(luò)請(qǐng)求下載相應(yīng)的圖片。在網(wǎng)絡(luò)情況較差的情況下,過(guò)多的網(wǎng)絡(luò)請(qǐng)求就會(huì)造成帶寬緊張,影響到css或js文件加載完成的時(shí)間,造成頁(yè)面空白loading過(guò)久。
解決方法:告訴WebView先不要自動(dòng)加載圖片,等頁(yè)面finish后再發(fā)起圖片加載。
//設(shè)置是否開(kāi)啟密碼保存功能,不建議開(kāi)啟,默認(rèn)已經(jīng)做了處理,存在盜取密碼的危險(xiǎn)
WebView.setSavePassword(false);
原因:4.0以上的系統(tǒng)我們開(kāi)啟硬件加速后,WebView渲染頁(yè)面更加快速,拖動(dòng)也更加順滑。但有個(gè)副作用就是,當(dāng)WebView視圖被整體遮住一塊,然后突然恢復(fù)時(shí)(比如使用SlideMenu將WebView從側(cè)邊滑出來(lái)時(shí)),這個(gè)過(guò)渡期會(huì)出現(xiàn)白塊同時(shí)界面閃爍。
解決方法:是在過(guò)渡期前將WebView的硬件加速臨時(shí)關(guān)閉,過(guò)渡期后再開(kāi)啟。
/**
* 請(qǐng)求網(wǎng)絡(luò)出現(xiàn)error
* @param view view
* @param errorCode 錯(cuò)誤
* @param description description
* @param failingUrl 失敗鏈接
*/
@Override
public void onReceivedError(WebView view, int errorCode, String description, String
failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
if (errorCode==404) {
//用javascript隱藏系統(tǒng)定義的404頁(yè)面信息
String data="Page NO FOUND!";
view.loadUrl("javascript:document.body.innerHTML=\"" + data + "\"");
} else {
if (webListener!=null){
webListener.showErrorView();
}
}
}
// 向主機(jī)應(yīng)用程序報(bào)告Web資源加載錯(cuò)誤。這些錯(cuò)誤通常表明無(wú)法連接到服務(wù)器。
// 值得注意的是,不同的是過(guò)時(shí)的版本的回調(diào),新的版本將被稱為任何資源(iframe,圖像等)
// 不僅為主頁(yè)。因此,建議在回調(diào)過(guò)程中執(zhí)行最低要求的工作。
// 6.0 之后
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.M) {
X5WebUtils.log("服務(wù)器異常"+error.getDescription().toString());
}
//ToastUtils.showToast("服務(wù)器異常6.0之后");
//當(dāng)加載錯(cuò)誤時(shí),就讓它加載本地錯(cuò)誤網(wǎng)頁(yè)文件
//mWebView.loadUrl("file:///android_asset/errorpage/error.html");
if (webListener!=null){
webListener.showErrorView();
}
}
/**
* 這個(gè)方法主要是監(jiān)聽(tīng)標(biāo)題變化操作的
* @param view view
* @param title 標(biāo)題
*/
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
if (title.contains("404") || title.contains("網(wǎng)頁(yè)無(wú)法打開(kāi)")){
if (webListener!=null){
webListener.showErrorView();
}
} else {
// 設(shè)置title
}
}
原因:WebView.loadUrl("url") 不會(huì)立馬就回調(diào) onPageStarted 或者 onProgressChanged 因?yàn)樵谶@一時(shí)間段,WebView 有可能在初始化內(nèi)核,也有可能在與服務(wù)器建立連接,這個(gè)時(shí)間段容易出現(xiàn)白屏,白屏用戶體驗(yàn)是很糟糕的。
解決方法:提前顯示進(jìn)度條雖然不是提升性能 , 但是對(duì)用戶體驗(yàn)來(lái)說(shuō)也是很重要的一點(diǎn)。
/**
* 在加載資源時(shí)通知主機(jī)應(yīng)用程序發(fā)生SSL錯(cuò)誤
* 作用:處理https請(qǐng)求
* @param view view
* @param handler handler
* @param error error
*/
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
if (error!=null){
String url=error.getUrl();
}
//https忽略證書問(wèn)題
if (handler!=null){
//表示等待證書響應(yīng)
handler.proceed();
// handler.cancel(); //表示掛起連接,為默認(rèn)方式
// handler.handleMessage(null); //可做其他處理
}
}
原因:WebView 默認(rèn)開(kāi)啟密碼保存功能 mWebView.setSavePassword(true),如果該功能未關(guān)閉,在用戶輸入密碼時(shí),會(huì)彈出提示框,詢問(wèn)用戶是否保存密碼,如果選擇”是”,密碼會(huì)被明文保到 /data/data/com.package.name/databases/webview.db 中,這樣就有被盜取密碼的危險(xiǎn)。
解決方法:通過(guò) WebSettings.setSavePassword(false) 關(guān)閉密碼保存提醒功能。
@Override
protected void onDestroy() {
try {
//有音頻播放的web頁(yè)面的銷毀邏輯
//在關(guān)閉了Activity時(shí),如果Webview的音樂(lè)或視頻,還在播放。就必須銷毀Webview
//但是注意:webview調(diào)用destory時(shí),webview仍綁定在Activity上
//這是由于自定義webview構(gòu)建時(shí)傳入了該Activity的context對(duì)象
//因此需要先從父容器中移除webview,然后再銷毀webview:
if (webView !=null) {
ViewGroup parent=(ViewGroup) webView.getParent();
if (parent !=null) {
parent.removeView(webView);
}
webView.removeAllViews();
webView.destroy();
webView=null;
}
} catch (Exception e) {
}
super.onDestroy();
}
原因:當(dāng)WebView加載頁(yè)面出錯(cuò)時(shí)(一般為404 NOT FOUND,Android WebView會(huì)默認(rèn)顯示一個(gè)出錯(cuò)界面。當(dāng)WebView加載出錯(cuò)時(shí),會(huì)在WebViewClient實(shí)例中的onReceivedError(),還有onReceivedTitle方法接收到錯(cuò)誤。
解決方法:自定義錯(cuò)誤頁(yè)面樣式。
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
String host=Uri.parse(url).getHost();
if (!BuildConfig.IS_DEBUG) {
if (Arrays.binarySearch(domainList, host) < 0) {
//不在白名單內(nèi),非法網(wǎng)址,這個(gè)時(shí)候給用戶強(qiáng)烈而明顯的提示
} else {
//合法網(wǎng)址
}
}
}
原因:webView加載一些別人的url時(shí)候,有時(shí)候會(huì)發(fā)生證書認(rèn)證錯(cuò)誤的情況。
解決方法:要將正常的呈現(xiàn)頁(yè)面給用戶,我們需要忽略證書錯(cuò)誤,需要調(diào)用WebViewClient類的onReceivedSslError方法,調(diào)用handler.proceed()來(lái)忽略該證書錯(cuò)誤。
//在onResume里面設(shè)置setJavaScriptEnabled(true)。
@Override
protected void onResume() {
super.onResume();
if (mWebView !=null) {
mWebView.getSettings().setJavaScriptEnabled(true);
}
}
//在onStop里面設(shè)置setJavaScriptEnabled(false);
@Override
protected void onStop() {
super.onStop();
if (mWebView !=null) {
mWebView.getSettings().setJavaScriptEnabled(false)
}
}
原因:WebView頁(yè)面中播放了音頻,退出Activity后音頻仍然在播放。
解決方法:需要在Activity的onDestory()中從父容器中移除WebView。
@Override
protected void onDestroy() {
try {
//有音頻播放的web頁(yè)面的銷毀邏輯
//在關(guān)閉了Activity時(shí),如果Webview的音樂(lè)或視頻,還在播放。就必須銷毀Webview
//但是注意:webview調(diào)用destory時(shí),webview仍綁定在Activity上
//這是由于自定義webview構(gòu)建時(shí)傳入了該Activity的context對(duì)象
//因此需要先從父容器中移除webview,然后再銷毀webview:
if (webView !=null) {
ViewGroup parent=(ViewGroup) webView.getParent();
if (parent !=null) {
parent.removeView(webView);
}
webView.removeAllViews();
webView.destroy();
webView=null;
}
} catch (Exception e) {
}
super.onDestroy();
}
原因:客戶端內(nèi)的WebView都是可以通過(guò)客戶端的某個(gè)schema打開(kāi)的,而要打開(kāi)頁(yè)面的URL很多都并不寫在客戶端內(nèi),而是可以由URL中的參數(shù)傳遞過(guò)去的。上面4.0.5 使用scheme協(xié)議打開(kāi)鏈接風(fēng)險(xiǎn)已經(jīng)說(shuō)明了scheme使用的危險(xiǎn)性。
解決方法:設(shè)置運(yùn)行訪問(wèn)的白名單,或者當(dāng)用戶打開(kāi)外部鏈接前給用戶強(qiáng)烈而明顯的提示。設(shè)置白名單操作其實(shí)和過(guò)濾廣告是一個(gè)意思,這里你可以放一些合法的網(wǎng)址允許訪問(wèn)。
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
String host=Uri.parse(url).getHost();
if (!BuildConfig.IS_DEBUG) {
if (Arrays.binarySearch(domainList, host) < 0) {
//不在白名單內(nèi),非法網(wǎng)址,這個(gè)時(shí)候給用戶強(qiáng)烈而明顯的提示
} else {
//合法網(wǎng)址
}
}
}
原因:有些手機(jī)你如果webView加載的html里,有一些js一直在執(zhí)行比如動(dòng)畫之類的東西,如果此刻webView 掛在了后臺(tái)這些資源是不會(huì)被釋放用戶也無(wú)法感知。導(dǎo)致一直占有cpu 耗電特別快。
解決方法:WebView在后臺(tái)的時(shí)候,會(huì)調(diào)用onStop方法,即此時(shí)關(guān)閉js交互,回到前臺(tái)調(diào)用onResume再開(kāi)啟js交互。
//在onResume里面設(shè)置setJavaScriptEnabled(true)。
@Override
protected void onResume() {
super.onResume();
if (mWebView !=null) {
mWebView.getSettings().setJavaScriptEnabled(true);
}
}
//在onStop里面設(shè)置setJavaScriptEnabled(false);
@Override
protected void onStop() {
super.onStop();
if (mWebView !=null) {
mWebView.getSettings().setJavaScriptEnabled(false)
}
}
原因:WebView從Lollipop(5.0)開(kāi)始webView默認(rèn)不允許混合模式, https當(dāng)中不能加載http資源, 而開(kāi)發(fā)的時(shí)候可能使用的是https的鏈接,但是鏈接中的圖片可能是http的,所以顯示圖片失敗。
解決方案:需要設(shè)置開(kāi)啟。
WebView與js的交互包含兩方面,一是在html中通過(guò)js調(diào)用java代碼;二是在安卓java代碼中調(diào)用js。
一、html中通過(guò)js調(diào)用java代碼
js中調(diào)用java代碼其實(shí)就記住一點(diǎn),WebView設(shè)置一個(gè)和js交互的接口(這里的接口是一般的意思,不是java中接口的含義),這個(gè)接口其實(shí)就是一個(gè)一般的類,同時(shí)為這個(gè)接口取一個(gè)別名。這個(gè)過(guò)程如下:
mWebView.addJavaScriptInterface(new DemoJavaScriptInterface(),"demo");
new DemoJavaScriptInterface()就是這個(gè)接口,demo就是這個(gè)接口的別名。
上面的代碼執(zhí)行后在html中js就能通過(guò)別名(這里是“demo”)來(lái)調(diào)用DemoJavaScriptInterface類中的任何方法了。
如果我們想讓html中的一個(gè)button點(diǎn)擊之后調(diào)用java中的函數(shù)可以這樣:
<input type="button" value="click me" onclick="window.demo.clickOnAndroid()"/>
但是因?yàn)榘踩珕?wèn)題,在Android4.2中(如果應(yīng)用的android:targetSdkVersion為17+)JS只能訪問(wèn)帶有@javaScriptInterface注解的java函數(shù),所以開(kāi)發(fā)版本較高的時(shí)候,在需要被調(diào)用的函數(shù)前加上這個(gè)注解。4.2以下為了安全盡量不要調(diào)用addJavascriptInterface,需要另謀他法。
下面是google官方給的實(shí)例:
WebViewDemo.java
[代碼]java代碼:
01020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 | package com.google.android.webviewdemo;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.util.Log;import android.webkit.JsResult;import android.webkit.WebChromeClient;import android.webkit.WebSettings;import android.webkit.WebView;/** * Demonstrates how to embed a WebView in your activity. Also demonstrates how * to have javascript in the WebView call into the activity, and how the activity * can invoke javascript. * <p> * In this example, clicking on the android in the WebView will result in a call into * the activities code in {@link DemoJavaScriptInterface#clickOnAndroid()}. This code * will turn around and invoke javascript using the {@link WebView#loadUrl(String)} * method. * </p><p> * Obviously all of this could have been accomplished without calling into the activity * and then back into javascript, but this code is intended to show how to set up the * code paths for this sort of communication. * */public class WebViewDemo extends Activity { private static final String LOG_TAG="WebViewDemo"; private WebView mWebView; private Handler mHandler=new Handler(); @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); mWebView=(WebView) findViewById(R.id.webview); WebSettings webSettings=mWebView.getSettings(); webSettings.setSavePassword(false); webSettings.setSaveFormData(false); webSettings.setJavaScriptEnabled(true); webSettings.setSupportZoom(false); mWebView.setWebChromeClient(new MyWebChromeClient()); mWebView.addJavascriptInterface(new DemoJavaScriptInterface(), "demo"); mWebView.loadUrl("file:///android_asset/demo.html"); } final class DemoJavaScriptInterface { DemoJavaScriptInterface() { } /** * This is not called on the UI thread. Post a runnable to invoke * loadUrl on the UI thread. */ public void clickOnAndroid() { mHandler.post(new Runnable() { public void run() { mWebView.loadUrl("javascript:wave()"); } }); } } /** * Provides a hook for calling "alert" from javascript. Useful for * debugging your javascript. */ final class MyWebChromeClient extends WebChromeClient { @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { Log.d(LOG_TAG, message); result.confirm(); return true; } }}</p> |
demo.html
[代碼]xml代碼:
0102030405060708091011121314151617181920212223 | <html> <script language="javascript"> /* This function is invoked by the activity */ function wave() { alert("1"); document.getElementById("droid").src="android_waving.png"; alert("2"); } </script> <body> <!-- Calls into the javascript interface for the activity --> <a onClick="window.demo.clickOnAndroid()"> <div style="width:80px; margin:0px auto; padding:10px; text-align:center; border:2px solid #202020;" > <img id="droid" src="android_normal.png"/><br> Click me! </div> </a> </body></html> |
main.xml
[代碼]xml代碼:
?
01020304050607080910111213141516171819202122 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/intro" android:padding="4dip" android:textSize="16sp" /> <WebView android:id="@+id/webview" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" /></LinearLayout> |
Android調(diào)用js
調(diào)用webview頁(yè)面內(nèi)的js方法,調(diào)用形式:
mWebView.loadUrl("javascript:wave()");
其中wave()是js中的一個(gè)方法,當(dāng)然你可以把這個(gè)方法改成其他的方法,也就是android調(diào)用其他的方法。但java不能直接獲取Js方法的返回結(jié)果。
小結(jié)
具體交互流程如下:
點(diǎn)擊圖片,則在js端直接調(diào)用android上的方法clickOnAndroid();
clickOnAndroid()方法(利用線程)調(diào)用js的方法。
被調(diào)用的js直接控制html。
利用webView的這種方式在有些時(shí)候UI布局就可以轉(zhuǎn)成相應(yīng)的html代碼編寫了,而html布局樣式之類有DW這樣強(qiáng)大的工具,而且網(wǎng)上很多源碼,很多代碼片。在UI和視覺(jué)效果上就會(huì)節(jié)省很多時(shí)間,重復(fù)發(fā)明輪子沒(méi)有任何意義。
交互結(jié)果
Android Webview中Java調(diào)用Js方法很容易,loadUrl("javascript:isOk()")就可以調(diào)用isOk這個(gè)Js方法,但不能直接獲取Js方法的返回結(jié)果。
1.傳統(tǒng)的方法中,Js獲取Java信息可以采用如下方式:
[代碼]java代碼:
?
1234567 | class JsObject { @JavascriptInterface public String toString() { return "injectedObject"; } }webView.addJavascriptInterface(new JsObject(), "injectedObject");webView.loadData("", "text/html", null);webView.loadUrl("javascript:alert(injectedObject.toString())"); |
2.Java獲取Js信息(如通過(guò)Js獲取網(wǎng)頁(yè)源代碼)可以這樣:
[代碼]java代碼:
01020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546 | import android.app.Activity;import android.graphics.Bitmap;import android.os.Bundle;import android.util.Log;import android.webkit.WebView;import android.webkit.WebViewClient;public class HtmlSource extends Activity { private WebView webView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); webView=(WebView)findViewById(R.id.webview); webView.getSettings().setJavaScriptEnabled(true); webView.addJavascriptInterface(new InJavaScriptLocalObj(), "local_obj"); webView.setWebViewClient(new MyWebViewClient()); webView.loadUrl("http://www.cnblogs.com/hibraincol/"); } final class MyWebViewClient extends WebViewClient{ public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } public void onPageStarted(WebView view, String url, Bitmap favicon) { Log.d("WebView","onPageStarted"); super.onPageStarted(view, url, favicon); } public void onPageFinished(WebView view, String url) { Log.d("WebView","onPageFinished "); view.loadUrl("javascript:window.local_obj.showSource(''+" + "document.getElementsByTagName('html')[0].innerHTML+'');"); super.onPageFinished(view, url); } } final class InJavaScriptLocalObj { public void showSource(String html) { Log.d("HTML", html); } }} |
當(dāng)網(wǎng)頁(yè)中有超鏈接跳轉(zhuǎn)時(shí),將會(huì)調(diào)用WebClient的shouldOverrideUrlLoading方法,若設(shè)置 WebViewClient 且該方法返回 true,則說(shuō)明由應(yīng)用的代碼處理該 url,WebView 不處理,就可以達(dá)到攔截跳轉(zhuǎn)的效果。
耗優(yōu)化是應(yīng)用體驗(yàn)優(yōu)化的一個(gè)重要課題,高功耗會(huì)引發(fā)用戶的電量焦慮,也會(huì)導(dǎo)致糟糕的發(fā)熱體驗(yàn),從而降低了用戶的使用意愿。而功耗又是涉及整機(jī)的長(zhǎng)時(shí)間多場(chǎng)景的綜合性復(fù)雜指標(biāo),影響因素很多。不論是功耗的量化拆解,還是異常問(wèn)題的監(jiān)控,以及主動(dòng)的功耗優(yōu)化對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)都是很有挑戰(zhàn)性的。
本文結(jié)合抖音的功耗優(yōu)化實(shí)踐中產(chǎn)出了一些實(shí)驗(yàn)結(jié)論,優(yōu)化思路,從功耗的基礎(chǔ)知識(shí),功耗組成,功耗分析,功耗優(yōu)化等幾個(gè)方面,對(duì) Android 應(yīng)用的功耗優(yōu)化做一個(gè)總結(jié)沉淀。
首先我們回顧一下功耗的概念,這里比較容易和能耗搞混。解釋一下為什么手機(jī)上用mA(電流值)來(lái)表征功耗水平,用 mAh(物理意義上是電荷值)來(lái)表征能耗水平。我們先來(lái)看幾個(gè)物理公式。
P=I × U, E=P × T
能耗(E):即能量損耗,指計(jì)算機(jī)系統(tǒng)一段時(shí)間內(nèi)總的能量消耗,單位是焦耳(J)
功耗(P):即功率損耗,指單位時(shí)間內(nèi)的能量消耗,反映消耗能量的速率,單位是瓦特(W)
電流(I):指手機(jī)電池放電的電流值,手機(jī)常用 mA 為單位
電壓(U):指手機(jī)電池放電的電壓值,標(biāo)準(zhǔn)放電電壓 3.7V,充電截止電壓 4.35V,放電截止電壓 2.75V(以典型值舉例,不同設(shè)備的電池電壓數(shù)值有差異)
電池容量 :常用單位 mAh,從單位意義上看是電荷數(shù),實(shí)際表征的是電池以典型電壓放電的時(shí)長(zhǎng)。
如下面的功耗測(cè)試圖所示,手機(jī)通常以恒定的典型電壓工作,為了計(jì)算方便,就把電壓恒定為 3.7V,那么 P=I × 3.7, E=I × 3.7 × T,即用 mA 表征功耗,mAh 表征能耗。
總結(jié):對(duì)同一機(jī)型,我們用電池容量(mAh)變化的來(lái)表征一段時(shí)間總能耗,用平均電流(mA)來(lái)表征功耗水平;如 4000mAh 電池的手機(jī)刷抖音 1 小時(shí)耗電 11%,耗電量(能耗)440mAh,平均電流 440mA
圖 1. 功耗測(cè)試圖
從摘要里我們已經(jīng)了解到高功耗會(huì)引發(fā)用戶的電量焦慮,也會(huì)導(dǎo)致糟糕的發(fā)熱體驗(yàn),從而降低了用戶的使用意愿。優(yōu)化功耗除了可以我們帶來(lái)更好的用戶體驗(yàn),提升用戶使用時(shí)長(zhǎng)外,降低應(yīng)用耗電還具有很明顯的社會(huì)價(jià)值,用一個(gè)當(dāng)前比較火的詞,就是可以為碳中和事業(yè)貢獻(xiàn)一份力量。
不同于 Crash、ANR 等常見(jiàn)的 APM 指標(biāo),功耗是一個(gè)綜合性的課題,分析起來(lái)很容易讓人無(wú)從下手。用戶反饋了耗電問(wèn)題,可能是 CPU 出現(xiàn)高負(fù)載,又或者是后臺(tái)頻繁的網(wǎng)絡(luò)訪問(wèn),也可能是動(dòng)畫泄漏導(dǎo)致高功耗。或者我們自己的業(yè)務(wù)沒(méi)什么變化,單純就是環(huán)境因素影響,導(dǎo)致用戶覺(jué)得耗電,比如低溫導(dǎo)致的鋰電池放電衰減。
我們的思路是從器件出發(fā),應(yīng)用的耗電最終都可以分解為手機(jī)器件的耗電,所以我們先對(duì)抖音做器件耗電的拆解,看主要耗電的是哪些器件,再看如何減少器件的使用,這樣就做到有的放矢。
下面我們先從功耗組成,功耗分析,以及功耗優(yōu)化等方面來(lái)講述如何開(kāi)展功耗優(yōu)化。
這里列舉了手機(jī)硬件的基本形態(tài),每個(gè)模塊又是由復(fù)雜的器件構(gòu)成。如我們常說(shuō)的耗電大頭 SoC 里就包含 CPU 的超大核,大核,小核,GPU,DDRC(內(nèi)存接口),以及外設(shè)區(qū)的各種小 IP 核等。所以整機(jī)的功耗最終就可以拆解為各個(gè)器件的功耗,而應(yīng)用的功耗就是計(jì)算其使用的器件產(chǎn)生的功耗。
以抖音的 Feed 流場(chǎng)景為例,亮度固定 120nit、7 格音量、WiFi 網(wǎng)絡(luò)下,我們對(duì)抖音做了器件級(jí)的功耗拆解。可以看到抖音的 feed 功耗主要集中在 SOC(CPU,GPU,DDR),Display,Audio,WIFI 等四個(gè)模塊。
那這些器件功耗是如何被拆解出來(lái)的呢?原理是:先對(duì)器件進(jìn)行耗電因子拆解,建立器件功耗模型,得到一個(gè)器件耗電的計(jì)算公式。通過(guò)運(yùn)行時(shí)統(tǒng)計(jì)器件的使用數(shù)據(jù),代入功耗模型,就可以計(jì)算出器件的功耗。應(yīng)用的功耗則是從器件的總功耗里按應(yīng)用使用的比較進(jìn)行分配,這樣就得到了應(yīng)用的器件耗電。由于影響器件功耗的耗電因子眾多,這里復(fù)雜的就是如何對(duì)耗電因子進(jìn)行拆解以及建模。有了精準(zhǔn)的建模,后面就是廠商適配校準(zhǔn)參數(shù)的過(guò)程了。
谷歌提供了一套通用的器件耗電模型和配置方案,OEM 廠商可以按通用方案對(duì)自己的產(chǎn)品進(jìn)行參數(shù)校準(zhǔn)和配置。如下圖里 AOSP 里的耗電配置里,以 Wifi 的耗電計(jì)算為例。https://source.android.com/devices/tech/power/values
谷歌提供的建模方案是對(duì) WIFI 分狀態(tài)計(jì)算耗電,WIFI 不同狀態(tài)下的耗電差異非常明顯。這里分為了 wifi.on(對(duì)應(yīng) wifi 打開(kāi)的基準(zhǔn)電流), wifi.active(對(duì)應(yīng) wifi 傳輸數(shù)據(jù)時(shí)的基準(zhǔn)電流), wifi.scan(對(duì)應(yīng) wifi 單次掃描的基準(zhǔn)耗電), wifi 數(shù)據(jù)傳輸?shù)暮碾?controller.rx,controller.tx, controller.idle)。根據(jù) wifi 收發(fā)數(shù)據(jù)的那計(jì)算 wifi 的耗電,通過(guò)統(tǒng)計(jì)這幾個(gè)狀態(tài)的時(shí)長(zhǎng)或次數(shù),乘以對(duì)應(yīng)的電流,就得到 wifi 器件的耗電了。
由于谷歌是按照通用性來(lái)設(shè)計(jì)的器件耗電模型,通常只能大致計(jì)算出器件的耗電水平,具體到某個(gè)產(chǎn)品上可能誤差很大。各 OEM 廠商通常有基于自身硬件的耗電統(tǒng)計(jì)方案,可以對(duì)耗電做更加精細(xì)準(zhǔn)確的計(jì)算。這里還用 wifi 舉例:如 OEM 廠商可以分別按照 2.4G,5GWIFI 單獨(dú)建模,并引入天線信號(hào)的變化對(duì)應(yīng)的基準(zhǔn)電流變化,以及統(tǒng)計(jì) wifi 芯片所工作的頻點(diǎn)時(shí)長(zhǎng),按頻點(diǎn)細(xì)化模型等等,OEM 廠商可以設(shè)計(jì)出更符合自己設(shè)備的精準(zhǔn)功耗模型,計(jì)算出更精準(zhǔn)的 wifi 耗電。這就要根據(jù)具體產(chǎn)品的硬件方案來(lái)確定了。
通過(guò)上面的功耗組成的介紹,我們可以看到功耗影響因素是多種多樣。在做應(yīng)用功耗分析時(shí),我們既要有方法準(zhǔn)確評(píng)估應(yīng)用的耗電水平,又要有方法來(lái)分解出耗電的組成,以找到優(yōu)化點(diǎn)。下面就分為功耗評(píng)估和功耗歸因分析這兩部分來(lái)介紹。
如前文功耗基礎(chǔ)知識(shí)里所說(shuō),我們使用電流值來(lái)評(píng)估應(yīng)用的功耗水平。在線下場(chǎng)景,我們通過(guò)控制測(cè)試條件(如固定測(cè)試機(jī)型版本,清理后臺(tái),固定亮度,音量,穩(wěn)定的網(wǎng)絡(luò)信號(hào)條件等)來(lái)測(cè)得可信的準(zhǔn)確電流值來(lái)評(píng)估應(yīng)用的前后臺(tái)功耗。在線上場(chǎng)景,由于應(yīng)用退后臺(tái)時(shí),用戶使用場(chǎng)景的復(fù)雜性(指用戶運(yùn)行的前臺(tái)應(yīng)用不同),我們只采集前臺(tái)整機(jī)電流來(lái)做線上版本監(jiān)控,使用其他指標(biāo),如后臺(tái) CPU 使用率來(lái)監(jiān)控后臺(tái)功耗。下面我們介紹一些常用功耗評(píng)估的手段。
目前業(yè)界最通用的整機(jī)耗電評(píng)估方式是通過(guò) PowerMonitor 外接電量計(jì)的方式,高頻率高精度采集電流進(jìn)行評(píng)估。常用需要精細(xì)化確認(rèn)耗電情況,尤其是后臺(tái)靜置,滅屏等狀態(tài)下的電流輸出,廠商的準(zhǔn)入測(cè)試等。常用的 Mosoon 公司的 PowerMonitorAAA10F,電流量程在 1uA ~ 6A 之間,電流精度 50uA,采樣周期 200us (5KHZ)。
PowerMonitor 雖然測(cè)量結(jié)果最準(zhǔn)確。但是需要拆機(jī)比較麻煩。我們還可以通過(guò)谷歌 BatteryManager 提供的接口直接讀取電池電量計(jì)的統(tǒng)計(jì)結(jié)果來(lái)獲得電流值。
電池電量計(jì)負(fù)責(zé)估計(jì)電池容量。其基本功能為監(jiān)測(cè)電壓,充電/放電電流和電池溫度,并估計(jì)電池荷電狀態(tài)(SOC)及電池的完全充電容量(FCC)。有兩種典型的電量計(jì):電壓型電量計(jì)和電流型電量計(jì),目前手機(jī)上使用的電量計(jì)主要是電流型電量計(jì)。
Android 提供了 BMS 的接口,通過(guò)屬性提供了電池電量計(jì)的統(tǒng)計(jì)結(jié)果
import android.os.BatteryManager;
import android.content.Context;
BatteryManager mBatteryManager=(BatteryManager)Context.getSystemService(Context.BATTERY_SERVICE);
Long energy=mBatteryManager.getLongProperty(BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER);
Slog.i(TAG, "Remaining energy=" + energy + "nWh");
以下面的 Nexus9 為例,該機(jī)型使用了 MAX17050 電流型電量計(jì),解析度 156.25uA,更新周期 175.8ms。
從實(shí)踐結(jié)果上看,由于不同的手機(jī)使用的電量計(jì)不同,導(dǎo)致直接讀取出來(lái)的電流值單位也不同,需要做數(shù)據(jù)轉(zhuǎn)化。為了簡(jiǎn)化電池?cái)?shù)據(jù)的獲取,我們開(kāi)發(fā)了 Thor SDK,只保留電流、電壓、電量等指標(biāo)的采集過(guò)程,針對(duì)不同機(jī)型做了數(shù)據(jù)歸一處理,用戶可以不用關(guān)心內(nèi)部實(shí)現(xiàn),只需要提供需要采樣的數(shù)據(jù)類型、采樣周期就可以定時(shí)返回所需要的功耗相關(guān)的數(shù)據(jù),我們用 Thor 對(duì)比 PowerMonitor 進(jìn)行了數(shù)據(jù)一致性的校驗(yàn),誤差<5mA,滿足線上監(jiān)控需求。
此外我們做了 Thor 采集功能本身的功耗影響,可以看到 1s 采集 1 次的情況下,平均電流上漲了 0.59mA,所以說(shuō)這種方案的功耗影響非常低,適合線上采集電流值。
廠商提供的耗電排行也可以用來(lái)查看一段時(shí)間內(nèi)的應(yīng)用耗電情況。如下面華為的耗電排行里,對(duì)硬件和軟件耗電進(jìn)行了分拆,并給出了應(yīng)用的具體耗電量。其他廠商 OV 也是支持具體的耗電量,小米則是提供耗電占比,并不會(huì)提供具體耗電量。
入口:設(shè)置->電池->耗電排行
從功耗評(píng)估我們可以判斷應(yīng)用的整體耗電情況,但具體到某個(gè) case 高耗電的原因是什么,就要具體問(wèn)題選擇不同的工具來(lái)進(jìn)行分析了。目前可以直接歸因到業(yè)務(wù)代碼的主要是 CPU 相關(guān)的工具,這也是我們目前分析問(wèn)題的主要方向,后續(xù)我們也會(huì)建設(shè)流量歸因等能力,下面我列舉了常用的分析工具。
谷歌官方提供的分析工具,需要先進(jìn)行功耗測(cè)試,再通過(guò) adb 抓取 bugreport.zip,再通過(guò)網(wǎng)頁(yè)工具打開(kāi),可提供粗粒度的功耗歸因。
本質(zhì)上是對(duì) systemserver 里的各種服務(wù)統(tǒng)計(jì)信息+手機(jī)狀態(tài)+內(nèi)核統(tǒng)計(jì)信息(kernel 喚醒)的展示,應(yīng)用耗電的估算依賴廠商配置的 power_profile.xml。比較適合對(duì)整機(jī)耗電問(wèn)題做耗電歸因,如歸因到某應(yīng)用耗電較高。
對(duì)于單個(gè)應(yīng)用,由于對(duì) wakelock,alarm,gps,job,syncservice,后臺(tái)服務(wù)運(yùn)行時(shí)長(zhǎng)等統(tǒng)計(jì)的比較詳細(xì),比較適合做后臺(tái)耗電的歸因。對(duì)于網(wǎng)絡(luò)異常,CPU 異常,只能看到消耗較多,無(wú)法歸因到具體業(yè)務(wù)。https://developer.android.com/topic/performance/power/setup-battery-historian?hl=zh-cn
相比于 BatteryHistorian 需要先手動(dòng)測(cè)試,再 adb 抓取的操作繁瑣,AS 自帶的 Profiler 提供了 Energy 的可視化展示。使用 debug 版本的應(yīng)用,可以直觀的看到功耗的消耗情況,方便了線下測(cè)試。需要注意的是這里展示的功耗值是通過(guò) GPS+網(wǎng)絡(luò)+CPU 計(jì)算的擬合值,并不是真實(shí)功耗值,只表征功耗水平。
Profiler 同步展示了 CPU 使用率,網(wǎng)絡(luò)耗電,內(nèi)存信息。支持 CPU 和線程級(jí)別的跟蹤。通過(guò)主動(dòng)錄制 Trace,可以分析各線程的 CPU 使用情況,以及耗時(shí)函數(shù)。對(duì)于容易復(fù)現(xiàn)的 CPU 高負(fù)載問(wèn)題或者固定場(chǎng)景的耗時(shí)問(wèn)題,這種方式可以很容易看到根因。但 trace 的展示方式并不適合偶現(xiàn)的 CPU 高負(fù)載,信息量特別多反而讓人難以抓住重點(diǎn)。
網(wǎng)絡(luò)耗電可以很方便抓取到上行下行的網(wǎng)絡(luò)請(qǐng)求,可以展示網(wǎng)絡(luò)請(qǐng)求的 api 細(xì)節(jié),并且劃分到線程上。對(duì)于頻繁的網(wǎng)絡(luò)訪問(wèn),很容易找到問(wèn)題點(diǎn)。但目前只支持通過(guò) HttpURLConnection 和 OkHttp 的網(wǎng)絡(luò)請(qǐng)求,使用其他的網(wǎng)絡(luò)庫(kù),Profiler 追蹤不到。
可以看到官方出品的工具,功能比較完善,但只支持 debug 版本的 app 分析,如果要分析 release 版本的 app,需要使用 root 手機(jī)。總體而言,Profiler 比較適合于線下固定某個(gè)業(yè)務(wù)場(chǎng)景的分析。https://developer.android.com/studio/profile/energy-profiler
使用上面的工具監(jiān)控單個(gè)線程的 CPU 異常是可以的。但是對(duì)于線程池,Handler,AsyncTask 等異步任務(wù)不太容易歸因具體的業(yè)務(wù),尤其是網(wǎng)絡(luò)庫(kù)的線程池,由于執(zhí)行的網(wǎng)絡(luò)請(qǐng)求邏輯是一樣的,只靠抓線程堆棧是不能歸因到具體業(yè)務(wù)的。需要統(tǒng)計(jì)提交任務(wù)的源頭代碼才能抓到真正問(wèn)題點(diǎn)。
我們可以通過(guò)多種機(jī)制,如改造線程池,java hook 等,對(duì)提交任務(wù)方進(jìn)行了詳細(xì)記錄和聚合,可以幫忙我們分析線程池里的耗時(shí)任務(wù)。
除了線下的 CPU 分析,我們?cè)谶M(jìn)行線上 CPU 異常監(jiān)控的建設(shè)時(shí),我們考慮到單純使用 CPU 使用率閾值不能精準(zhǔn)的判斷進(jìn)程是否處于 CPU 異常。比如不同的 CPU 型號(hào)本身的性能不同,在某些低端 CPU 上的使用率就是比較高。又比如系統(tǒng)有不同的溫控策略,省電策略,會(huì)對(duì)手機(jī)進(jìn)行限頻,對(duì)任務(wù)進(jìn)行 CPU 核心遷移。在這種情況下,應(yīng)用也會(huì)有更高的 CPU 使用率。
因此我們基于不同的變量因素(如 CPU 型號(hào),進(jìn)程/線程的 CPU 時(shí)長(zhǎng)在不同核,不同頻點(diǎn)的分布,充電,電量,內(nèi)存,網(wǎng)絡(luò)狀態(tài)等),將 CPU 的使用閾值進(jìn)行精細(xì)判定,針對(duì)不同場(chǎng)景、不同設(shè)備、不同業(yè)務(wù)制定精細(xì)化的 CPU 異常閾值,從而實(shí)現(xiàn)了高精度的 CPU 異常抓取。
此外還有業(yè)界的一些歸因框架,在這里不展開(kāi)介紹了。
上面介紹了功耗的組成,以及如何分析我們應(yīng)用的耗電。這里我們對(duì)功耗優(yōu)化做一個(gè)整體性介紹。我們把優(yōu)化思路從器件角度展開(kāi),列舉我們有哪些優(yōu)化的思路和措施,可以減少器件的使用情況,進(jìn)而降低功耗。此外對(duì)于一些用戶可感知的有損業(yè)務(wù)的降級(jí),我們通過(guò)低功耗模式來(lái)做,在低電量時(shí)通過(guò)更激進(jìn)的降級(jí)手段,緩解用戶的電量焦慮,帶來(lái)用戶的使用時(shí)長(zhǎng)的提升。
下圖列舉了各器件上的優(yōu)化思路,有一些優(yōu)化思路會(huì)對(duì)多個(gè)器件都有收益,在這里沒(méi)有特別詳細(xì)的區(qū)分,就劃分在主要影響的器件上,如減少刷新區(qū)域,對(duì) GPU,CPU,DDR 都有收益,主要收益在 GPU 繪制上,在下圖里就列舉在 GPU 上了。
同時(shí)我們列舉了廠商側(cè)的一些優(yōu)化方案,應(yīng)用通常無(wú)需關(guān)注,比如降低屏幕刷新率,TP 掃描頻率,整機(jī)低分辨率等,這種可以通過(guò)廠商合作的方式進(jìn)行更細(xì)致的調(diào)優(yōu),如分場(chǎng)景動(dòng)態(tài)調(diào)整屏幕刷新率,在搜索列表場(chǎng)景使用 90HZ 高刷,在短視頻場(chǎng)景結(jié)合幀率對(duì)齊進(jìn)行刷新率降低為 30HZ,以獲得更平衡的功耗和性能體驗(yàn)。
顯示功耗的優(yōu)化主要圍繞對(duì)屏幕,GPU,CPU,視頻解碼器,TP 等器件降級(jí)使用或者減少處理,盡量使用硬件處理等實(shí)現(xiàn)的。對(duì)于屏幕而言主要是降低亮度,刷新率,TP 掃描頻率等。
屏幕亮度是屏幕功耗的最大來(lái)源,亮度和功耗幾乎是正比的關(guān)系,參見(jiàn)下圖:
可以看出無(wú)論是 IPS 屏幕還是 OLED 屏幕,隨著屏幕亮度增加,功耗幾乎是線性增加。針對(duì) OLED 屏幕則是白色內(nèi)容的功耗更高,深色內(nèi)容則功耗相對(duì)更低。應(yīng)用通用的降低亮度的方式有進(jìn)入應(yīng)用后主動(dòng)降低亮度,或者使用深色的 UI 模式,來(lái)達(dá)到屏幕亮度降低的效果。廠商會(huì)通過(guò) FOSS 或者 CABC 的方案,降低屏幕亮度。
深色模式
利用 AMOLED 屏幕本身的原理,黑色功耗最低,所以可以盡量采用較暗的主題顏色等,最終獲取較低的功耗,可以保持用戶使用時(shí)間更長(zhǎng)。
為什么說(shuō) AMOLED 屏幕顯示黑色界面會(huì)消耗更少的電量呢?這要從它與傳統(tǒng)的 LCD 屏幕之間的發(fā)光原理區(qū)別上來(lái)說(shuō)。
LCD 背光顯示屏,主要是靠背光層,發(fā)光層由大量 LED 燈泡組成,顯示白光,通過(guò)液晶層偏振控制,顯示出 RGB 顏色。在這種情況下,黑色與其它顏色的像素并沒(méi)有什么不同,雖然看起來(lái)并沒(méi)有光亮,但是依然還是處于發(fā)光的狀態(tài)。
AMOLED 屏幕根本就沒(méi)有背光一說(shuō)。相反,每個(gè)小的亞像素只是發(fā)出微弱的 RGB 光,如果屏幕需要顯示黑色,只需要通過(guò)調(diào)整電壓使得液晶分子排列旋轉(zhuǎn)從而遮蔽住背光就可以實(shí)現(xiàn)黑色的效果,不會(huì)額外點(diǎn)亮任何顏色。
下面引用測(cè)試應(yīng)用為 Reddit Sync 的不同場(chǎng)景下彩色和黑色模式功耗對(duì)比。(參考鏈接:https://m.zol.com.cn/article/4895723.html#p4)
從上面的圖表我們可以很清楚的看到,在黑色背景的情況下,AMOLED 屏幕在能耗上的確要比普通顏色背景少了很多,在 Reddit Sync 的測(cè)試中,平均耗電量要降低 40%左右。
應(yīng)用可以設(shè)計(jì)自己的深色模式主題,同步手機(jī)系統(tǒng)深色模式開(kāi)關(guān)的切換。目前抖音背景設(shè)置有兩種模式如下圖,可以看到經(jīng)典模式就是深色模式,正好對(duì)應(yīng)于深色主題,這個(gè)也可以和手機(jī)平臺(tái)的深色模式也結(jié)合起來(lái)。
FOSS
FOSS (Fidelity Optimized Signal Scaling,保真優(yōu)化信號(hào)縮放)是芯片廠商提供的一種對(duì) AMOLED 屏幕調(diào)節(jié)的低功耗方案。LCD 屏幕上對(duì)應(yīng)的是 CABC (Content Adaptive Brightness Control,內(nèi)容適應(yīng)背光控制)。一方面降低屏幕亮度,一方面調(diào)節(jié)顯示內(nèi)容灰度值,從而使顯示效果差異不大,由于降低了屏幕亮度,所以獲取的功耗收益較大。一般大約是 0.2 小時(shí)左右,即平均可延長(zhǎng)手機(jī)使用時(shí)間 0.2 小時(shí)左右。
已知的情況是廠商的 FOSS 方案在某些參數(shù)情況下會(huì)導(dǎo)致個(gè)別場(chǎng)景出現(xiàn)變色或閃爍問(wèn)題。如果遇到未確認(rèn)閃爍問(wèn)題,在內(nèi)部定位無(wú)法確認(rèn)原因時(shí),可以跟廠商咨詢進(jìn)行排除。
目前市面上部分手機(jī)支持 60HZ,90HZ,120HZ,144HZ 等,高的刷新率帶來(lái)了流暢度提高,用戶的體驗(yàn)更好,但是功耗更高。通常來(lái)講在系統(tǒng)應(yīng)用界面比如桌面,設(shè)置,刷新率會(huì)跟當(dāng)前系統(tǒng)設(shè)置保持一致,而在具體應(yīng)用中,刷新率會(huì)根據(jù)不同場(chǎng)景做調(diào)整。比如抖音,即使在高刷屏幕上,平臺(tái)系統(tǒng)一般選擇讓抖音運(yùn)行在 60HZ 刷新率,從而相對(duì)功耗較低。
針對(duì)不同的刷新率,PhoneArena 就做了一個(gè)比較有參考性的數(shù)據(jù)來(lái)驗(yàn)證這個(gè)觀點(diǎn)。他們選取了兩個(gè)品牌四款產(chǎn)品,都是高刷新率的機(jī)型,在同一條件下進(jìn)行 60Hz 刷新率和 120Hz 刷新率的測(cè)試,結(jié)果 120HZ 刷新率下手機(jī)續(xù)航相比 60HZ 下的確縮短了至少 10%,即便是支持 90Hz 的一加 8 也是比 60HZ 刷新率要差。
圖片來(lái)源:https://www.sohu.com/a/394532665_115511
通常游戲中為了提高點(diǎn)擊響應(yīng)速度會(huì)提高 TP 掃描頻率,其他場(chǎng)景都采用默認(rèn)的掃描頻率。抖音一般使用默認(rèn)的 TP 掃描幀率。
GPU 的優(yōu)化思路主要在減少不必要的繪制或者降低繪制面積,這體現(xiàn)在更低的分辨率,更低的幀率,更少的繪制圖層等方面。此外視頻應(yīng)用使用 SurfaceView 替換 TextureView 也有顯著的功耗收益。對(duì)于復(fù)雜的運(yùn)算,我們可以選擇更高能效比的器件來(lái)進(jìn)行,比如使用硬件繪制代替軟件繪制,使用 NPU 代替 GPU 執(zhí)行復(fù)雜算法,對(duì)整體功耗都有明顯降低。
應(yīng)用低分辨率
通常該模式下游戲和特定應(yīng)用一般以較低分辨率運(yùn)行。縮小了 GPU 繪制區(qū)域和傳輸區(qū)域大小,降低了 GPU 和 CPU 以及傳輸 DDR 的功耗。功耗收益在游戲場(chǎng)景下比較大,線下測(cè)試特定平臺(tái)下1080p->720p約20mA左右,1440p->720p約40mA左右。
其原理如下,應(yīng)用圖層在低分辨率下繪制,通過(guò) HWC 通道放大到屏幕分辨率并跟其余圖層合成后送顯。
該功能通常平臺(tái)側(cè)設(shè)置,非游戲應(yīng)用無(wú)需關(guān)注,游戲應(yīng)用可以自己選擇設(shè)置低分辨率。
部分游戲比如騰訊系游戲(如 QQ 飛車、王者榮耀和和平精英等)內(nèi)部也有不同分辨率的設(shè)置,默認(rèn)以低分辨率運(yùn)行,從而可以實(shí)現(xiàn)較低功耗。
整機(jī)低分辨率
所有應(yīng)用都運(yùn)行在低分辨率下。同樣也縮小了 GPU 繪制區(qū)域和傳輸區(qū)域大小,降低了 GPU 和 CPU 以及傳輸 DDR 的功耗。功耗收益跟應(yīng)用低分辨率相同,普通應(yīng)用在該模式下也有功耗收益。用戶從系統(tǒng)設(shè)置菜單中切換,應(yīng)用本身通常無(wú)需關(guān)注。
其原理如下,所有圖層都在低分辨率下繪制,并在低分辨率下進(jìn)行合成。合成后經(jīng)過(guò) scaler 一次性放大到屏幕分辨率,然后進(jìn)行送顯。其中 scaler 是放縮硬件,由芯片平臺(tái)提供。
應(yīng)用布局動(dòng)畫位置相近,布局出來(lái)一個(gè)較小的區(qū)域,繪制區(qū)域最小,刷新區(qū)域最小, 從而功耗最低。不同場(chǎng)景,收益不同。
如下圖兩種情況,可以看到左側(cè)圖,有 3 個(gè)動(dòng)畫區(qū)域(紅色框住區(qū)域),最終形成的 Dirty 區(qū)域?yàn)榇蟮募t框區(qū)域,整個(gè)面積較大。而對(duì)比中間圖,動(dòng)畫兩個(gè)紅色區(qū)域,經(jīng)過(guò)運(yùn)算后形成的 Dirty 大紅框區(qū)域就較小,GPU 的繪制區(qū)域跟刷新的傳輸區(qū)域都較小,從而相對(duì)而言,功耗較低。從最右側(cè)功耗數(shù)據(jù)圖中可以看出收益較大。
可以在開(kāi)發(fā)者選項(xiàng)中打開(kāi):設(shè)置 -> 開(kāi)發(fā)者選項(xiàng) -> 顯示GPU視圖更新,當(dāng)刷新范圍與動(dòng)畫范圍明顯不一致時(shí)便是動(dòng)畫布局不合理。這種情況需要具體到代碼層面分析寫法的問(wèn)題并修改。
通常在游戲或應(yīng)用動(dòng)畫中使用,可以降低 GPU 繪制頻率和后面的刷新頻率。通過(guò)降低動(dòng)畫繪制頻率,可以降低 GPU,CPU 及 DDR 功耗。
不同幀率功耗情況對(duì)比如下,可以看到低幀率下相比高幀率,功耗明顯低了很多。
在抖音應(yīng)用中,低繪制幀率可以通過(guò)在抖音內(nèi)部主動(dòng)降低動(dòng)畫等幀率實(shí)現(xiàn)。在抖音推薦界面音樂(lè)轉(zhuǎn)盤動(dòng)畫和音符動(dòng)畫中降低幀率,可以顯著的降低功耗。此外也可以通過(guò)廠商側(cè)提供 soft vsync 實(shí)現(xiàn) 30HZ 繪制,這部分抖音與廠商合作,SurfaceFlinger 控制 APP vsync,降幀時(shí) SurfaceFlinger vsync 輸出降為 30fps,在特定條件下主動(dòng)降低幀率,以延長(zhǎng)使用時(shí)長(zhǎng)。
在抖音推薦頁(yè)面中,通過(guò)視頻和降低頻率后的動(dòng)畫達(dá)到同步,可以實(shí)現(xiàn)整個(gè)界面以30HZ 繪制和刷新。否則,如果視頻30hz和動(dòng)畫30幀正好交錯(cuò),最終形成的繪制/刷新頻率還是60幀,沒(méi)有達(dá)到最優(yōu)。我們通過(guò)調(diào)節(jié)各種動(dòng)畫的繪制流程,將動(dòng)畫整體繪制對(duì)齊,整體幀率明顯降低。
過(guò)度繪制(Overdraw)描述的是屏幕上的某個(gè)像素在同一幀的時(shí)間內(nèi)被繪制了多次。在多層次重疊的 UI 結(jié)構(gòu)里面,如果不可見(jiàn)的 UI 也在做繪制的操作,會(huì)導(dǎo)致某些像素區(qū)域被繪制了多次,同時(shí)也會(huì)浪費(fèi)大量的 CPU 以及 GPU 資源。
可以通過(guò)如下來(lái)調(diào)試過(guò)度繪制:打開(kāi)手機(jī),設(shè)置 -> 開(kāi)發(fā)者選項(xiàng) -> 調(diào)試 GPU 過(guò)度繪制 -> 顯示 GPU 過(guò)度繪制。過(guò)度繪制的存在會(huì)導(dǎo)致界面顯示時(shí)浪費(fèi)不必要的資源去渲染看不見(jiàn)的背景,或者對(duì)某些像素區(qū)域多次繪制,就會(huì)導(dǎo)致界面加載或者滑動(dòng)時(shí)的不流暢、掉幀,對(duì)于用戶體驗(yàn)來(lái)說(shuō)就是 App 特別的卡頓。為了提升用戶體驗(yàn),提升應(yīng)用的流暢性,優(yōu)化過(guò)度繪制的工作還是很有必要做的。
抖音的 feed 頁(yè)的過(guò)度繪制非常的嚴(yán)重,抖音存在 5 層過(guò)度繪制。下圖左側(cè)是優(yōu)化前的過(guò)渡繪制情況,右側(cè)是優(yōu)化后的過(guò)度繪制情況,可以看出優(yōu)化后明顯改善。
TextureView 和 SurfaceView 是兩個(gè)最常用的播放視頻控件。TextureView 控件位于主圖層上,解碼器將視頻幀傳遞到 TextureView 對(duì)象還需要 GPU 做一次繪制才能在屏幕上顯示,所以其功耗更高,消耗內(nèi)存更大,CPU 占用率也更高。
控件位置差異如下,可以看出 SurfaceView 擁有獨(dú)立的 Surface 位于單獨(dú)的圖層上,而 TextureView 位于主圖層上。
BufferQueue 是 Android 圖形架構(gòu)的核心,其一側(cè)是生產(chǎn)者,另一側(cè)是消費(fèi)者。從這方面看,SurfaceView 和 TextureView 的差異如下。容易看出,SurfaceView 流程更短,內(nèi)存使用更少,也沒(méi)有 GPU 繪制,功耗更省。
下面是一些 SurfaceView 替換 TextureView 后的收益數(shù)據(jù):
硬件繪制是指通過(guò) GPU 繪制,Android 從 3.0 開(kāi)始支持硬件加速繪制,它在 UI 顯示和繪制效率方面遠(yuǎn)高于軟件繪制,但是 GPU 功耗相對(duì)較高。目前是系統(tǒng)默認(rèn)的繪制方式。
軟件繪制是指通過(guò) CPU 實(shí)現(xiàn)繪制,Android 上面使用 Skia 圖形庫(kù)來(lái)進(jìn)行繪制。兩者差異參見(jiàn)下圖。
目前默認(rèn)是開(kāi)硬件加速的,可以通過(guò)設(shè)置 Activity,Application,窗口,View 等方式來(lái)指定軟件繪制。如果應(yīng)用需要單獨(dú)指定某些場(chǎng)景的軟件繪制方式,需要對(duì)性能、功耗等做好評(píng)估。參考鏈接:https://developer.android.com/guide/topics/graphics/hardware-accel
現(xiàn)在的較新的 SoC 平臺(tái)都帶有專門進(jìn)行 AI 運(yùn)算的 NPU 芯片,使用 NPU 代替 GPU 運(yùn)行一些復(fù)雜算法,可以有效的節(jié)省 GPU 功耗。如視頻的超分算法,可以給用戶帶來(lái)很好的體驗(yàn)。但是超分開(kāi)啟對(duì) GPU 的耗電影響很大,在某些平臺(tái)測(cè)試整機(jī)功耗可以高出 100mA,選擇用 NPU 替換 GPU 是一種優(yōu)化方式。
CPU 的優(yōu)化是功耗優(yōu)化里最常見(jiàn)的,我們遇到的大部分的 CPU 異常都是出現(xiàn)了死循環(huán)。這里使用上面介紹過(guò)的功耗歸因工具,都可以很容易的發(fā)現(xiàn)死循環(huán)問(wèn)題。此外高頻的耗時(shí)函數(shù),效果和死循環(huán)類似,很容易讓 CPU 大核跑到高頻點(diǎn),帶來(lái) CPU 功耗增加。另外一個(gè)典型的 CPU 問(wèn)題,就是動(dòng)畫泄漏,泄漏動(dòng)畫大概能帶來(lái) 20mA 的功耗增加。
由于 CPU 工作耗電很高,手機(jī)平臺(tái)大多會(huì)增加各種低功耗的 DSP 來(lái)分擔(dān) CPU 的工作,減少耗電,如常見(jiàn)視頻解碼,使用硬解會(huì)有更好的功耗表現(xiàn)。
死循環(huán)治理
死循環(huán)是我們遇到的最明顯的 CPU 異常,通常表現(xiàn)為某一個(gè)線程占滿了一個(gè)大核。線程使用率達(dá)到了 100%,手機(jī)會(huì)很容易發(fā)熱,卡頓。
這里舉一個(gè)實(shí)際修復(fù)的死循環(huán)例子,在一段循環(huán)打包日志的代碼邏輯里,所有 log打包完了,才會(huì)break跳出循環(huán)。當(dāng)db query出現(xiàn)了異常,異常處理分支并沒(méi)有做break,導(dǎo)致出現(xiàn)了死循環(huán)。
// 方法邏輯有裁剪,僅貼出主要邏輯
private JSONArray packMiscLog() {
do {
......
try {
cursor=mDb.query(......);
int n=cursor.getCount();
......
if (start_id >=max_id) {
break;
}
} catch (Exception e) {
} finally {
safeCloseCursor(cursor);
}
} while (true);
return ret;
}
對(duì)于死循環(huán)治理,我們通過(guò)實(shí)際解決的問(wèn)題,總結(jié)了幾種常見(jiàn)的死循環(huán)套路。
// 邊界條件未滿足,無(wú)法break
while (true) {
...
if (shouldExit()) {
break
}
}
// 異常處理不妥當(dāng),導(dǎo)致死循環(huán)
while (true) {
try {
do someting;
break;
} catch (e) {
}
}
// 消息處理不當(dāng),導(dǎo)致Handler線程死循環(huán)
void handleMessage(Message msg) {
//do something
handler.sendEmptyMessage(MSG)
}
高頻耗時(shí)函數(shù)治理
除了死循環(huán)問(wèn)題,我們遇到的另外一種常見(jiàn)的就是高頻的耗時(shí)函數(shù)。通過(guò)線上監(jiān)控 CPU 異常,我們也找到很多可優(yōu)化的點(diǎn)。如 md5 壓縮算法的耗時(shí),正則表達(dá)式的不合理使用,使用 cmd 執(zhí)行系統(tǒng)命令的耗時(shí)等。這種就 case by case 的修復(fù),就有很不錯(cuò)的收益。
Alarm,Wakelock,JobScheduler 的規(guī)范使用
最常見(jiàn)的后臺(tái) CPU 耗電就是對(duì)后臺(tái)資源的不合理使用。Alarm 的頻繁喚醒,wakelock 的長(zhǎng)時(shí)間不釋放,JobScheduler 的頻繁執(zhí)行,都會(huì)使 CPU 保持喚醒狀態(tài),造成后臺(tái)耗電。這種行為很容易讓系統(tǒng)判斷應(yīng)用為后臺(tái)異常耗電,通常會(huì)被系統(tǒng)清理,或者發(fā)出高耗電提醒。
我們可以通過(guò) dumpsys alarm & dumpsys power & dumpsys jobscheduler 查看相關(guān)的統(tǒng)計(jì)信息,也可以通過(guò) BH 的后臺(tái)統(tǒng)計(jì)來(lái)分析自身的使用情況。
參考綠盟的功耗標(biāo)準(zhǔn),滅屏 Alarm 觸發(fā)小于過(guò) 12 次/h,即 5min 一次,5min 一次在數(shù)據(jù)業(yè)務(wù)下可以保證長(zhǎng)鏈接存活,廠商的后臺(tái)功耗優(yōu)化也通常會(huì)強(qiáng)制對(duì)齊 Alarm 為 5min 觸發(fā)一次。
后臺(tái)的 Partial Wakelock 通常會(huì)被重點(diǎn)限制,非可感知的場(chǎng)景(音樂(lè),導(dǎo)航,運(yùn)動(dòng))等會(huì)被廠商強(qiáng)制釋放 wakelock。按照綠盟的標(biāo)準(zhǔn),滅屏下每小時(shí)累計(jì)持鎖小于 5min,從實(shí)際經(jīng)驗(yàn)上看,持 Partial 鎖超過(guò) 1min 就會(huì)被標(biāo)為 Long 的 wakelock,如果是應(yīng)用在后臺(tái)無(wú)可感知業(yè)務(wù)并且頻繁持鎖,導(dǎo)致系統(tǒng)無(wú)法休眠的,系統(tǒng)會(huì)觸發(fā) forcestop 清理。
某些定時(shí)任務(wù)可以使用 JobScheduler 來(lái)替代 Alarm,Job 的好處是可以組合多種觸發(fā)條件,選擇一個(gè)最恰當(dāng)?shù)臅r(shí)刻讓系統(tǒng)調(diào)度自己的后臺(tái)任務(wù)。這里建議使用充電+網(wǎng)絡(luò)可用狀態(tài)下處理自己的后臺(tái)任務(wù),對(duì)功耗體驗(yàn)是最好的。如果是非充電場(chǎng)景下,設(shè)置條件頻繁觸發(fā) job,同樣會(huì)帶來(lái)耗電問(wèn)題。值得一提的是 Job 執(zhí)行完要及時(shí)結(jié)束。因?yàn)?JobScheduler 在執(zhí)行時(shí)會(huì)持有一個(gè)*job/*開(kāi)頭的 wakelock,最長(zhǎng)執(zhí)行時(shí)間 10min,如果一直在執(zhí)行狀態(tài)不結(jié)束,就會(huì)導(dǎo)致系統(tǒng)無(wú)法休眠。
硬解通常是用手機(jī)平臺(tái)自帶的硬件解碼器來(lái)做解碼從而實(shí)現(xiàn)視頻播放,基于專用芯片的硬解碼速度快、功耗低;軟解碼方面,通常使用 FFMPEG 內(nèi)置的 H.264 和 H.265 的軟件解碼庫(kù)來(lái)做解碼。
下表是三星手機(jī)和蘋果手機(jī)分別在軟硬解情況下的功耗,可以看出硬解功耗比軟解功耗顯著降低,目前抖音默認(rèn)使用硬解。
圖片來(lái)源:http://www.noobyard.com/article/p-eedllxrr-qz.html
網(wǎng)絡(luò)耗電是應(yīng)用耗電的一個(gè)重要部分,一個(gè)數(shù)據(jù)包的收發(fā),會(huì)同步拉動(dòng) CPU 和 Modem/WIFI 兩大系統(tǒng)。由于 LTE 的 CDRX 特性(即沒(méi)有數(shù)據(jù)包接收,維持一定時(shí)間的激活態(tài),再進(jìn)入睡眠,依賴運(yùn)營(yíng)商配置,通常為 10s),所以批量進(jìn)行網(wǎng)絡(luò)訪問(wèn),減少頻繁的網(wǎng)絡(luò)喚醒對(duì)網(wǎng)絡(luò)功耗很有幫忙。此外優(yōu)化壓縮算法,減少數(shù)據(jù)傳輸量也從基礎(chǔ)上減少了網(wǎng)絡(luò)耗電。
此外弱信號(hào)條件下的網(wǎng)絡(luò)請(qǐng)求會(huì)提高天線的功率,也會(huì)觸發(fā)頻繁的搜網(wǎng),帶來(lái)更高的網(wǎng)絡(luò)功耗。根據(jù)網(wǎng)絡(luò)質(zhì)量進(jìn)行網(wǎng)絡(luò)請(qǐng)求調(diào)度,提前預(yù)緩存網(wǎng)絡(luò)資源,可以減少網(wǎng)絡(luò)耗電。
對(duì)于應(yīng)用的后臺(tái) PUSH 來(lái)說(shuō),使用廠商穩(wěn)定的 push 鏈路替代自己的長(zhǎng)鏈接可以減少功耗。如果不能替換,也可以優(yōu)化長(zhǎng)鏈接保活的心跳,根據(jù)不同的網(wǎng)絡(luò)條件動(dòng)態(tài)的調(diào)整心跳。根據(jù)經(jīng)驗(yàn),數(shù)據(jù)業(yè)務(wù)下通常是 5min,WIFI 網(wǎng)絡(luò)下通常可以達(dá)到 20min 或更久。
抖音對(duì)于長(zhǎng)鏈接進(jìn)行了的心跳優(yōu)化,進(jìn)入后臺(tái)的長(zhǎng)鏈接心跳時(shí)間間隔 [4min, 28min],初始心跳 4min。采用動(dòng)態(tài)心跳試探策略,每次步進(jìn) 2min,確定最大心跳間隔。
由于系統(tǒng)對(duì)后臺(tái)應(yīng)用有多種網(wǎng)絡(luò)限制策略,最常見(jiàn)的是 Doze 模式,手機(jī)滅屏一段時(shí)間后會(huì)進(jìn)入 doze,限制非白名單應(yīng)用訪問(wèn)網(wǎng)絡(luò),并在窗口期解除限制,窗口期為每 10min 放開(kāi) 30s。所以在后臺(tái)進(jìn)行網(wǎng)絡(luò)訪問(wèn)前要特別注意進(jìn)行網(wǎng)絡(luò)可用的判斷,選擇窗口期進(jìn)行網(wǎng)絡(luò)訪問(wèn),避免因?yàn)楸幌蘧W(wǎng)而浪費(fèi)了 CPU 資源。
這里舉一個(gè) Doze 未適配的后臺(tái)耗電例子,用戶反饋抖音自上次手機(jī)充滿電(24h)后,沒(méi)有在前臺(tái)使用過(guò),耗電占比 31%,分析日志發(fā)現(xiàn) I 在 Doze 限制網(wǎng)絡(luò)期間,會(huì)觸發(fā)輪詢判斷網(wǎng)絡(luò)是否及時(shí)恢復(fù),此邏輯在后臺(tái)未適配 Doze 的窗口期模式,導(dǎo)致了后臺(tái)頻繁嘗試網(wǎng)絡(luò)請(qǐng)求帶來(lái)的 CPU 耗電。
音頻的耗電最終體現(xiàn)在 Codec 和 SmartPA(連接喇叭的功率放大器)兩部分。減少 Audio 耗電最明顯的就是減少音頻的音量,這直接反應(yīng)到喇叭的響度上。
用 0-15 級(jí)的音量進(jìn)行測(cè)試,可以看到音量對(duì)功耗的影響巨大,尤其是超過(guò) 10 之后,整體增幅非常巨大。每一級(jí)幾乎與功耗成百分比上漲。
由于用戶對(duì)音量的感受很明顯,直接全局降低音量會(huì)帶來(lái)不好的體驗(yàn)。廠商通常會(huì)針對(duì)不同的場(chǎng)景,設(shè)計(jì)不同的音頻參數(shù),如電影場(chǎng)景,游戲場(chǎng)景,導(dǎo)航場(chǎng)景,動(dòng)態(tài)調(diào)節(jié)音頻的高低頻配置參數(shù),兼顧了效果和功耗。
從這個(gè)角度出發(fā),可以選擇和廠商合作,根據(jù)播放視頻的內(nèi)容,精細(xì)化調(diào)整音頻參數(shù),如電影剪輯類型視頻就使用電影場(chǎng)景的參數(shù),游戲視頻就切換為游戲場(chǎng)景的配置參數(shù),從而達(dá)到用戶無(wú)感調(diào)節(jié)音量節(jié)省功耗的目的。
Camera 是功耗大戶,尤其是高分辨率高幀率的錄制會(huì)帶來(lái)快速的功耗消耗和溫升。經(jīng)過(guò)線下測(cè)算,開(kāi)播場(chǎng)景,Camera 功耗 200mA+,占整機(jī)的 25%以上。
優(yōu)化Camera功耗的思路主要是從業(yè)務(wù)降級(jí)的角度上進(jìn)行,如降低錄制的分辨率,降低錄制幀率等。之前抖音直播和生產(chǎn)端都是使用30幀,但最終只使用15幀,在開(kāi)播端主動(dòng)下調(diào)采集幀率,按需設(shè)置幀率為15幀,功耗顯著降低了120ma。
sensor 的典型功耗值很低,如我們常用到的 accelerometer(加速度計(jì))的典型功耗只有 180uA。但 sensor 的開(kāi)啟會(huì)導(dǎo)致 cpu 的喚醒與負(fù)載增加,尤其是在應(yīng)用退到后臺(tái),sensor 的濫用會(huì)顯著增加待機(jī)功耗。可以在低電量時(shí)關(guān)閉不必要的 sensor,減少耗電。
精確度,頻率,間隔是影響 GPS 耗電的三個(gè)主要因素。其中精度影響定位的工作模式,頻率和間隔是影響工作時(shí)長(zhǎng),我們可以通過(guò)優(yōu)化這三者來(lái)減少 GPS 的耗電
Android 原生定位提供 GPS 定位和網(wǎng)絡(luò)定位兩種模式。GPS 定位支持離線定位,依靠衛(wèi)星,沒(méi)有網(wǎng)絡(luò)也能定位,精度高,但功耗大,因需要開(kāi)啟移動(dòng)設(shè)備中的 GPS 定位模塊,會(huì)消耗較多電量。
Network 定位(網(wǎng)絡(luò)定位),定位速度快,只要具備網(wǎng)絡(luò)或者基站要求,在任何地方都可實(shí)現(xiàn)瞬間定位,室內(nèi)同樣滿足;功耗小,耗電量小;但定位精度差,容易受干擾,在基站或者 WiFi 數(shù)量少、信號(hào)弱的地方定位質(zhì)量較差,或者無(wú)法定位;必須連接網(wǎng)絡(luò)才能實(shí)現(xiàn)定位。
我們可以在滿足定位要求的情況下,主動(dòng)使用低精度的網(wǎng)絡(luò)定位,減少定位耗電,抖音在進(jìn)入低功耗模式時(shí),進(jìn)行了 GPS 降級(jí)為網(wǎng)絡(luò)定位,并且擴(kuò)大了定位間隔。
這里除了業(yè)務(wù)上主動(dòng)控制頻率與間隔外,還推薦使用廠商的定位服務(wù)。為了優(yōu)化定位耗電,海外 gms 以及國(guó)內(nèi)廠商都提供了位置服務(wù) SDK,本質(zhì)上是通過(guò)系統(tǒng)服務(wù)統(tǒng)一管理位置請(qǐng)求,根據(jù)電量,信號(hào),請(qǐng)求方的延遲精度要求,進(jìn)行動(dòng)態(tài)調(diào)整,達(dá)到功耗與定位需求的平衡。提供了諸如被動(dòng)位置更新,獲取最近一次定位的位置信息,批量后臺(tái)位置請(qǐng)求等低功耗定位能力。
https://developer.android.com/guide/topics/location/battery https://developer.huawei.com/consumer/cn/doc/development/HMSCore-References/location-description-0000001088559417
上述的優(yōu)化措施,有些在常規(guī)模式下已經(jīng)實(shí)施。但有一部分是有損用戶體驗(yàn)的,我們選擇在低電量場(chǎng)景下去做,降低功耗,減少用戶的電量焦慮,獲得用戶在低電量下更多使用時(shí)長(zhǎng)。
在低功耗模式預(yù)研中,我們列舉了很多可做的措施,通過(guò) AB 實(shí)驗(yàn),我們?nèi)サ袅藰I(yè)務(wù)負(fù)向的降級(jí)手段,比如亮度降低,音量降低等。此外在功能觸發(fā)的策略上,我們通過(guò)對(duì)比了低電量彈窗提醒,設(shè)置里增加開(kāi)關(guān)+Toast 提醒,以及低電量自動(dòng)進(jìn)入,最終選擇了對(duì)用戶體驗(yàn)最好的 30%電量無(wú)打擾自動(dòng)進(jìn)入的觸發(fā)方式。
經(jīng)過(guò)實(shí)驗(yàn)發(fā)現(xiàn),一些高發(fā)熱機(jī)型,通過(guò)低功耗模式全程開(kāi)啟,也可以拿到業(yè)務(wù)收益。說(shuō)明部分有損的降級(jí),用戶在易發(fā)熱的情況下也是接受的,可以置換出業(yè)務(wù)收益,目前低功耗模式線下測(cè)試功耗收益穩(wěn)定在 20mA 以上。
功耗優(yōu)化是一個(gè)復(fù)雜的綜合課題,既包含了利用工具對(duì)功耗做拆解評(píng)估,對(duì)異常的監(jiān)控治理,也包含了主動(dòng)挖掘優(yōu)化點(diǎn)進(jìn)行優(yōu)化。上面列舉的優(yōu)化思路,我們也只是做了部分,還有部分待開(kāi)展,包括功耗歸因的工具建設(shè)上,我們也還有很多可以優(yōu)化的點(diǎn)。我們會(huì)持續(xù)發(fā)力,產(chǎn)出更多的方案,在滿足使用需求的前提下,消耗更少的物理資源,給抖音用戶帶來(lái)更好的功耗體驗(yàn)。
抖音 Android 基礎(chǔ)技術(shù)團(tuán)隊(duì)是一個(gè)深度追求極致的團(tuán)隊(duì),我們專注于性能、架構(gòu)、包大小、穩(wěn)定性、基礎(chǔ)庫(kù)、編譯構(gòu)建等方向的深耕,保障超大規(guī)模團(tuán)隊(duì)的研發(fā)效率和數(shù)億用戶的使用體驗(yàn)。目前北京、上海、杭州、深圳都有大量人才需要,歡迎有志之士與我們共同建設(shè)億級(jí)用戶的 APP! 感興趣的同學(xué)可以點(diǎn)擊“閱讀原文”,進(jìn)入字節(jié)跳動(dòng)招聘官網(wǎng)查詢「抖音基礎(chǔ)技術(shù) Android」相關(guān)職位,也可以郵件聯(lián)系:gaoyuan.mmm@bytedance.com 咨詢相關(guān)信息或者直接發(fā)送簡(jiǎn)歷內(nèi)推!
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。