Android 开发的十宗罪

这里列举出了大多数开发者都会犯的错误,这些错误将严重影响 APP 运行(大多数与生命周期有关)。

一、前言

通常,会在代码中看到特定的模式,并且知道在某些情况下(通常在触发进程终止/内存不足情况后),该应用可能崩溃或无法正常工作。

二、十宗罪

我们看看这些错误的发生:

2.1 对 Activity/Fragment/View 的静态引用

如果看到如下代码:

public static Activity activity = ...;
public static Fragment fragment = ...;
public static TextView textView = ...;
public static RecyclerView recyclerView = ...;

要么

companion object {
    var activity: Activity? = ...
    var fragment: Fragment? = ...
    var textView: TextView? = ...
    var recyclerView: RecyclerView? = ...
}

很有可能,APP 正在泄漏内存,而您的视图将永远不会消失。

通常将对 Android 特定的 系统组件 或 UI组件 的 静态引用 用作快捷方式,以便 “变得更容易从另一个屏幕上调用其上的方法”。

不,不要那样做。

要为新组件提供新的初始值,请使用

Intent.putExtra

Fragment.setArguments。

为了在屏幕之间进行通信,需要使用诸如共享的 ViewModel 和共享的 LiveData 之类的东西。

除了 static refs 之外,还应该至少支持某种形式的事件分发机制-甚至EventBus是一种改进。WeakReference 也不是解决方案。

从理论上讲 startActivityForResult,您也可以使用,但是在自己的应用程序流程中拥有多个 Activity(一次在任务堆栈上包含两个 Activity)已经是它自己的蠕虫病毒了。

2.2 对 Fragment 的实例引用(不使用 findFragmentByTag)

如果看到如下代码:

public class MyActivity extends AppCompatActivity {
    WeatherFragment weatherFragment = new WeatherFragment();
    ChatFragment chatFragment = new ChatFragment();
    ...
}

要么

class MyActivity: AppCompatActivity() {
    private val weatherFragment = WeatherFragment()
    private val chatFragment = ChatFragment()
    ...
}

然后,这些片段将在每次初始化,而不是先尝试通过检查它们是否已经存在

supportFragmentManager.findFragmentByTag()。

val fragment = supportFragmentManager
       .findFragmentByTag("myFragment") 
    ?: MyFragment().also { fragment -> 
        addFragment(fragment, "myFragment")
                         }
this.myFragment = fragment

只要可以始终首先确保检查它们是否已经存在,就可以对 Fragments 进行实例引用实际上是可以的 findFragmentByTag()。

否则,我们将得到重复的或意外未附加的片段。而且,如果我们尝试与未连接的片段进行通讯,则会崩溃。

2.3 FragmentPagerAdapter 内的片段列表

如果看到如下代码:

public class FragmentAdapter extends FragmentPagerAdapter {
    private final List<Fragment> mFragmentList = new ArrayList<>();
    private final List<String> mFragmentTitleList = new ArrayList<>();

    public FragmentAdapter(FragmentManager manager) {
        super(manager);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public void addFragment(Fragment fragment, String title) {
        mFragmentList.add(fragment);
        mFragmentTitleList.add(title);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragmentTitleList.get(position);
    }
}

很有可能,您的应用会中断流程的重新创建。为什么?因为 getItem() 从未调用,所以系统已经将 Fragments 添加到 FragmentManager。

val fragment = MyFragment()
fragment.setController(this) // this is wrong
fragmentAdapter.addFragment(fragment) // this is wrong

这些添加了的片段 fragmentAdapter.addFragment() 将被完全忽略。如果我们假设与#2类似,我们可以安全地与在运行时创建的片段进行通信,则这些片段肯定是未附加的,并且应用程序将崩溃。

Fatal Exception: java.lang.IllegalStateException
    Fragment has not been attached yet

在更简单的情况下,解决方案是直接在 getItem() 调用内部实例化 Fragment 。

class MyFragmentPagerAdapter(
    private val context: Context,
    fragmentManager: FragmentManager
    ) : FragmentPagerAdapter(fragmentManager) {
        override fun getCount() = 2

        override fun getItem(position: Int) = when(position) {
            0 -> FirstFragment()
            1 -> SecondFragment()
            else -> throw IllegalStateException("Unexpected position $position")
        }

        override fun getPageTitle(position: Int): CharSequence = when(position) {
            0 -> context.getString(R.string.first)
            1 -> context.getString(R.string.second)
            else -> throw IllegalStateException("Unexpected position $position")
        }
    }
import kotlin.synthetic...

class ParentFragment: Fragment() {
    override fun onCreateView(...) = ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewPager.adapter = MyFragmentPagerAdapter(requireContext(), childFragmentManager)
        tabLayout.setupWithViewPager(viewPager)
    }
}

