1. 程式人生 > >Android面試題-機型適配之痛,例如三星、小米、華為、魅族等。

Android面試題-機型適配之痛,例如三星、小米、華為、魅族等。

原始碼分析相關面試題

Activity相關面試題

Service相關面試題

與XMPP相關面試題

與效能優化相關面試題

與登入相關面試題

與開發相關面試題

與人事相關面試題

由於開源三方定製系統較多,請大家詳細描述場景、機型及解決方案,方便其他朋友參考

[問答]-Android開發中有哪些相容性問題?都是怎麼解決的?
[問答] 你在工作中遇到的最複雜的問題或者bug是什麼?你是怎麼搞定的?

華為P6和P7

場景:使用MIPush,在華為部分手機上無法推送成功。
機型:[華為P6,華為P7]
解決方案:P6和P7是華為的高階機型,不允許推送,防止騷擾使用者,無解。

魅族3和魅族4

場景:魅族手機ListView的Item中的EditText無法編輯,點選EditText彈出軟鍵盤後,軟鍵盤會立即自動隱藏
機型:[魅族3,魅族4]
解決方案:
方法一:將ListView換成RecyclerView

HTC M8

場景:HTC M8 從一個Activity 使用QQSDK 登陸, 登陸成功後, 返回Activity結果Activity 被銷燬了
機型:HTC M8 等某些帶有 虛擬 Menu 鍵盤的手機
解決方案:後來調查發現是這個Activity是全屏,遮蔽了Menu鍵盤的黑條. 但是跳轉到QQ卻把那個Menu的黑條顯示了出來, 這導致發生了 screenSize 的變化 從而導致我的Activity銷燬了.
知道了這個原因, 在manifest中的 configChanges 新增screenSize 解決了這個問題.

所有android4.4機型

場景:Android4.4系統使用了SystemBarTintManager庫修改透明狀態列後,會導致根佈局從螢幕頂端開始佈局,而不是從ActionBar開始佈局
機型:所有android4.4機型
解決方案:
方法一:針對4.4建立一套額外的佈局,即layout-v19資料夾,並且在根佈局外層再套一層LinearLayout,並在LinearLayout中新增一個屬性android:fitsSystemWindows=”true”

方法二:是為4.4及以上添加了paddingTop去適配,新增layout覺得不好適配。

方法三:在Build.VERSION.SDK_INT <= 18的版本中,通過colorDrawable.setAlpha(alpha);設定actionbar背景色透明度的時候,colorDrawable需要設定callback。

final Drawable.Callback mDrawableCallback = new Drawable.Callback() {
   @Override
   public void invalidateDrawable(Drawable who) {
      getActionBar().setBackgroundDrawable(who);
   }

   @Override
   public void scheduleDrawable(Drawable who, Runnable what, long when) {
   }

   @Override
   public void unscheduleDrawable(Drawable who, Runnable what) {
   }
 };
    colorDrawable.setCallback(mDrawableCallback);

魅族MX3

場景:Camera拍攝,用setPreviewFormat設定成YV12,預覽會變成綠屏,實際用getPreviewFormat顯示是支援YV12的
機型:魅族MX3
解決方案:沒辦法只能設定成NV21了

其代表機型為:三星I8258、華為H30-T00、紅米等。

Camera常見問題:
1)Intent呼叫手機內相機程式

如果我們設定了照片的儲存路徑,那麼很可能會遇到一下三種問題:

問題一:onActivityResult方法中的data返回為空(資料表明,93%的機型的data將會是Null,所以如果我們指定了路徑,就不要使用data來獲取照片,起碼在使用前要做空判斷)。
問題二:照片無法儲存。
如果自定義儲存路徑是/mnt/sdcard/lowry/,而手機SD卡下在拍照前沒有名為lowry的資料夾,那麼部分手機拍照後圖片不會儲存,導致我們無法獲得照片,大多數手機的相機遇到資料夾不存在的情況都會自己創建出不存在的資料夾,而個別手機卻不會建立,

