Android 输入法导致内存泄露以及WebView内存泄露解决方案

如果开发到一定程度肯定会去处理内存的问题,因为不处理该问题的话,轻一点会导致应用频繁GC,严重的情况下会产生内存溢出导致崩溃。而只要讲到内存溢出经常会遇到一句话,使用Activity的Context导致,而Context被引用,最后导致Activity泄露。

1、查找泄露原因

既然是查询View持有Activity的引用导致的内存泄露,那么就从Activity的setContentView看起。

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

通过代码一步一步查找,发现最后是实现了PhoneWindow的setContentView的方法。PhoneWindow位于\frameworks\base\policy\src\com\android\internal\policy\impl\PhoneWondow.java;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//最后还是在这里实现,如果mContentParent不为null,mContentParent直接把layoutResID的布局添加到根布局
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}

可以看见Activity的setContentView(int layoutRes)最后还是用LayoutInflater实现了。那我们再看mLayoutInflater是什么时候生成的,代码如下:

1
2
3
4
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}

可以看出是在PhoneWindow生成时一起生成的,那么PhoneWindow什么生成的,继续看Activityd的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
//这里可以看出把Activity当成Context传给PhoneWindow了,并用这个Context生成了LayoutInflater。
mWindow = new PhoneWindow(this);
....
省略
....
}

这里我们可以看到Activity传递到了PhoneWindow,并通过Activity生成了LayoutInflater。这里记住,下面分析时要用到。

既然是Activity被View持有,那么我们就从View何时被赋值Context出发。那么我先列出View有多少种生成方法。
1、构造函数生成

1
2
public View(Context context) {
}

对构造函数生成,可以看出在这个方法中我们可以直接控制Context,也就是说我们自己可以控制是否直接使用Activity,可以直接使用Application,当然会出些问题,原因以及解决方法下面再说。

2、LayoutInflater方法生成

然后再看LayoutInflater如何生成View,在这之前我先找到LayoutInflater的实现类,因为你会发现LayoutInflater是抽象的。现在从LayoutInflater的生成方法开始看起。

1
2
3
4
5
6
7
8
9
10
11
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}

调用context的getSystemService方法,一直往上找会发现最后调用了ContextImpl的getSystemService的方法。
LayoutInflater实例化的地方为ContextImpl,位于\frameworks\base\core\java\android\app\ContextImpl.java。代码为:

1
2
3
4
5
6
7
8
9
10
11
12
static {
....
省略
....
registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
}});
....
省略
....
}

通过一步一步的代码跟踪,发现了LayoutInflater的实现类BridgeInflater,位于frameworks\base\tools\layoutlib\bridge\src\android\view\BridgeInflater.java,在这我们只需要关心onCreateView方法。
最后调用PolicyManager的makeNewLayoutInflater方法生成,同时传入ctx.getOuterContext(),这就是LayoutInflater所持有的Context,这就是ContextImpl生成时传入ContextImpl的Context。ContextImpl的对象生成代码位于\frameworks\base\core\java\android\app\ActivityThread.java:

1
2
3
4
5
6
7
8
9
10
11
private Context createBaseContextForActivity(ActivityClientRecord r,
final Activity activity) {
生成ContextImpl并把Activity通过setOuterContext方法赋值给ContextImpl。
ContextImpl appContext = new ContextImpl();
appContext.init(r.packageInfo, r.token, this);
appContext.setOuterContext(activity);
....
省略
....
return baseContext;
}