遵循此 Stack Overflow答案 中的说明,可以通过其标签访问片段实例。

对于动态 FragmentPagerAdapters,该解决方案比较棘手,但并非并非不可能。

public class DynamicFragmentPagerAdapter extends PagerAdapter {
    private static final String TAG = "DynamicFragmentPagerAdapter";

    private final FragmentManager fragmentManager;

    public static abstract class FragmentIdentifier implements Parcelable {
        private final String fragmentTag;
        private final Bundle args;

        public FragmentIdentifier(@NonNull String fragmentTag, @Nullable Bundle args) {
            this.fragmentTag = fragmentTag;
            this.args = args;
        }

        protected FragmentIdentifier(Parcel in) {
            fragmentTag = in.readString();
            args = in.readBundle(getClass().getClassLoader());
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(fragmentTag);
            dest.writeBundle(args);
        }

        protected final Fragment newFragment() {
            Fragment fragment = createFragment();
            Bundle oldArgs = fragment.getArguments();
            Bundle newArgs = new Bundle();
            if(oldArgs != null) {
                newArgs.putAll(oldArgs);
            }
            if(args != null) {
                newArgs.putAll(args);
            }
            fragment.setArguments(newArgs);
            return fragment;
        }

        protected abstract Fragment createFragment();
    }

    private ArrayList<FragmentIdentifier> fragmentIdentifiers = new ArrayList<>();

    private FragmentTransaction currentTransaction = null;

    private Fragment currentPrimaryItem = null;

    public DynamicFragmentPagerAdapter(FragmentManager fragmentManager) {
        this.fragmentManager = fragmentManager;
    }

    private int findIndexIfAdded(FragmentIdentifier fragmentIdentifier) {
        for (int i = 0, size = fragmentIdentifiers.size(); i < size; i++) {
            FragmentIdentifier identifier = fragmentIdentifiers.get(i);
            if (identifier.fragmentTag.equals(fragmentIdentifier.fragmentTag)) {
                return i;
            }
        }
        return -1;
    }

    public void addFragment(FragmentIdentifier fragmentIdentifier) {
        if (findIndexIfAdded(fragmentIdentifier) < 0) {
            fragmentIdentifiers.add(fragmentIdentifier);
            notifyDataSetChanged();
        }
    }

    public void removeFragment(FragmentIdentifier fragmentIdentifier) {
        int index = findIndexIfAdded(fragmentIdentifier);
        if (index >= 0) {
            fragmentIdentifiers.remove(index);
            notifyDataSetChanged();
        }
    }

    @Override
    public int getCount() {
        return fragmentIdentifiers.size();
    }

    @Override
    public void startUpdate(@NonNull ViewGroup container) {
        if (container.getId() == View.NO_ID) {
            throw new IllegalStateException("ViewPager with adapter " + this
                                            + " requires a view id");
        }
    }

    @SuppressWarnings("ReferenceEquality")
    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (currentTransaction == null) {
            currentTransaction = fragmentManager.beginTransaction();
        }
        final FragmentIdentifier fragmentIdentifier = fragmentIdentifiers.get(position);
        // Do we already have this fragment?
        final String name = fragmentIdentifier.fragmentTag;
        Fragment fragment = fragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            currentTransaction.attach(fragment);
        } else {
            fragment = fragmentIdentifier.newFragment();
            currentTransaction.add(container.getId(), fragment, fragmentIdentifier.fragmentTag);
        }
        if (fragment != currentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }
        return fragment;
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        if (currentTransaction == null) {
            currentTransaction = fragmentManager.beginTransaction();
        }
        currentTransaction.detach((Fragment) object);
    }

    @SuppressWarnings("ReferenceEquality")
    @Override
    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;
        if (fragment != currentPrimaryItem) {
            if (currentPrimaryItem != null) {
                currentPrimaryItem.setMenuVisibility(false);
                currentPrimaryItem.setUserVisibleHint(false);
            }
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
            currentPrimaryItem = fragment;
        }
    }

    @Override
    public void finishUpdate(@NonNull ViewGroup container) {
        if (currentTransaction != null) {
            currentTransaction.commitNowAllowingStateLoss();
            currentTransaction = null;
        }
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return ((Fragment) object).getView() == view;
    }

    @Override
    public Parcelable saveState() {
        Bundle bundle = new Bundle();
        bundle.putParcelableArrayList("fragmentIdentifiers", fragmentIdentifiers);
        return bundle;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        Bundle bundle = ((Bundle)state);
        bundle.setClassLoader(loader);
        fragmentIdentifiers = bundle.getParcelableArrayList("fragmentIdentifiers");
    }
}

如果你的 FragmentPagerAdapter中有addFragment 和 removeFragment ,没准这是你打算写什么。不可以,无法从外部将片段实例直接添加到 FragmentPagerAdapter中。

