在 Editor.updateCursorposition tionMz 中 Meizu 设备上的 NullPointerException

最近,我的 Android 应用程序在魅族设备 只有(M5c,M5s,M5 Note)上出现了崩溃。 Android 版本: 6.0。

下面是完整的堆栈跟踪:

Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.text.Layout.getLineForOffset(int)' on a null object reference
at android.widget.Editor.updateCursorPositionMz(Editor.java:6964)
at android.widget.Editor.updateCursorsPositions(Editor.java:1760)
at android.widget.TextView.getUpdatedHighlightPath(TextView.java:5689)
at android.widget.TextView.onDraw(TextView.java:5882)
at android.view.View.draw(View.java:16539)
at android.view.View.updateDisplayListIfDirty(View.java:15492)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3719)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3699)
at android.view.View.updateDisplayListIfDirty(View.java:15443)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:286)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:292)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:327)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:3051)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2855)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2464)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1337)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6819)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:894)
at android.view.Choreographer.doCallbacks(Choreographer.java:696)
at android.view.Choreographer.doFrame(Choreographer.java:631)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:880)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5969)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:830)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:720)

与我的代码没有直接关系(即使在其他线程的 stracktrace 中也是如此)。我只知道它发生在有 TextView 的片段中的 每次都是。这可能发生在 TextView 获得焦点的时候,但我无法确定。当然,我不能复制这个错误,除非我买一个魅族。

Also, since the top method is called updateCursorPositionMz, it looks to me like this may be an internal issue in Meizu's FlymeOS ("Mz" = "Meizu"?).

有人已经有这个问题,知道原因和如何解决它?

谢谢。

10749 次浏览

更新(2019年8月8日)

正如@andreas-wenger,@waseefakhtar 和@vadim-kotov 提到的那样,从 原材料: 1.1.0-alpha08开始,这个补丁就包含在内了。

旧答案

最后我终于有机会把我的手放在魅族身上。正如我所想的,每当用户点击某个字段以获得焦点时,就会发生崩溃。

在我的例子中,我在 TextInputLayout内部有一些 android.support.design.widget.TextInputEditText。简单地用 AppCompatEditText代替这些 TextInputEditText就解决了问题,像这样:

<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="...">


<android.support.v7.widget.AppCompatEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>


</android.support.design.widget.TextInputLayout>

行为保持不变(因为 TextInputEditText扩展了 AppCompatEditText)。但是我还没有找到问题的根本原因。

从 xml 删除提示: 不管是从 TextInputLayout 文本输入输出布局还是从 编辑文本

材料组件

<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">


<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_input_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>

设计支援

<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">


<android.support.design.widget.TextInputEditText
android:id="@+id/text_input_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.TextInputLayout>

在你的代码集提示 程序化:

val myTextInputEditText = findViewById<TextInputEditText>(R.id.text_input_edit_text)
myTextInputEditText.hint = "Your hint"

魅族 M5S,Android 6.0上测试

在我的例子中,我验证了使用 AppCompatEditText而不是 TextInputEditText确实可以防止崩溃,但是我们不能使用这个解决方案。我们使用一个带有扩展 TextInputEditText的视图的 sdk,因此切换到 AppCompatEditText将需要将相当多的 sdk 代码复制/修改到我们的项目中。

我试着在 TextInputEditTextTextInputLayout上设置暗示,但是我最终看到了一个双重暗示(像模糊的文字,我确定我没有喝太多)。

我看了一下@Andrew: https://github.com/android-in-china/Compatibility/issues/11链接到的 GitHub 问题

在这个问题上,他们解释说,根本原因是 Meizu 的问题,因为 TextInputEditText.getHint()TextInputEditText.mHint不同。

TextInputEditText位于 TextInputLayout内部,并且提示是在 TextInputEditText的 xml 中指定的时候,支持库基本上将提示“移动”到包含它的 TextInputLayout: 它将提示设置在容器上,然后将其设置为编辑文本上的 null。

这个来源是在 SetEditText ():

    // If we do not have a valid hint, try and retrieve it from the EditText, if enabled