解決的方法就是在指定儲存路徑前先判斷路徑中的資料夾是否都存在,不存在先建立再呼叫相機。

問題三:照片可以儲存,但是名字不對。
file:///mnt/sdcard/123 1.jpg,由於URI的fromFile方法會將路徑中的空格用“%20”取代。

其實對於大多數的手機這都不算事,手機在解析儲存路徑的時候都會將“%20”替換為空格,這樣實際上最終的照片名字還是我們當初指定的名字:123 1.jpg,遺憾的是個別手機(如酷派7260)系統自帶的相機沒有將“%20”讀成空格,拍照後的照片的名字是123%201.jpg,我們用路徑“file:///mnt/sdcard/123 1.jpg”能找到照片才怪!

總結:

(1)使用onActivityResult中的intent(data)前要做空判斷。
(2)指定拍照路徑時,先檢查路徑中的資料夾是否都存在,不存在時先建立資料夾再呼叫 相機拍照。
(3)指定拍照儲存路徑時,照片的命名中不要包含空格等特殊符號。

通過Camera的open方法呼叫手機攝像頭
原因:第一次對焦未結束,應用層又發起的第二次對焦,引起對焦失敗。

解決方案一:傳入AutoFocusCallback;

解決方案二:延時操作;

解決方案三:異常捕獲。

聯想278T、酷派8022

場景:攝像頭個數判斷錯誤,當我們使用Camera.getNumberOfCameras()方法檢測攝像頭數量時返回的結果不準確,如果我們嘗試開啟一個不存在的攝像頭肯定會丟擲異常,這也提醒我們在開啟Camera攝像頭時需要加異常保護。

所有手機

問題
Android 自定義Perference的時候,系統預設的Perference裡Layout的預設值都被廠商改動了。。。一般設計到統一取值的時候,Google都用”?android:attrs。。。。”的格式,但是Google原始碼在此處用了數值,中間title的margin值所有廠商都有變動,導致自定義的Perference和預設的顯示不齊

解決
因為App的使用者機型比較雜,hack的方法比較不適用,故貼上Google原始碼,自己重新封裝, 自己統一

所有手機

PopupWindow中巢狀EditText,會出現EditText長按無法觸發“貼上”選項,可以改成Dialog巢狀EditText,包括DialogFragment。

三星Note系列,S系列

會呼叫Activity的onPause和onStop方法.其他手機會保持在onResume狀態

酷派8720L

場景:在獲取系統相機拍照然後儲存在本地有時候會儲存不上,獲取不到地址。
問題原因:通過除錯發現當拍完照返回的時候自己設的成員變數值會被回收,估計就是記憶體不足的原因。重啟機器後就好了。
解決方案:無方案。

所有手機

場景:輸入法中的emoji適配,Android4.1之前的系統不支援emoji顯示

解決方案:所以對於Android4.1之前的系統,我採用了bitmap來顯示emoji。

三星手機

問題:
(1) 攝像頭拍照後圖片資料不一定能返回 ; onActivityResult的data為空
(2) 三星的camera強制切換到橫屏 導致Activity重啟生命週期 (但是部分機型 配置 android:configChanges 也不能阻止橫豎屏切換);
(3) APP Activity A呼叫系統拍照 –> 拍照 –> 在拍好照片的介面做幾次橫豎屏轉換 –> 返回APP介面Activity A ,A 被銷燬。

解決方案:如果 activity 的銷燬如果無法避免 那麼在activity銷燬之前呼叫 onSaveInstanceState 儲存圖片的路徑
當activity重新建立的時候 會將 onSaveInstanceState 儲存的檔案傳遞給onCreate()當中
在onCreate當中 檢查照片的地址是否存在檔案 以此來判定拍照是否成功
Demo 下載地址: http://download.csdn.NET/detail/aaawqqq/7653475

OPPO 手機