2.4 带有构造函数参数的片段,而不使用 FragmentFactory

如果您的代码如下所示:

class MyFragment(
    val bookDao: BookDao
    ): Fragment() {
        ...
    }

要么

public class MyFragment extends Fragment {
    private final BookDao bookDao;
    public MyFragment(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}

而且您没有这样的代码:

public class FragmentFactoryImpl extends FragmentFactory {
    private final BookDao bookDao;
    public FragmentFactoryImpl(BookDao bookDao) {
        this.bookDao = bookDao;
    }
    @NonNull 
    @Override
    public Fragment instantiate(
        @NonNull ClassLoader classLoader, 
        @NonNull String className, 
        @Nullable Bundle args) {
        if(className.equals(MyFragment.class.getName())) {
            return new MyFragment(bookDao);
        }
        ...

然后,您的应用将崩溃,并带有以下异常:

//无法实例化片段:确保类名存在,是公共的,并且有一个空的公共构造函数
Unable to instantiate fragment: make sure class name exists, is public, and has an empty constructor that is public

最初,我打算使用参数编写此示例 String title,但是 fragment.setArguments(Bundle) 即使使用 FragmentFactory,发送动态参数仍需要使用,哎呀!fragment.setArguments(Bundle) 如果我们想要稳定的应用程序,则无法使用。

2.5 通过创建时的 setters 在 Fragment 上设置变量

如果您的代码如下所示:

val dialogFragment = MyDialogFragment()

dialogFragment.onDateSelectedListener = this

dialogFragment.show(supportFragmentManager, "dialog")

很有可能,系统重新创建对话框时,不会设置侦听器。

如果未使用将参数通过 setArguments() 发送给 Fragment ,则应在重新创建时将这些参数再次设置为 Fragment 。

class MyActivity: AppCompatActivity(), DateSelectedListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
            val dialog: DialogFragment? = supportFragmentManager.findFragmentByTag("dialog")
                if(dialog != null) {
                    dialog.onDateSelectedListener = this
                }
    }
}

当然,如果我们忘记了这样做,通常情况下,我们只会得到一个 DatePickerDialog,当选择日期时它不会执行任何操作。提供 DialogFragment 的回调时,正确的解决方案是使用 setTargetFragment()。

但是,同一错误实际上可能会导致崩溃,例如,当以 “因为实施 Parcelable 会花费太多精力” 的相同方式传递数据列表时,数据就不再存在。

可能是,您不需要 Parcelable,您需要本地数据持久性。

另一种情况是,是否要在 Fragment 实例上使用这些设置器来“注入依赖项”。这些依赖关系在娱乐中将不存在,并且应用程序将崩溃。

2.6 在 Fragments 之间使用 Jetpack ViewModel 共享状态,而无需使用 SavedStateHandle(或等效方法)

当您具有如下所示的代码时:

    private val currentFilter = MutableLiveData(TasksFilterType.ALL)
    fun currentFilter(): LiveData<TasksFilterType> = currentFilter

但是您没有如下代码:

class MyViewModel: ViewModel() {
    fun saveState(bundle: Bundle) {
        bundle.putSerializable("currentFilter", currentFilter.value!!)
    }
    fun restoreState(bundle: Bundle) {
        currentFilter.value = bundle.getSerializable("currentFilter") as TasksFilterType
    }
}

class MyActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
            viewModel = ViewModelProviders.of(this)
            .get(MyViewModel::class.java, object: ViewModelProvider.Factory {
                override fun <T : ViewModel> create(vmClass: Class<T>) =
                    MyViewModel().also { vm ->
                    savedInstanceState?.run {
                    vm.restoreState(getBundle("viewModelState"))
                }
            }
        })
    }

    override fun onSaveInstanceState(bundle: Bundle) {
        super.onSaveInstanceState(bundle)
            bundle.putBundle("viewModelState", viewModel.saveState(Bundle())
    }
}

或者,当完全使用 Jetpack 时,您仍然没有看起来像这样的代码:

private val currentFilter = savedStateHandle.getLiveData("currentFilter", TasksFilterType.ALL)

然后,当最终用户尝试为其美味的圣诞晚餐拍照,然后他们返回您的应用程序时,您的应用程序将使最终用户感到沮丧!

从技术上讲,我之前已经提到过:使用 Jetpack 时,应使用来创建 ViewModel AbstractSavedStateViewModelFactory。这样,就有可能获得 SavingStateLiveData,它将自动正确保存/恢复状态。

2.7 在您的应用程序中的任何位置,完全没有任何静态的non-final变量(无需特殊考虑)

如果您的代码如下所示:

public class DataHolder {
    private DataHolder() {}
    public static List<Data> data = null;
}

或像这样:

object DataHolder {
    var data: List<Data>? = null
}

在初始屏幕上,您将看到以下内容:

