WebView 内存泄漏
解决方案网上很多,
- 开启一个新的进程,使用完毕后再关闭该进程。
- 使用代码方式构建 取代在 Xml 布局文件中声明 WebView ,原因是WebVIew在布局文件中声明 会持有当前Activity的引用,用代码实现的,构建WebView时候传递当前Activity的弱引用,页面退出时候,在OnDestroy里再尝试去释放,这么个做法.
问题
- 为什么在布局文件中声明的webView会持有当前Activity 呢?(其实也不局限于WebView了)
思路.
布局文件中的xml文件,都是通过Activity的setContentView 来 展示
而Activity 的 setContentView 其实是 PhoneWindow的 setContentView
代码如下
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
//mContentParent 就是 com.android.internal.R.id.content对应的view,我们自己的xml文件生成的view ,就会添加到mContentParent中
if (mContentParent == null) {
installDecor(); //生成 Decor ,每一个窗口对应一个Decor, 可以把Decor 理解成 mContentParent的 容器
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent); // 这段代码就是把xml资源文件转换成对应的View的,我们稍后进去看.
}
}
我们的WebView 作为布局文件中的一个节点,继承路径是 WebView > MockView > FrameLayout > ViewGroup > View
而View的构造,除了一个测试用的无参构造,其他的都是包含Context的,思路也就清晰了。
接下来就是看下Android怎么把布局文件中的View实例化的。
跟随代码进入 LayoutInflater类的 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
View view = tryInflatePrecompiled(resource, res, root, attachToRoot); // 这个讲是Android 9 新增的提前编译布局文件功能,貌似不会走这里
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot); // 生成View的具体方法在这里。
} finally {
parser.close();
}
}
接着上代码 ,进入 LayoutInflater类的 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs); // 看这里 ,根据标签创建 View . 接着进去
ViewGroup.LayoutParams params = null;
}
LayoutInflater类的createViewFromTag
方法,最终会调用 LayoutInflater类的 createView 方法,该方法是通过反射机制来构造View的,用的 是 双参的构造方法, >> final Object[] mConstructorArgs = new Object[2];
代码
@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class); // 这里,就能理解为什么自定义View 在布局文件中声明时候,必须要带 包名且类名须正确无误的必要性了
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext; // 参数0 xxContext . 文章开篇问题的 答案就在这里咯
Object[] args = mConstructorArgs;
args[1] = attrs; // 参数1
try {
final View view = constructor.newInstance(args); // 反射调用构造,生成View
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
}