if (hintEnabled) {
if (TextUtils.isEmpty(hint)) {
// Save the hint so it can be restored on dispatchProvideAutofillStructure();
originalHint = this.editText.getHint();
setHint(originalHint);
// Clear the EditText's hint as we will display it ourselves
this.editText.setHint(null);
}

然后,当您调用 TextInputEditText.getHint()时,它将返回容器的提示。

getHint()(提示值)和 mHint(空值)之间的这种不一致性似乎给魅族设备带来了问题

我找到了另一种方法来避免这个问题。

在 Meizu 设备方面,我:

1)以编程方式将 TextInputEditText的提示重置为最初从 xml 设置的值(通过调用其重写的 getHint()返回容器的提示)。

2)将 TextInputEditText的提示颜色设置为透明,以避免双重/模糊提示效果:

private void hackFixHintsForMeizu(TextInputEditText... editTexts) {
String manufacturer = Build.MANUFACTURER.toUpperCase(Locale.US);
if (manufacturer.contains("MEIZU")) {
for (TextInputEditText editText : editTexts) {
editText.setHintTextColor(Color.TRANSPARENT);
editText.setHint(editText.getHint());
}
}
}

TextInputLayoutTextInputEditText上添加提示为我修复了崩溃:

    <android.support.design.widget.TextInputLayout
android:id="@+id/text_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login"
app:hintAnimationEnabled="false">


<android.support.design.widget.TextInputEditText
android:id="@+id/text_input_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login" />
</android.support.design.widget.TextInputLayout>

最后,以编程方式重置 TextInputEditText的提示,以避免提示文本颜色过深:

editText = findViewById(R.id.text_input_edit_text);
editText.setHint("");

在魅族 MX6上用 Android 6.0进行验证

我的解决方案是基于 https://github.com/android-in-china/Compatibility/issues/11#issuecomment-427560370中提到的 FixedTextInputEditText

首先,我创建了一个固定的 TextInputEditText实例:

public class MeizuTextInputEditText extends TextInputEditText {
public MeizuTextInputEditText(Context context) {
super(context);
}


public MeizuTextInputEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}


public MeizuTextInputEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}


@Override
public CharSequence getHint() {
try {
return getMeizuHintHack();
} catch (Exception e) {
return super.getHint();
}
}


private CharSequence getMeizuHintHack() throws NoSuchFieldException, IllegalAccessException {
Field textView = TextView.class.getDeclaredField("mHint");
textView.setAccessible(true);
return (CharSequence) textView.get(this);
}
}

但是那样的话,我就必须用 MeizuTextInputEditText代替我所有的 TextInputEditText用法,而这并不是在一个更大的代码库上可以轻易做到的事情。另外,在创建未来视图时,您总是需要考虑使用 MeizuTextInputEditText而不是“破碎”视图。忘记它将很容易再次引入生产问题。

因此,最终的修复包括自定义视图类,再加上视图泵库(https://github.com/InflationX/ViewPump) ,我们可以很容易地做到这一点。正如文档中解释的那样,您需要注册一个定制的拦截器,它看起来像下面这个:

public class TextInputEditTextInterceptor implements Interceptor {
@Override
public InflateResult intercept(Chain chain) {
InflateRequest request = chain.request();
View view = inflateView(request.name(), request.context(), request.attrs());


if (view != null) {
return InflateResult.builder()
.view(view)
.name(view.getClass().getName())
.context(request.context())
.attrs(request.attrs())
.build();
} else {
return chain.proceed(request);
}
}


@Nullable
private View inflateView(String name, Context context, AttributeSet attrs) {
if (name.endsWith("TextInputEditText")) {
return new MeizuTextInputEditText(context, attrs);
}
return null;
}
}

注册自定义拦截器就像在文档中一样,通过在活动的 onCreate 上设置 View泵来完成:

@Override
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewPump.Builder viewPumpBuilder = ViewPump.builder();
if (isMeizuDevice()) {
viewPumpBuilder.addInterceptor(new TextInputEditTextInterceptor());
}
ViewPump.init(viewPumpBuilder.build());
}

正如你所看到的,我只有在检测到魅族设备时才给 MeizuTextInputEditText充气。这样,对于不需要反射的设备,就不会触发反射。此外,这个方法是一个基本的活动类,我有从其他每个活动扩展到我的项目,所以每个活动,是在我的项目启动和设备是一个魅族将自动修复!

我使用的是 Kotlin 和片段,我只是递归修复 onViewCreated 中的所有文本输入。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fixTextInputEditText(view) // call this in onViewCreated
}