場景:OPPO 手機啟動 Service 報 SecurityException
解決方案:try catch 該異常

HTC Desire 820、Lenovo A320T

場景:這個問題主要在部分機型的4.X系統上遇見,小圖示大小沒有按照24dp裁剪,而是採用了桌面圖示一樣的大小96dp

解決方案:按照標準來,小圖示大小為24dp,大圖示為桌面icon圖示大小96dp

魅族5.X手機,大圖顯示問題

場景:Flyme系統對原生Android原始碼做了修改,採用BigPictureStyle方式顯示大圖通知欄的時候,訊息與大圖重合了,如下圖。

解決方案

首先,通過BigPictureStyle來實現大圖功能肯定是走不通的,因為事實就擺著行不通的嘛。京東的App肯定是通過RemoteViews來實現的。於是,開始走彎路,嘗試通過RemoteViews來展示大圖。但是谷歌規定,自定義佈局展示的通知欄訊息最大高度是64dp。那麼,京東的App是怎麼實現的?在嘗試了各種方法以後,最後又是通過投機取巧的方式解決了問題

private void showBigPictureNotificationWithMZ(Context context) {
    NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    Notification.Builder builder = new Notification.Builder(context);
    Notification notification = generateNotification(builder);
    notification.bigContentView = mRemoteViews;
    notificationManager.notify(notifyId, notification);
}

需要先生成Notification的例項,然後手動給notification.bigContentView賦值,再notify,就可以了

問題二:頂部狀態列(StatusBar)小圖示顯示異常

場景:當通知來的時候,如果不在通知欄瀏覽,會在頂部狀態列出現一個向上翻滾動畫的通知訊息,這條通知訊息左邊是一個小圖示。部分系統這個小圖示顯示異常,是一個純灰色的正方形,如下圖。

解決方案

首先產生灰色圖示的原因就是5.0系統引入了材料設計,谷歌強制使用帶有alpha通道的圖示,並且RGB的alpha值必須是0(實測不為0也是可以的,但系統會忽略所有RGB值)。因此,使用JPG的圖片是不行的,最好的代替方案就是一張背景透明的PNG圖片。

問題三:Android 7.X機型,通知欄小圖示顯示成灰色

問題詳情

這個問題跟第二個有點類似,在7.0系統及以上,有部分應用的小圖示是灰色的,大圖可以正常顯示。碰巧的是,顯示異常的小圖示,顏色都是灰色的。

解決方案

與小圖示顯示異常解決方案類似,將小圖示替換為透明背景的PNG圖片。

問題四:RemoteViews顯示異常

問題詳情

由於系統提供的通知欄訊息型別有時候不能滿足要求,部分通知欄訊息採用自定義RemoteViews來實現。採用RemoteViews,特別是手動生成Bitmap然後直接傳給一個自定義Layout,再通過setContentView方式設定通知欄訊息時,會存在各種各樣的坑。

Android通知欄的背景色有幾種情況,白色、暗色、暗色透明和黑色。如果生成的Bitmap帶背景色,這個背景色就很難選擇。如果選擇黑色背景,那麼在白色通知欄的機型上就很難看。因此不能完全在各個系統上面完美展示出來。如果不帶背景色,那麼字型顏色也面臨同樣的困惑。試想,如果在白色的背景上顯示白色的文字,使用者看到白茫茫一片,是什麼感受?

另一方面,大部分廠商對原生的Android系統都會有各種各樣的改造,通知欄的樣式也不例外。如果按照原生的樣式來設計,那麼在大部分國內廠商的機子上顯示都和正常的普通通知欄訊息不一樣。例如華為6.0系統的機子,原生系統的時間線在右上角,華為的在左邊,這樣會給使用者帶來錯覺。

解決方案

詳見RemoteViews適配一節。

問題五:通知欄更新頻率

問題詳情