可以看出Activity和ContextImpl互相持有,那么Application和ContextImpl也互相持有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
public final class BridgeInflater extends LayoutInflater {
private final IProjectCallback mProjectCallback;
private boolean mIsInMerge = false;
private ResourceReference mResourceReference;
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit."
};
protected BridgeInflater(LayoutInflater original, Context newContext) {
//把Context传递给LayoutInflater父类
super(original, newContext);
mProjectCallback = null;
}
public BridgeInflater(Context context, IProjectCallback projectCallback) {
//把Context传递给LayoutInflater父类
super(context);
mProjectCallback = projectCallback;
mConstructorArgs[0] = context;
}
@Override
public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
View view = null;
try {
for (String prefix : sClassPrefixList) {
try {
//调用LayoutInflater的createView方法
view = createView(name, prefix, attrs);
if (view != null) {
break;
}
} catch (ClassNotFoundException e) {
// Ignore. We'll try again using the base class below.
}
}
try {
if (view == null) {
view = super.onCreateView(name, attrs);
}
} catch (ClassNotFoundException e) {
// Ignore. We'll try again using the custom view loader below.
}
// Finally try again using the custom view loader
try {
if (view == null) {
view = loadCustomView(name, attrs);
}
} catch (ClassNotFoundException e) {
// If the class was not found, we throw the exception directly, because this
// method is already expected to throw it.
throw e;
}
} catch (Exception e) {
// Wrap the real exception in a ClassNotFoundException, so that the calling method
// can deal with it.
ClassNotFoundException exception = new ClassNotFoundException("onCreateView", e);
throw exception;
}
setupViewInContext(view, attrs);
return view;
}
@Override
public View createViewFromTag(View parent, String name, AttributeSet attrs) {
View view = null;
try {
//调用LayoutInflater的createViewFromTag方法
view = super.createViewFromTag(parent, name, attrs);
} catch (InflateException e) {
// try to load the class from using the custom view loader
try {
view = loadCustomView(name, attrs);
} catch (Exception e2) {
// Wrap the real exception in an InflateException so that the calling
// method can deal with it.
InflateException exception = new InflateException();
if (e2.getClass().equals(ClassNotFoundException.class) == false) {
exception.initCause(e2);
} else {
exception.initCause(e);
}
throw exception;
}
}
setupViewInContext(view, attrs);
return view;
}
@Override
public View inflate(int resource, ViewGroup root) {
Context context = getContext();
if (context instanceof BridgeContext) {
BridgeContext bridgeContext = (BridgeContext)context;
ResourceValue value = null;
Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource);
if (layoutInfo != null) {
value = bridgeContext.getRenderResources().getFrameworkResource(
ResourceType.LAYOUT, layoutInfo.getSecond());
} else {
layoutInfo = mProjectCallback.resolveResourceId(resource);
if (layoutInfo != null) {
value = bridgeContext.getRenderResources().getProjectResource(
ResourceType.LAYOUT, layoutInfo.getSecond());
}
}
if (value != null) {
File f = new File(value.getValue());
if (f.isFile()) {
try {
XmlPullParser parser = ParserFactory.create(f);
BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
parser, bridgeContext, false);
//调用LayoutInflater的inflate方法
return inflate(bridgeParser, root);
} catch (Exception e) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
"Failed to parse file " + f.getAbsolutePath(), e, null /*data*/);
return null;
}
}
}
}
return null;
}
....
省略
....
}

最后还是会调用LayoutInflater的inflate的方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
//获取页面的xml资源解析
final XmlResourceParser parser = res.getLayout(resource);
try {
//解析xml资源文件
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
//设置View构造函数的参数,设置Context参数为LayoutInflater自带的context,下面createView方法会使用到。
mConstructorArgs[0] = inflaterContext;
....
省略
....
//创建View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
....
省略
....
}

上面代码就是创建的一下流程,可以看出View是LayoutInflater创建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
....
省略
....
//View 最后调用了onCreateView方法生成了View,mFactory2、mFactory默认为null,最后会调用onCreateView方法。
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
....
省略
....
}

最后还是到了createView方法。

1
2
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
....
省略
....
//获取默认构造,使用View(Context context, @Nullable AttributeSet attrs)
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
....
省略
....
Object[] args = mConstructorArgs;
args[1] = attrs;
//此处args参数中args[0]已结设置为LayoutInflater,上面中已经提及。
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
....
省略
....
}

从上面分析可以看出最后和View绑定的Context就是LayoutInflater所持有的Context。这时我们就要着重分析LayoutInflater持有的Context到底是不是Context,如果是那么是什么时候赋值的,那我们再研究一下LayoutInflater的生成方法:

1
2
3
4
5
6
7
8
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}

如果研究过Activity和Application的源码我们会发现Activity的源码继承类比Activity多了一层,那就是Activity继承于ContextThemeWrapper,而Application继承于ContextWrapper,他们都对getSystemService方法进行了重写,ContextThemeWrapper的重写代码为:

1
2
3
4
5
6
7
8
9
@Override public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}

而ContextWrapper重写的代码为:

1
2
3
4
@Override
public Object getSystemService(String name) {
return mBase.getSystemService(name);
}

ContextThemeWrapper继承于ContextWrapper,而getBaseContext()方法如下:

1
2
3
public Context getBaseContext() {
return mBase;
}

可以看出ContextThemeWrapper和ContextWrapper最终实现除了cloneInContext方法外都一样,最终都是通过自己持有的ContextImpl对象执行getSystemService方法来获取LayoutInflater对象。而前面分析可知最终都会用ContextImpl对象所持有的mOuterContext对象,即Application和Activity生成LayoutInflater对象,进而LayoutInflater对象持有Activity或Application。我们在看一下ContextThemeWrapper重写中用到的cloneInContext方法。

1
2
3
4
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new BridgeInflater(this, newContext);
}

可以看出该方法的作用就是克隆,只是把新生成LayoutInflater对象的Context对象替换掉。其实我有点不明白,因为ContextImpl方法中生成LayoutInflater用到的Context就是Activity本身,为什么还要这一步。到这一步可以很容易的看出View为什么会持有Activity了。逻辑整理如下:

