0、介绍
本篇整理自己在开发过程中遇到的各种坑,以便以后参考。
1、自定义Dialog样式时发现顶部出现一条蓝色横线
//部分手机dialog会出现蓝色横线,因此将它隐藏掉
Context context = getDialog().getContext();
int divierId = context.getResources().getIdentifier("android:id/titleDivider", null, null);
View divider = getDialog().findViewById(divierId);
if (divider != null) divider.setBackgroundColor(Color.TRANSPARENT);
在show之前将横线隐藏掉
2、 Permission Denial: starting Intent with revoked permission android.permission.CAMERA
崩溃原因:测试机版本>=M,代码中通过ACTION调取摄像机,如果Manifest文件中声明了android.permission.CAMERA权限则会崩溃。
解决方案:
1、如果项目中没有使用到相机权限则把CAMERA权限删掉。
2、启动该ACTION时先询问相机权限是否已动态声明。
3、getExternalFilesDir(null)可能返回空
getExternalFilesDir(null).getPath() 由于可能返回空报NullPointerException,因此使用时需要加一层trycatch保护
4、升级AS的ndk时出现"No toolchains found in the NDK toolchains folder for ABI with prefix: mips64el-linux-android"
崩溃原因:更新之后少了ndk-bundle下的toolchains少了文件
解决方案:
https://developer.android.com/ndk/downloads/?hl=zh-en
在这里下载稳定的ndk库,下载完之后跟Android/sdk/ndk-bundle/toolchains/下的文件比对,把少了的补充进去。
5、gradle dependency中exclude用法
一般在dependencies下的依赖格式:
groupName:moduleName:版本号
例如:
com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION
查看某个依赖它下面的依赖树,一般类似于:
+--- com.android.support:appcompat-v7:27.1.1
| +--- com.android.support:support-annotations:27.1.1
| +--- com.android.support:support-core-utils:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- com.android.support:support-compat:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- android.arch.lifecycle:runtime:1.1.0
| | +--- android.arch.lifecycle:common:1.1.0
| | \--- android.arch.core:common:1.1.0
| +--- com.android.support:support-fragment:27.1.1
| | +--- com.android.support:support-compat:27.1.1 (*)
| | +--- com.android.support:support-core-ui:27.1.1
| | | +--- com.android.support:support-annotations:27.1.1
| | | +--- com.android.support:support-compat:27.1.1 (*)
| | | \--- com.android.support:support-core-utils:27.1.1 (*)
| | +--- com.android.support:support-core-utils:27.1.1 (*)
| | +--- com.android.support:support-annotations:27.1.1
| | +--- android.arch.lifecycle:livedata-core:1.1.0
| | | +--- android.arch.lifecycle:common:1.1.0
| | | +--- android.arch.core:common:1.1.0
| | | \--- android.arch.core:runtime:1.1.0
| | | \--- android.arch.core:common:1.1.0
| | \--- android.arch.lifecycle:viewmodel:1.1.0
| +--- com.android.support:support-vector-drawable:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- com.android.support:support-compat:27.1.1 (*)
| \--- com.android.support:animated-vector-drawable:27.1.1
| +--- com.android.support:support-vector-drawable:27.1.1 (*)
| \--- com.android.support:support-core-ui:27.1.1 (*)
+--- com.jakewharton:butterknife:8.8.1
| +--- com.jakewharton:butterknife-annotations:8.8.1
| | \--- com.android.support:support-annotations:25.3.0 -> 27.1.1
| +--- com.android.support:support-annotations:25.3.0 -> 27.1.1
| \--- com.android.support:support-compat:25.3.0 -> 27.1.1 (*)
exclude的格式为:
implementation ('com.jakewharton:butterknife:8.8.1') {
exclude group: 'com.android.support',module:'support-annotations'
}
规则很简单,移除该依赖下的groupName为"com.android.support",且moduleName为"support-annotations"的依赖
改完之后的依赖树
+--- com.android.support:appcompat-v7:27.1.1
| +--- com.android.support:support-annotations:27.1.1
| +--- com.android.support:support-core-utils:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- com.android.support:support-compat:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- android.arch.lifecycle:runtime:1.1.0
| | +--- android.arch.lifecycle:common:1.1.0
| | \--- android.arch.core:common:1.1.0
| +--- com.android.support:support-fragment:27.1.1
| | +--- com.android.support:support-compat:27.1.1 (*)
| | +--- com.android.support:support-core-ui:27.1.1
| | | +--- com.android.support:support-annotations:27.1.1
| | | +--- com.android.support:support-compat:27.1.1 (*)
| | | \--- com.android.support:support-core-utils:27.1.1 (*)
| | +--- com.android.support:support-core-utils:27.1.1 (*)
| | +--- com.android.support:support-annotations:27.1.1
| | +--- android.arch.lifecycle:livedata-core:1.1.0
| | | +--- android.arch.lifecycle:common:1.1.0
| | | +--- android.arch.core:common:1.1.0
| | | \--- android.arch.core:runtime:1.1.0
| | | \--- android.arch.core:common:1.1.0
| | \--- android.arch.lifecycle:viewmodel:1.1.0
| +--- com.android.support:support-vector-drawable:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- com.android.support:support-compat:27.1.1 (*)
| \--- com.android.support:animated-vector-drawable:27.1.1
| +--- com.android.support:support-vector-drawable:27.1.1 (*)
| \--- com.android.support:support-core-ui:27.1.1 (*)
+--- com.jakewharton:butterknife:8.8.1
| +--- com.jakewharton:butterknife-annotations:8.8.1
| \--- com.android.support:support-compat:25.3.0 -> 27.1.1 (*)
同时还支持只指定moduleName或者只指定groupName的情况。
只指定moduleName:
implementation ('com.jakewharton:butterknife:8.8.1') {
exclude module:'support-annotations'
}
这之后的依赖树为:
+--- com.android.support:appcompat-v7:27.1.1
| +--- com.android.support:support-annotations:27.1.1
| +--- com.android.support:support-core-utils:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- com.android.support:support-compat:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- android.arch.lifecycle:runtime:1.1.0
| | +--- android.arch.lifecycle:common:1.1.0
| | \--- android.arch.core:common:1.1.0
| +--- com.android.support:support-fragment:27.1.1
| | +--- com.android.support:support-compat:27.1.1 (*)
| | +--- com.android.support:support-core-ui:27.1.1
| | | +--- com.android.support:support-annotations:27.1.1
| | | +--- com.android.support:support-compat:27.1.1 (*)
| | | \--- com.android.support:support-core-utils:27.1.1 (*)
| | +--- com.android.support:support-core-utils:27.1.1 (*)
| | +--- com.android.support:support-annotations:27.1.1
| | +--- android.arch.lifecycle:livedata-core:1.1.0
| | | +--- android.arch.lifecycle:common:1.1.0
| | | +--- android.arch.core:common:1.1.0
| | | \--- android.arch.core:runtime:1.1.0
| | | \--- android.arch.core:common:1.1.0
| | \--- android.arch.lifecycle:viewmodel:1.1.0
| +--- com.android.support:support-vector-drawable:27.1.1
| | +--- com.android.support:support-annotations:27.1.1
| | \--- com.android.support:support-compat:27.1.1 (*)
| \--- com.android.support:animated-vector-drawable:27.1.1
| +--- com.android.support:support-vector-drawable:27.1.1 (*)
| \--- com.android.support:support-core-ui:27.1.1 (*)
+--- com.jakewharton:butterknife:8.8.1
| +--- com.jakewharton:butterknife-annotations:8.8.1
| \--- com.android.support:support-compat:25.3.0 -> 27.1.1 (*)
剩余的一种情况就不演示了。
注意:
1、exclude语法在官方包(就是support库这些)的依赖下不奏效,这个不知道什么原因。
6、DialogFragment给Dialog配置onDismissListener()不起作用。
案例:
dialogFragment.getDialog().setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialogInterface) {
//TODO
}
});
dialogFragment.show(getFragmentManager(),"");
这种情况下dialog关闭时onDismiss不会调用。
原因:
查看DialogFragment的源码发现,DialogFragment里面也有给dialog设置监听器。
public void onActivityCreated(Bundle var1) {
super.onActivityCreated(var1);
......
this.mDialog.setCancelable(this.mCancelable);
this.mDialog.setOnCancelListener(this);
this.mDialog.setOnDismissListener(this);
if (var1 != null) {
Bundle var4 = var1.getBundle("android:savedDialogState");
if (var4 != null) {
this.mDialog.onRestoreInstanceState(var4);
}
}
}
}
因此如果我们按案例中的方案设置的监听器会被DialogFragment本身覆盖,自然调用不到。
解决方案
自定义DialogFragment,覆盖DialogFragment的onDismiss()方法,添加上自己的逻辑既可。
@Override
public void onDismiss(DialogInterface dialogInterface) {
super.onDismiss(dialogInterface);
if (onDissmissListener != null) onDissmissListener.onDismiss(dialogInterface);
}
7、GridView的item有默认点击背景
解决方案:
gridView.setSelector(new ColorDrawable(Color.TRANSPARENT));// 去掉默认点击背景
8、gradle dependecy一直依赖最新版本的依赖
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
9、Android stroke边线框只画某一边
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:left="-2dp"
android:right="-2dp"
android:top="-2dp">
<shape>
<solid android:color="#ffffff"/>
<stroke
android:width="1dp"
android:color="#ff0000"/>
</shape>
</item>
</layer-list>
10、TextView当使用Spannable时不显示省略号的问题
重写TextView,反射修改其中的属性
public class FoldTextView extends AppCompatTextView {
public FoldTextView (Context context) {
super(context);
}
public FoldTextView (Context context, AttributeSet attrs) {
super(context, attrs);
}
public FoldTextView (Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
StaticLayout layout = null;
Field field = null;
try {
Field staticField = DynamicLayout.class.getDeclaredField("sStaticLayout");
staticField.setAccessible(true);
layout = (StaticLayout) staticField.get(DynamicLayout.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (layout != null) {
try {
field = StaticLayout.class.getDeclaredField("mMaximumVisibleLineCount");
field.setAccessible(true);
field.setInt(layout,2);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
if (layout != null && field != null) {
try {
field.setInt(layout,Integer.MAX_VALUE);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
11、子module下的资源文件id不是静态常量
原因:
(1)该module编译后的代码中该资源被替换成值
(2)主工程下有同名资源id的话会被替换掉
(3)主工程会重新针对该资源生成一个id
(4)之后就会出现子module中该资源找不到的情况
子module资源id非静态常量可能出现的情况
(1)主工程有相同名称资源时会使用主工程的
(2)子module下由于资源id非静态常量,没办法使用switch-case
(3)子module下没法直接使用ButterKnife
12、当同个布局下出现两个同名id控件的情况
出现情况:Fragment包含ViewPager,ViewPager又包含Fragment,此时父Fragment和子Fragment拥有同id控件,父Fragment通过findViewById()找到的不一定是父Fragment下的控件,因为它会返回第一个找到的控件
13、Fragment中调用startActivityForResult
(1)getActivity().startActivityForResult(),只有父Aty的onActivityResult被调用,Fragment的不被调用
(2)startActivityForResult(),当前Fragment和父Activity的onActivityResult被调用
()getParentFragment().startAcitivtyForResult(),父Fragment和父Activity的onActivityResult被调用
14、latest.release指向非最新版本号
与第三方联调时候会发现当第三方第一时间更新了maven依赖时通过latest.release没有把最新版本的依赖拉下来,拉的是之前的版本。这个时候手动改版本号就可解决。
如果找得到原因的话,这里补充
15、Dialog初始化Context误区
因为最近有个需求是需要计时后弹出Dialog,此时如果用户的app在后台且计时到了需要弹窗,如果Activity被回收了,出现了crash
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@ce0817e is not valid; is your activity running?
原因:
Dialog可以传入Context初始化,但Dialog的展示需要依附Activity,换言之就是show()时候需要判断Activity是否存活
解决方案:
初始化时Context尽量传入为Activity
if(!activity.isFinishing()) dialog.show()
16、ViewPager+Fragment重建时泄漏问题
场景:
当首页使用ViewPager+Fragment,之后进入其他Activity,在某一个Activity中崩溃时,app会尝试恢复栈顶崩溃前的Aty。这种崩溃情况下会导致Fragment重复创建内存泄漏。
此时Aty生命周期:onSaveInstanceState---> onRestoreINstanceState。并重新走onCreate()创建流程。
如果你与我使用的是该场景:
FragmentManager fragmentManager = getSupportFragmentManager();
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
fragmentList.add(new HomeFragment());
titleList.add("HomeFragment");
fragmentList.add(new TaskFragment().addStatusBarPadding(true));
titleList.add("TaskFragment");
fragmentList.add(new UserCenterFragment().addStatusBarPadding(true));
titleList.add("UserCenterFragment");
mPageAdapter = new PagerFragment(fragmentManager, fragmentList, titleList);
viewPager.setAdapter(mPageAdapter);
你会发现通过adapter.getItem()拿到的Fragment其实与viewPager上的不是同一个实例。
解决方案:
class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
savedInstanceState.remove("android:support:fragments"); //不保存fragments,当Activity被销毁时保证每次Fragment都是重新创建
}
}
}
补充另一个更加优化的处理方案:https://www.jianshu.com/p/e35089896ed4
17、PopupWindow注意事项
1、调用show()时,需要注意不能在onCreate阶段调用,否则报错
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
正确做法:post延时处理
findViewById(R.id.root).post(new Runnable() {
@Override
public void run() {
popupWindow.showAsDropDown(findViewById(R.id.btn1));
}
});
2、调用dismiss()时,需要判断当前Actiivty是否finish(),否则crash(目前该crash只在5.X手机上发现)
java.lang.IllegalArgumentException: View=com.iqiyi.ishow.attention.view.nul{3c6fd43e V.E..... .......D 0,0-720,120} not attached to window manager
正确做法:判断Activity状态
private static void dismissWithCheck() {
if (popupWindow != null && popupWindow.isShowing()) {
Context context = ((ContextWrapper)popupWindow.getContentView().getContext()).getBaseContext();
try {
if (context instanceof Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (!((Activity) context).isFinishing() && !((Activity) context).isDestroyed()) {
popupWindow.dismiss();
}
} else {
// Api < 17. Unfortunately cannot check for isDestroyed()
if (!((Activity) context).isFinishing()) {
popupWindow.dismiss();
}
}
}
} catch (IllegalArgumentException e) {
Log.append("IllegalArgumentException:").append(e.getMessage()).append("\n");
} catch (Exception e) {
Log.append("Exception:").append(e.getMessage()).append("\n");
}
}
}
18、Logcat查看日志技巧
官网链接:https://developer.android.com/studio/debug/am-logcat#memory-logs
当引用发生GC时,响应的消息会输出到logcat中,可以根据Logcat定位gc相关信息。
19、单个页面多个ViewPager使用同一个id出现的情况
描述场景:当一个Activity中包含了多个ViewPager,且这几个ViewPager的id是一样的时候,如果ViewPager中包含的都是Fragment,那只有第一个ViewPager会显示Fragment,其余的ViewPager会显示没有子View。
描述结果:由于ViewPager具有相同的id,所有的Fragment都会被添加到Activity的contentView找到的第一个ViewPager中,查看布局元素就会发现,所有的Fragment生命周期正常,且Fragment.getView().getParent()都指向第一个ViewPager。
原因分析:
首先看FragmentPagerAdapter中如何生成Fragment
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
#1
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
#2 这句是关键
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
public long getItemId(int position) {
return position;
}
1、这里的container指的是ViewPager
2、在#1步骤,代码会根据生成的name从FragmentManager中找对应的Fragment,如果找到,则不走getItem()获取Fragment。也就是说如果你的页面中存在2个同名的ViewPager,那么第二个ViewPager可能会因为生成的name相同而没法将Fragment添加到FragmentManager中。而会出现什么样的后果还没尝试过
3、在#2步骤,代码会把得到的Fragment添加到FragmentTransaction事务中
@NonNull
public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment,
@Nullable String tag) {
doAddOp(containerViewId, fragment, tag, OP_ADD);
return this;
}
void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {
final Class<?> fragmentClass = fragment.getClass();
final int modifiers = fragmentClass.getModifiers();
...
if (containerViewId != 0) {
if (containerViewId == View.NO_ID) {
throw new IllegalArgumentException("Can't add fragment "
+ fragment + " with tag " + tag + " to container view with no id");
}
if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
throw new IllegalStateException("Can't change container ID of fragment "
+ fragment + ": was " + fragment.mFragmentId
+ " now " + containerViewId);
}
fragment.mContainerId = fragment.mFragmentId = containerViewId;
}
addOp(new Op(opcmd, fragment));
}
这里最关键的一个步骤是fragment.mContainerId = fragment.mFragmentId = containerViewId; 将Fragment的mContainerId置为了ViewPager的id。接下来看Fragment.mContainerId在哪里使用,就会发现神奇的事情。
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
...
case Fragment.CREATED:
...
if (newState > Fragment.CREATED) {
if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
if (!f.mFromLayout) {
ViewGroup container = null;
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
#步骤1
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
if (container == null && !f.mRestored) {
String resName;
try {
resName = f.getResources().getResourceName(f.mContainerId);
} catch (Resources.NotFoundException e) {
resName = "unknown";
}
throwException(new IllegalArgumentException(
"No view found for id 0x"
+ Integer.toHexString(f.mContainerId) + " ("
+ resName
+ ") for fragment " + f));
}
}
#步骤2
f.mContainer = container;
f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
if (container != null) {
#步骤3
container.addView(f.mView);
}
if (f.mHidden) {
f.mView.setVisibility(View.GONE);
}
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
false);
// Only animate the view if it is visible. This is done after
// dispatchOnFragmentViewCreated in case visibility is changed
f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
&& f.mContainer != null;
} else {
f.mInnerView = null;
}
}
f.performActivityCreated(f.mSavedFragmentState);
dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
if (f.mView != null) {
f.restoreViewState(f.mSavedFragmentState);
}
f.mSavedFragmentState = null;
}
...
}
在FragmentManagerImpl.moveToState方法中,当newState为CREATED的时候,演示了Fragment是如何添加到布局中的:
步骤1、通过mContainer.onFindViewById方法,根据Fragment.mContainerId从页面中找到Fragment的父布局。mContainer是FragmentManagerImpl的局部变量,它的赋值对象一般情况下是FragmentHostCallbacks,而调用onFindViewById方法最终会调用FragmentActivity.findViewById()方法,也就是从根布局中找Fragment的父布局。在该场景下,由于单个页面中存在多个相同id的ViewPager,所以FragmentManagerImpl根据Fragment.mContainerId找到的一直都是第一个ViewPager。
步骤2、调用Fragment.performCreateView(),在里面又会调用我们熟悉的onCreateView()方法创建根布局。
步骤3、container.addView()这里会把Fragment.mView添加到container里,这里也就证实了为什么后面ViewPager的Fragment会被添加进第一个ViewPager里面了。
结论:
当同一个页面下如果存在多个id相同的ViewPager时,如果每一页都是Fragment的情况下会出现添加异常情况,后面几个ViewPager期望添加的Fragment会因为FragmentManagerImpl中找Fragment的父布局时,由于根据id都找到了第一个ViewPager,而将所有的Fragment都添加到了第一个ViewPager中去。此时就出现了后面几个ViewPager没有内容的情况。
还有种情况是可能让同一个页面中允许存在2个相同id的ViewPager且显示正常的。当且仅当两个ViewPager传入的FragmentManger不是同一个的情况(例如一个传入的是Activity的getSupportFragmentManager,另一个传入的是Fragment的getChildFragmentManager),当FragmentManger不为同一个的情况时,在步骤1中就会因为从不同的根布局中寻找各自的子View,这个时候找到的ViewPager就不会是同一个,因而添加Fragment的显示逻辑就会正常。
20、ImageView设置ImageResource的时候是否引起重绘
需要根据drawable的尺寸来分析。如果跟之前的drawable一致那么就不会requestLayout(),如果不一致的话则会requestLayout()
public void setImageDrawable(@Nullable Drawable drawable) {
if (mDrawable != drawable) {
mResource = 0;
mUri = null;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
updateDrawable(drawable);
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}