每個應用基本都有自更新的邏輯,App開機的時候提示使用者升級,點選升級按鈕後在Notification出現一個下載帶進度條的通知。應用一般是在開啟一個工作執行緒在後臺下載,然後在下載的過程中通過回撥更新通知欄中的進度條。我們知道,下載進度的快慢是不可控的,如果每次下載中的回撥都去更新通知欄,那麼可能幾百毫秒、幾十毫秒、甚至幾毫秒就更新一次通知欄,應用可能就會ANR,甚至崩潰。

解決方案

控制通知欄更新頻率,一般控制在0.5s或者1s就可以了。在某一個更新時間間隔內下載的進度回撥直接丟棄,需要注意的是下載完成的回撥,需要實時回撥通知欄訊息顯示下載完成。

問題六:噁心的後臺通知和“守護”通知
問題詳情
但凡存在後臺通知或者“守護”通知的應用,在7.0系統以後都會原形畢露.


解決方案:無

小米推送SDK接入問題

問題詳情

為了提升推送到達,考拉接入了小米推送的SDK。小米推送分為通知欄訊息和透傳訊息,通知欄訊息屬於系統級推送,在MIUI的機子上可以在程序被殺死的情況下也能收到應用推送。然而有個問題,小米認為應用在前臺時,不會回撥任何方法;小米認為應用在後臺的時候,收到通知欄訊息的同時,會回撥onNotificationMessageArrived方法。這時候就要小心翼翼地處理這條訊息了。因為如果你的應用前後臺判斷邏輯和小米的不一樣,那麼就有可能小米幫你發了一條通知欄訊息,你自己又發了一遍,造成通知欄訊息的重複傳送(這個坑考拉踩過T_T)。另一方面,在7.0系統的機子上,主標題和小圖示的顏色是可以改變的,目前小米推送SDK沒有開放這個介面供呼叫方定製。

解決方案

目前只能解決第一個問題——前後臺判斷的問題。應用是否在後臺可以根據以下程式碼進行判斷。在Android 5.0以上,可以通過ActivityManager.RunningAppProcessInfo判斷,Android 5.0及以下版本通過ActivityManager.RunningTaskInfo判斷。經測試,這個方案在Android 4.4以上結果是可以完全匹配的。

public static boolean isAppInBackgroundInternal(Context context) {
    ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
        List<ActivityManager.RunningAppProcessInfo> runningProcesses = manager.getRunningAppProcesses();
        if (!ListUtils.isEmpty(runningProcesses)) {
            for (ActivityManager.RunningAppProcessInfo runningProcess : runningProcesses) {
                if (runningProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                    return false;
                }
            }
        }
    } else {
        List<ActivityManager.RunningTaskInfo> task = manager.getRunningTasks(1);
        if (!ListUtils.isEmpty(task)) {
            ComponentName info = task.get(0).topActivity;
            if (null != info) {
                return !isKaolaProcess(info.getPackageName());
            }
        }
    }
    return true;
}

Android通知欄適配

RemoteViews適配
由於系統自帶的通知欄訊息樣式不能完全滿足產品們腦洞大開的需求,有時候我們需要自定義佈局樣式展示通知欄訊息。Android系統可以將自定義佈局通過setContent(7.X系統推薦使用setCustomContentView)設定到Notification.Builder中,來實現樣式的更變。setContent方法需要傳入一個RemoteViews物件,它是一個普通的資料型別,不是View,作用是供其他程序展示檢視。RemoteViews只支援4種基本的佈局^9:

FrameLayout
LinearLayout
RelativeLayout
GridLayout
這些佈局下面只支援幾種檢視控制元件:

AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
只能通過上述組合生成一個RemoteViews。

自定義佈局與檢視