class SplashActivity: AppCompatActivity() {
    ...
        DataHolder.data = data
        startActivity(Intent(this, SecondActivity::class.java))
        finish()
    ...
}

在第二个 Activity 中,您将得到以下内容:

class SecondActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
            DataHolder.data!!.let { data ->
            ...
        }
    }
}

然后您的应用将崩溃!

Android 重新打开应用程序时,它将还原任务堆栈中的最后一个最高活动。这意味着虽然 SplashActivity 可以执行操作的是那个 MAIN,但实际上并不是启动应用程序的那个!将会是 SecondActivity,并且 DataHolder 永远不会初始化。

这种数据加载将需要特殊考虑,例如,公开一个 LiveData 将对 inside 进行初始数据加载的操作 LiveData.onActive()。
(此 LiveData 将像您通常期望的那样驻留在 ViewModel 中,Activity 已经通过使用 LifecycleOwner 调用 watch 来“广播”其活动状态,并触发 LiveData 成为活动状态。)

您绝不能期望某个活动先前已在您的应用程序中执行过,这完全不能保证。
(注意:静态变量可以正常工作的一个特殊地方是变量仅由分配一次 Application.onCreate()。)

2.8 仅在 saveInstanceState == null 时执行初始数据加载

如果您有这样的代码:

class MainActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
            ...
            if(savedInstanceState == null) {
                viewModel.startDataLoading()
            }

然后,您的应用在重新启动后将永远不会开始加载数据。更不用说,不是 Activity 应该知道何时开始加载数据。与上述相同的规则适用:LiveData.onActive() 应该是确定该规则的规则,应通过观察结果集来触发数据加载。
(注意:您还可以通过触发 ViewModel 构造函数中的数据加载来确保至少一次调用数据加载,尽管这不会仅由于的影响而延迟和执行数据加载 onStart。当与进行镜像时,它可以在某些情况下正常工作另一个 ViewModel 生命周期回调,onCleared() —如果您按文档使用,则此回调将正确运行 ViewModelProvider)。

2.9 在 if(savedInstanceState == null)块之外执行初始 Fragment 事务

如果您的代码如下所示:

class MainActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
            supportFragmentManager.beginTransaction()
            .replace(R.id.container, FirstFragment())
            .commit()
    }

而且您没有如下代码:

    class MainActivity: AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)          
            if(savedInstanceState == null) {
                supportFragmentManager.beginTransaction()
                    .add(R.id.container, FirstFragment())
                    .commit()
            }

然后,您将覆盖系统重新创建的片段,所有导航历史记录都将丢失!

实际上,如果使用 addToBackStack(),则无法保证实际恢复的导航历史记录是什么。

而且,如果第一个代码段已使用 add(),则最终会出现多个重叠的 fragments。不,将可点击的 全屏 背景设置为背景颜色以隐藏重叠的 fragments 不是一个真正的解决方案!

2.10 使用 onResume / onPause 进行除启用导航操作或检索摄像头以外的任何操作

这实际上不是进程死亡的 bug,但仍然值得一提。

如果您的代码如下所示:

override fun onResume() { 
    super.onResume() 
        viewModel.events.observe(this /*viewLifecycleOwner*/, Observer {
            // ...
        })
}

或者您有如下代码:

override fun onPause() {
    super.onPause()
        compositeDisposable.clear()
}

可能是,您的应用程序使用起来非常令人沮丧,并且可能潜在地存在一些隐藏的错误!

onResume 可以运行多次:将应用程序置于后台几次并使其变为前台,它将每次运行。如果您在中注册了观察者 onResume,但是仅在中删除了这些观察者 onDestroy,那么您的观察员将运行很多次!

如果您停止在中观察事件总线的事件 onPause,则通过在应用程序上放置 DialogFragment,IntentChooser 或运行时权限请求对话框,您会突然开始忽略事件。如果未将这些事件排入队列,则可能导致神秘的错误(“为什么我的代码无法运行?”)。

如果用户希望在手机上使用多窗口模式,那么让另一个应用程序突然处于焦点状态会使该应用程序“在再次轻按之前不再起作用”是非常令人沮丧的。因此,在移动 clear() 中 onStop() 是一个更好的选择:这是什么原因 LiveData 变为无效的内部 onStop(),而不是 onPause()。

尽管 Android 10 已经支持多简历(多窗口中的所有应用都已恢复),但 Android 10 目前并不常见。

说到这,如果您正在编写相机应用程序,则可能还需要处理新的生命周期回调 onTopResumedActivityChanged,以使其在 Android 10 上正常运行。

三、结论

了解在 Android 代码库中识别可能导致生产中令人讨厌的崩溃以及用户脾气暴躁的模式。最好的礼物是拥有 99.9%+ 无崩溃率的应用程序。

Author | Gabor Varadi ,
译者 | xmamiga