PhoneWindow 用Activity生成LayoutInflater

LayoutInflater对象持有Activity;

LayoutInflater在生成View对象时把Activity赋值给View,

View 持有Activity的引用。

##2、解决方案##

我的思路很简单既然是因为View持有Activity的引用,那就不让Activity的强引用就可以了。

从以上代码分析可以看出Activity之所以会被持有都是因为PhoneWindow中生成LayoutInflater对象调用了Activity的getSystemService方法。该方法生成的对象持有Activity对象的引用。一开始我准备直接在BaseActivity中重写的getSystemService方法。最早为:

1
2
3
4
5
6
7
8
9
10
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
//使用代理类
baseContextWrappernew = new BaseContextWrapper(this);
mInflater = LayoutInflater.from(BaseApplication.getInstance());
}
return mInflater;
}
return super.getSystemService(name);

但这种方法有个严重的问题,就是Activity的很多主题样式不能使用了,所以有看了ContextThemeWrapper的方法,自己继承ContextWrapper类,重写其中的几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//创建代理类
public class BaseContextWrapper extends ContextWrapper {
static Field field;
static {
try {
field = ContextWrapper.class.getDeclaredField("mBase");
field.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
Context mContext;
public BaseContextWrapper(Context context) {
super(null);
mContext = context;
if(context instanceof ContextWrapper){
context = ((ContextWrapper)context).getBaseContext();
}
attachBaseContext(context);
}
//当获取主题信息时直接调用Activity的getTheme方法。
@Override
public Resources.Theme getTheme() {
if(mContext != null){
return mContext.getTheme();
}
return super.getTheme();
}
@Override public Object getSystemService(String name) {
if(mContext == null){
return super.getSystemService(name);
}
return mContext.getSystemService(name);
}
//释放Activity的持有。
public void recycle(){
try {
field.set(this,null);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public Activity getActivityContext(){
Activity context = (Activity) mContext;
return context;
}
}

这样View就只会持有BaseContextWrapper对象,当Activity销毁时会执行recycle方法释放Activity已经ContextImpl。这样View已经不能直接持有Activity了,但还有一个问题就是View本身持有包含它的ViewGroup就是ViewParent,这里只需要去除持有关系即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ViewGroup viewGroup = (ViewGroup) getWindow().getDecorView();
//去除方法要在BaseContextWrapper的recycle方法之前执行。
clearView(viewGroup);
if(baseContextWrappernew != null){
baseContextWrappernew.recycle();
}
private void clearView(View viewClear){
if(viewClear instanceof ViewGroup){
int length = ((ViewGroup)viewClear).getChildCount();
View view = null;
for(int i=0;i< length;i++){
view = ((ViewGroup) viewClear).getChildAt(i);
clearView(view);
}
try {
((ViewGroup) viewClear).removeAllViews();
} catch(Exception e){
}
}
}

这些只是清楚了VIew和Activity的基本持有关系,但各种事件监听之类的要自己注意去除。

##3、LayoutInflater 的Activity or Application##
从我上面的分析其实已经可以看出了部分区别。

1、就是用Activity那么View持有的Context就是Activity,如果用的是Application那么View持有的Context就是Application,而有些人比较喜欢直接用View的Context转为Activity,如果用Application那时就会出错。
2、Application继承自ContextWrapper,而Activity继承自ContextThemeWrapper,而ContextThemeWrapper有时对ContextWrapper的继承,对其中一部分方法进行了修改,这也就是不同所在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//在Activity生成时会获取Activity的主题,并调用该方法
@Override public void setTheme(int resid) {
mThemeResource = resid;
initializeTheme();
}
/** @hide */
@Override
public int getThemeResId() {
return mThemeResource;
}
@Override public Resources.Theme getTheme() {
if (mTheme != null) {
return mTheme;
}
mThemeResource = Resources.selectDefaultTheme(mThemeResource,
getApplicationInfo().targetSdkVersion);
initializeTheme();
return mTheme;
}
//Activity的主题产生作用。覆盖Application的相同属性
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
theme.applyStyle(resid, true);
}
//生成主题并获取Application的主题赋值给新主题,并使Activity的主题产生作用。
private void initializeTheme() {
final boolean first = mTheme == null;
if (first) {
mTheme = getResources().newTheme();
Resources.Theme theme = mBase.getTheme();
if (theme != null) {
mTheme.setTo(theme);
}
}
//Activity的主题产生作用。
onApplyThemeResource(mTheme, mThemeResource, first);
}

这里就已经产生基本的不同了,比如EditText 的字体颜色 设置属性为?textcolor,这时如果Application设置为黑色,而Activity的主题中设置为红色,那么如果这个Activity的某个EditText用了Application,那么这时字体会显示黑色而不是预期的红色。