除了上面提到的佈局與控制元件,有沒有辦法自定義佈局與檢視呢?我們知道,任何一個View,都可以生成一個Bitmap物件,支援的檢視控制元件裡有ImageView,可以通過ImageView.setBitmapResource()將自定義檢視設定到一個ImageView中,然後再隨便放到一個佈局上,就可以實現通知欄訊息的任意佈局。理想是美好的,但現實是殘酷的。使用這種方式自定義的佈局,會存在與原生的通知欄訊息樣式不一致的可能,包括小圖示/大圖示的大小,字型的大小與顏色,時間的顯示方式(不同版本的時間顯示位置和樣式都不一樣)。下面解決一個最關鍵,也最致命的問題——字型顏色。如果字型顏色和背景顏色一樣,那這條通知欄訊息就沒法看了,如RemoteViews顯示異常一節介紹的一樣。

解決字型顏色和背景顏色一樣的問題有三種解決方案,分別是:

背景色固定不透明,字型顏色與背景色形成反差。(360和京東的做法)
背景色透明,字型顏色採用系統原生的notification_style。
背景色透明,通過特殊方式拿到通知欄字型顏色和字型大小。

其中,第一種方案簡單,能夠相容所有廠商機型。例如京東固定背景色為黑色,字型為紅色。這種方式的唯一缺陷是樣式上不能與普通通知欄訊息重合,在白色背景的通知欄上極為顯眼。第二種方式,通過閱讀原始碼可知,系統的通知欄標題和內容採用的顏色分別是@android:color/primary_text_dark和@android:color/secondary_text_dark,但踩過坑之後發現並非所有的機型預設都是這兩個顏色,有可能獲取不到值。因此這種方案只能作為參考,不能用於實際環境中。最後詳細介紹一下第三種方式。

Android預設字型顏色獲取

這種方案有一點投機取巧,是網上尋找代替方案時在簡書上找到的,作者是hackware。思路就是通過Notification.Builder生成一條空的Notification,但不呼叫notify()方法,然後通過這條Notification想辦法獲取裡面的佈局元素,通過遍歷,就能拿到對應的字型和顏色了。具體看程式碼:

private static final String NOTIFICATION_TITLE = "notification_title";
public static final int INVALID_COLOR = -1; // 無效顏色
private static int notificationTitleColor = INVALID_COLOR; // 獲取到的顏色快取
/**
 * 獲取系統通知欄主標題顏色,根據Activity繼承自AppCompatActivity或FragmentActivity採取不同策略。
 *
 * @param context 上下文環境
 * @return 系統主標題顏色
 */
public static int getNotificationColor(Context context) {
    try {
        if (notificationTitleColor == INVALID_COLOR) {
            if (context instanceof AppCompatActivity) {
                notificationTitleColor = getNotificationColorCompat(context);
            } else {
                notificationTitleColor = getNotificationColorInternal(context);
            }
        }
    } catch (Exception ignored) {
    }
    return notificationTitleColor;
}
/**
 * 通過一個空的Notification拿到Notification.contentView,通過{@link RemoteViews#apply(Context, ViewGroup)}方法返回通知欄訊息根佈局例項。
 *
 * @param context 上下文
 * @return 系統主標題顏色
 */
private static int getNotificationColorInternal(Context context) {
    Notification.Builder builder = new Notification.Builder(context);
    builder.setContentTitle(NOTIFICATION_TITLE);
    Notification notification = builder.build();
    try {
        ViewGroup root = (ViewGroup) notification.contentView.apply(context, new FrameLayout(context));
        TextView titleView = (TextView) root.findViewById(android.R.id.title);
        if (null == titleView) {
            iteratorView(root, new Filter() {
                @Override
                public void filter(View view) {
                    if (view instanceof TextView) {
                        TextView textView = (TextView) view;
                        if (NOTIFICATION_TITLE.equals(textView.getText().toString())) {
                            notificationTitleColor = textView.getCurrentTextColor();
                        }
                    }
                }
            });
            return notificationTitleColor;
        } else {
            return titleView.getCurrentTextColor();
        }
    } catch (Exception e) {
        DebugLog.e(e.getMessage());
        return getNotificationColorCompat(context);
    }
}
/**
 * 使用getNotificationColorInternal()方法,Activity不能繼承自AppCompatActivity(實測5.0以下機型可以,5.0及以上機型不行),
 * 大致的原因是預設通知佈局檔案中的ImageView(largeIcon和smallIcon)被替換成了AppCompatImageView,
 * 而在5.0及以上系統中,AppCompatImageView的setBackgroundResource(int)未被標記為RemotableViewMethod,導致apply時拋異常。
 *
 * @param context 上下文
 * @return 系統主標題顏色
 */