private fun fixTextInputEditText(view: View) {
val manufacturer = Build.MANUFACTURER.toUpperCase(Locale.US)
if ("MEIZU" in manufacturer) {
val views = getAllTextInputs(view)
views.forEach(::hackFixHintsForMeizu)
}
}


private fun getAllTextInputs(v: View): List<TextInputEditText> {
if (v !is ViewGroup) {
val editTexts = mutableListOf<TextInputEditText>()
(v as? TextInputEditText)?.let {
editTexts += it
}
return editTexts
}


val result = mutableListOf<TextInputEditText>()
for (i in 0 until v.childCount) {
val child = v.getChildAt(i)
result += getAllTextInputs(child)
}
return result
}


private fun hackFixHintsForMeizu(editText: TextInputEditText) {
if (editText.hint != null) {
editText.setHintTextColor(Color.TRANSPARENT)
editText.hint = editText.hint
}
}

这只是在 Android lib 的材料组件中修复的,参见: Https://github.com/material-components/material-components-android/pull/358

以上所有的变体都经过了修改,对我没有效果。

我的应用程序使用片段,TextInputEditText 有时使用没有 TextInputLayout,升级到最新的 AndroidX 在这个时候不是选项,替换 TextInputEditText 在这个时候也不是一个选项。

我的版本(基于这些解决方案和谷歌的修复) :

import android.os.Build
import java.util.*
import android.content.Context
import android.support.design.widget.TextInputEditText
import android.util.AttributeSet
import android.widget.TextView
import android.support.design.widget.TextInputLayout
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import java.lang.reflect.Field
import java.lang.reflect.Method
import android.support.design.R


class MyInputEditText(context: Context?, attrs: AttributeSet?,defStyleAttr:Int) : TextInputEditText(context, attrs,defStyleAttr){


constructor(context: Context?, attrs: AttributeSet?):this(context,attrs,R.attr.editTextStyle)
constructor(context: Context?):this(context,null,R.attr.editTextStyle)




private val buggyMeizu = ("meizu") in Build.MANUFACTURER.toLowerCase(Locale.US)


private lateinit var getTextInputLayoutMethod:Method
private lateinit var providesHintMethod:Method
private lateinit var mHintField:Field


init {
if (buggyMeizu) {
getTextInputLayoutMethod=TextInputEditText::class.java.getDeclaredMethod("getTextInputLayout")
getTextInputLayoutMethod.isAccessible=true


providesHintMethod=TextInputLayout::class.java.getDeclaredMethod("isProvidingHint")
providesHintMethod.isAccessible=true


mHintField=TextView::class.java.getDeclaredField("mHint")
mHintField.isAccessible=true
}
}




private fun getTILProvidesHint():Boolean {
val layout=getTIL()
if (layout!=null) {
val result=providesHintMethod.invoke(layout) as Boolean
return result;
} else {
return false
}
}


private fun getTIL():TextInputLayout? = getTextInputLayoutMethod.invoke(this) as TextInputLayout?


private fun getBaseHint():CharSequence? = mHintField.get(this) as CharSequence?


override fun getHint(): CharSequence? {
if (!buggyMeizu) {
return super.getHint()
} else {
val layout=getTIL()
return if (layout != null && (getTILProvidesHint()) )
layout.hint
else
provideHintWrapped()
}
}




override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
val needHint=(outAttrs.hintText==null)
val ic = super.onCreateInputConnection(outAttrs)
if (buggyMeizu) {
if (ic != null && needHint) {
outAttrs.hintText = this.provideHintWrapped()
}
}
return ic
}


private fun provideHintWrapped():CharSequence? {


val hintFromLayout=getHintFromLayoutMine()
if (hintFromLayout!=null) {
return hintFromLayout
} else {
val baseHint=getBaseHint()
if (baseHint!=null) {
return baseHint
} else {
return null
}
}


}
private fun getHintFromLayoutMine(): CharSequence? {
val layout = getTIL()
return layout?.hint
}


override fun onAttachedToWindow() {


if (buggyMeizu) {


val baseHint=getBaseHint()


if (getTIL() != null
&& getTILProvidesHint()
&& baseHint == null) {
this.hint=""
}
}


super.onAttachedToWindow()
}
}

之后,在所有布局和代码文件中用 MyInputEditText 查找并替换 TextInputEditText。