標籤:
最近兩天一直在糾結個問題,就是我們新版的軟體通過IDEA編譯出來運行在4.4的手機上整個相機UI是完全錯亂的,同事幾個手機運行都一樣,錯亂的樣子就是整個UI壓縮擠壓在一起,完全不是你在布局裡面設定的還具有相對位置的樣子。 但是通過IDEA的布局檔案的design按鈕看到的布局展示demo又是正常的,所以,一直懷疑是編譯的問題,或者某些控制項的id是否有重複。於是重新rebuild,重新刪除out,gen目錄,重新編譯,問題依舊。暈哦,怎麼回事?於是,我想仔細去分析下。於是隨意又用meizu的4.1系統的手機去試了下,結果,我擦,居然正常的,怎麼回事?問了同事最近有沒有改過相機相關的代碼,都說沒有。我看了提交記錄,的確是沒有啊,怎麼可能呢?實在想不明白,後來追蹤以前的記錄,發現改了manifest裡面一個地方就是android:targetSdkVersion這個屬性,暈,這個屬相和相機UI展示有毛線關係,哎,還是看看吧。同事從android:targetSdkVersion = 14改成了android:targetSdkVersion = 19,於是恢複試試,結果,我去,居然運行ok了,再改回19試試,擦,又不ok了。好了,原因找到了,是android:targetSdkVersion的問題,可是為什麼android:targetSdkVersion會影響到相機UI的展示?八竿子打不著啊!!android:targetSdkVersion="14",targetSdkVersion屬性會告訴系統應用是在api level為14的系統上進行的測試,應用不允許有向上相容的行為。當應用運行在版本更高的api level的系統上時,應用還是按照targetSdkVersion版本的運行,而不需要根據更高版本的系統來顯示。但是為什麼會出現這個問題?接下來我看了下4.4系統api的說明,在Build.java類裡面 /** * Android 4.4: KitKat, another tasty treat. * * <p>Applications targeting this or a later release will get these * new changes in behavior:</p> * <ul> * <li> The default result of {android.preference.PreferenceActivity#isValidFragment * PreferenceActivity.isValueFragment} becomes false instead of true.</li> * <li> In {@link android.webkit.WebView}, apps targeting earlier versions will have * JS URLs evaluated directly and any result of the evaluation will not replace * the current page content. Apps targetting KITKAT or later that load a JS URL will * have the result of that URL replace the content of the current page</li> * <li> {@link android.app.AlarmManager#set AlarmManager.set} becomes interpreted as * an inexact value, to give the system more flexibility in scheduling alarms.</li> * <li> {@link android.content.Context#getSharedPreferences(String, int) * Context.getSharedPreferences} no longer allows a null name.</li> * <li> {@link android.widget.RelativeLayout} changes to compute wrapped content * margins correctly.</li> * <li> {@link android.app.ActionBar}'s window content overlay is allowed to be * drawn.</li> * <li>The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} * permission is now always enforced.</li> * <li>Access to package-specific external storage directories belonging * to the calling app no longer requires the * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} or * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} * permissions.</li> * </ul> */ public static final int KITKAT = 19;裡面有句話說了android.widget.RelativeLayout} changes to compute wrapped content margins correctly,說重新修改了RelativeLayout計算margin的方式,我想,可能是不是和改動RelativeLayout有關呢?接下來又去看了RelativeLayout的api說明。擦,這下貌似有問題了,RelativeLayout裡面介紹說Note: In platform version 17 and lower, RelativeLayout was affected by a measurement bug that could cause child views to be measured with incorrect MeasureSpec values. (See MeasureSpec.makeMeasureSpec for more details.) This was triggered when a RelativeLayout container was placed in a scrolling container, such as a ScrollView or HorizontalScrollView. If a custom view not equipped to properly measure with the MeasureSpec mode UNSPECIFIEDwas placed in a RelativeLayout, this would silently work anyway as RelativeLayout would pass a very large AT_MOST MeasureSpec instead.This behavior has been preserved for apps that set android:targetSdkVersion="17" or older in their manifest's uses-sdk tag for compatibility. Apps targeting SDK version 18 or newer will receive the correct behavior他說RelativeLayout裡面的MeasureSpec.makeMeasureSpec在17以前實現是有問題的,17以後才改了,哦,MeasureSpec.makeMeasureSpec這個方法有問題,好了,繼續調查。我們的相機ui是這樣寫的 <com.pinguo.camera360.camera.controller.CameraLayout android:id="@+id/layout_camera_container" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ff000000"> <!-- 預覽取景地區,ID不能隨意變動 --> <include android:id="@+id/layout_camera_preview" layout="@layout/camera_preview_container"/> <!-- 底部bar,ID不能隨意變動 --> <include android:id="@+id/layout_camera_bottom_bar" layout="@layout/layout_camera_bottom_menu2"/> </com.pinguo.camera360.camera.controller.CameraLayout> 這個CameraLayout是一個自訂的View,繼承的是ViewGroup, ----public class CameraLayout extends ViewGroup----- 然後CameraLayout裡面重寫了onMeasure方法,也用到了MeasureSpec.makeMeasureSpec方法,恩,可能有問題,繼續 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); calcLayoutRect(width, height); final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != View.GONE) { switch (child.getId()) { // 就是這個android:id="@+id/layout_camera_preview" case PREVIEW_ID: int wm2 = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY,preLayRect.right - preLayRect.left); int hm2 = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY,preLayRect.bottom - preLayRect.top); child.measure(wm2, hm2); break; // 就是這個android:id="@+id/layout_camera_bottom_bar" case BOTTOM_ID: int wm3 = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY,botLayRect.right - botLayRect.left); int hm3 = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY,botLayRect.bottom - botLayRect.top); child.measure(wm3, hm3); break; default: break; } } } }仔細一看,發現 MeasureSpec.makeMeasureSpec(preLayRect.right - preLayRect.left,MeasureSpec.EXACTLY);這個方法參數傳反了,public static int makeMeasureSpec(int size, int mode)方法第一個應該是size,第二個是mode,但是我們自己不小心寫錯了。那麼為什麼寫錯了,一直沒出錯呢,使用者沒反饋呢,不應該啊。繼續看源碼當我們把android:targetSdkVersion="14"配置成14的時候,也就是使用4.0的實現,4.0源碼的makeMeasureSpec是這樣實現的,4009 /**14010 * Creates a measure specification based on the supplied size and mode.14011 *14012 * The mode must always be one of the following:14013 * <ul>14014 * <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>14015 * <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>14016 * <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>14017 * </ul>14018 *14019 * @param size the size of the measure specification14020 * @param mode the mode of the measure specification14021 * @return the measure specification based on size and mode14022 */14023 public static int makeMeasureSpec(int size, int mode) {14024 return size + mode;14025 }大爺的,這就是上面系統說的那個問題了,這個實現不管你參數傳沒傳反,都是返回一樣的結果。所以,配置成android:targetSdkVersion="14,即使參數傳反了,也沒問題。如果配置android:targetSdkVersion="19",那麼採用4.4的實現來運行代碼。而4.4修改後的實現是這樣的 /** * Creates a measure specification based on the supplied size and mode. * * The mode must always be one of the following: * <ul> * <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li> * <li>{@link android.view.View.MeasureSpec#EXACTLY}</li> * <li>{@link android.view.View.MeasureSpec#AT_MOST}</li> * </ul> * * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.</p> * * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */ public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } }sUseBrokenMakeMeasureSpec是什麼呢?從源碼可知它是用來適配的。// Older apps may need this compatibility hack for measurement.sUseBrokenMakeMeasureSpec = targetSdkVersion <= JELLY_BEAN_MR1;恩,問題找到了,當你配置android:targetSdkVersion="19"後,此時sUseBrokenMakeMeasureSpec == false,然後走的邏輯就是return (size & ~MODE_MASK) | (mode & MODE_MASK);因為此時我們方法參數傳發了,所以,就導致UI布局整個出了問題,錯亂了。當配置android:targetSdkVersion="14"後,及時當前運行在4.4的手機上,sUseBrokenMakeMeasureSpec也為ture,系統走的還是老的錯誤實現方式布局(雖然是錯誤的,但是google能讓它正常運行,只是實現不對而已),所以,就還是ok的。這就是和原因所在。
關於android:targetSdkVersion所導致的問題