private static int getNotificationColorCompat(Context context) {
    try {
        Notification.Builder builder = new Notification.Builder(context);
        Notification notification = builder.build();
        int layoutId = notification.contentView.getLayoutId();
        ViewGroup root = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null);
        TextView titleView = (TextView) root.findViewById(android.R.id.title);
        if (null == titleView) {
            return getTitleColorIteratorCompat(root);
        } else {
            return titleView.getCurrentTextColor();
        }
    } catch (Exception e) {
    }
    return INVALID_COLOR;
}
private static void iteratorView(View view, Filter filter) {
    if (view == null || filter == null) {
        return;
    }
    filter.filter(view);
    if (view instanceof ViewGroup) {
        ViewGroup viewGroup = (ViewGroup) view;
        for (int i = 0; i < viewGroup.getChildCount(); i++) {
            View child = viewGroup.getChildAt(i);
            iteratorView(child, filter);
        }
    }
}
private static int getTitleColorIteratorCompat(View view) {
    if (view == null) {
        return INVALID_COLOR;
    }
    List<TextView> textViews = getAllTextViews(view);
    int maxTextSizeIndex = findMaxTextSizeIndex(textViews);
    if (maxTextSizeIndex != Integer.MIN_VALUE) {
        return textViews.get(maxTextSizeIndex).getCurrentTextColor();
    }
    return INVALID_COLOR;
}
private static int findMaxTextSizeIndex(List<TextView> textViews) {
    float max = Integer.MIN_VALUE;
    int maxIndex = Integer.MIN_VALUE;
    int index = 0;
    for (TextView textView : textViews) {
        if (max < textView.getTextSize()) {
            // 找到字號最大的字型,預設把它設定為主標題字號大小
            max = textView.getTextSize();
            maxIndex = index;
        }
        index++;
    }
    return maxIndex;
}
/**
 * 實現遍歷View樹中的TextView,返回包含TextView的集合。
 *
 * @param root 根節點
 * @return 包含TextView的集合
 */
private static List<TextView> getAllTextViews(View root) {
    final List<TextView> textViews = new ArrayList<>();
    iteratorView(root, new Filter() {
        @Override
        public void filter(View view) {
            if (view instanceof TextView) {
                textViews.add((TextView) view);
            }
        }
    });
    return textViews;
}

private interface Filter {
    void filter(View view);
}

RemoteViews適配方案

獲取系統通知標題顏色,如果能夠獲取到,那麼標題、內容和時間的顏色都設定為標題顏色。
獲取不到的情況下,遍歷系統通知裡的所有文字,取字號最大的那條文字的顏色作為標題、內容和時間的顏色。
以上兩個步驟的實現在getNotificationColor()方法裡。如果還獲取不到,那麼標題和內容採用Android原生系統提供的,其中標題是@android:color/primary_text_dark,內容是@android:color/secondary_text_dark。
有一點需要說明的是,以上適配只適合在Android 7.0以下系統。Android 7.0+修改了Notification,採用@android:color/primary_text_dark和@android:color/secondary_text_dark已經獲取不到顏色值了,考慮到7.0所採用的通知欄主色調是白色,因此目前暫時的解決方案是遇到7.0的系統採用黑色字型。面對眾多廠商的原始碼修改,目前測試有ZUK的7.0系統為暗色背景,暫時的解決方案是根據機型適配。

  • 歡迎關注微信公眾號,長期推薦技術文章和技術視訊

  • 微信公眾號名稱:Android乾貨程式設計師