记一次overloadaggressively造成的崩溃

将Kotlin版本从1.1.2-4升级到1.3.11后,发现打的release包开机就会崩溃:崩溃日志为

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
java.lang.RuntimeException: Unable to instantiate application com.tencent.sigma.patch.HotPatchApplication: java.lang.ClassNotFoundException: Didn't find class "com.tencent.sigma.patch.HotPatchApplication" on path: DexPathList[[zip file "/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk"],nativeLibraryDirectories=[/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/lib/arm, /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk!/lib/armeabi, /system/lib, /vendor/lib]]
at android.app.LoadedApk.makeApplication(LoadedApk.java:993)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5801)
at android.app.ActivityThread.-wrap1(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1683)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:173)
at android.app.ActivityThread.main(ActivityThread.java:6650)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:818)
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.tencent.sigma.patch.HotPatchApplication" on path: DexPathList[[zip file "/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk"],nativeLibraryDirectories=[/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/lib/arm, /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk!/lib/armeabi, /system/lib, /vendor/lib]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:125)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at android.app.Instrumentation.newApplication(Instrumentation.java:1088)
at android.app.LoadedApk.makeApplication(LoadedApk.java:987)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5801) 
at android.app.ActivityThread.-wrap1(Unknown Source:0) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1683) 
at android.os.Handler.dispatchMessage(Handler.java:106) 
at android.os.Looper.loop(Looper.java:173) 
at android.app.ActivityThread.main(ActivityThread.java:6650) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:818) 
Suppressed: java.io.IOException: Failed to open dex files from /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk because: Failure to verify dex file '/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk': Out-of-order annotation_element name_idx: 7150 then 7150
at dalvik.system.DexFile.openDexFileNative(Native Method)
at dalvik.system.DexFile.openDexFile(DexFile.java:353)
at dalvik.system.DexFile.<init>(DexFile.java:100)
at dalvik.system.DexFile.<init>(DexFile.java:74)
at dalvik.system.DexPathList.loadDexFile(DexPathList.java:374)
at dalvik.system.DexPathList.makeDexElements(DexPathList.java:337)
at dalvik.system.DexPathList.<init>(DexPathList.java:157)
at dalvik.system.BaseDexClassLoader.<init>(BaseDexClassLoader.java:65)
at dalvik.system.PathClassLoader.<init>(PathClassLoader.java:64)
at com.android.internal.os.ClassLoaderFactory.createClassLoader(ClassLoaderFactory.java:73)
at com.android.internal.os.ClassLoaderFactory.createClassLoader(ClassLoaderFactory.java:88)
at android.app.ApplicationLoaders.getClassLoader(ApplicationLoaders.java:69)
at android.app.ApplicationLoaders.getClassLoader(ApplicationLoaders.java:35)
at android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:695)
at android.app.LoadedApk.getClassLoader(LoadedApk.java:729)
at android.app.LoadedApk.getResources(LoadedApk.java:956)
at android.app.ContextImpl.createAppContext(ContextImpl.java:2282)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5723)
... 8 more

乍一看,以为是HotPatchApplication被混淆的原因导致Class Not Found,于是尝试keep住HotPatchApplication这个类,但是发现问题依旧。于是又怀疑是前不久升级gradle对分包造成了影响,对apk中的dex文件进行分析,发现HotPatchApplication这个类稳稳的躺在第一个dex文件中。

后来又进行了以下的尝试:

​ 1.尝试按顺序更改Kotlin版本,最后,当Kotlin版本为1.3.0时,发现问题出现了。

​ 2.另一个线索是当我打开Proguard时,那就是minifyEnable true,崩溃就出现了。

经过以上排查,基本确定了问题出在Kotlin和混淆的身上。最后发现上述日志中有一行:

1
Suppressed: java.io.IOException: Failed to open dex files from /data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk because: Failure to verify dex file '/data/app/com.tencent.dreamreader-P2-qUIVdDrdEOhONvRwOtw==/base.apk': Out-of-order annotation_element name_idx: 7150 then 7150

这个报错是第一次见到,上网搜了下,最终定位在混淆时的一个参数身上:-overloadaggressively

ProGuard官网解释:

-overloadaggressively Specifies to apply aggressive overloading while obfuscating. Multiple fields and methods can then get the same names, as long as their arguments and return types are different, as required by Java bytecode (not just their arguments, as required by the Java language). This option can make the processed code even smaller (and less comprehensible). Only applicable when obfuscating.

可以看出,开启该选项可能把不同的方法或者变量混淆成相同的名字,这样会尽可能的减小代码体积。但是由于过于激进,造成了dex文件中有重复annotation_element,这个问题在编译和打包时并不会报错,只有在开启软件时才会发现。

至于为什么升级了Kotlin才有这个问题,猜测原因是新版本的kotlin中加入了Keep注释与Java的Keep注释同名造成的。

解决办法:

去掉混淆文件(proguard.txt)中的-overloadaggressively

参考:

https://stackoverflow.com/questions/56458360/app-crashed-when-r8-enabled-with-existing-proguard-settings

https://issuetracker.google.com/issues/129241209#comment11

https://stackoverflow.com/questions/51948250/failure-to-verify-dex-file-out-of-order-annotation-element-name-idx

初识ViewPager2

ViewPager2是Google于2019年2月7号发布的ViewPager的升级版本,目前最新版本是1.0.0-alpha01。ViewPager2主要解决了传统ViewPager中的几个问题:

  • 不支持从右到左布局
  • 不支持纵向滚动
  • notifyDataSetChanged()有时不起作用的Bug

实现原理

查看ViewPager2内部代码可以看出,其实现原理是在其内部封装了一个RecyclerView,利用LinearLayoutManager+PagerSnapHelper实现了ViewPager的滚动效果以及对纵向滚动的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ViewPager2 extends ViewGroup {

private void initialize(Context context, AttributeSet attrs) {
mRecyclerView = new RecyclerView(context) {
...
};

mLayoutManager = new LinearLayoutManager(context);
mRecyclerView.setLayoutManager(mLayoutManager);
setOrientation(context, attrs);

new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
...
}
}

目前存在的问题:

由于直接使用的RecycleView实现,一些ViewPager的特性没有得到完全实现,官方指出了下面几个问题:

  • 不支持设置offscreen limit
  • 与TabLayout的集成存在问题
  • FragmentStateAdapter还有稳定性问题
  • 不支持设置pageWitch,默认100%/100%
  • 不支持clipToPadding
  • 不支持fakeDrag
  • 设置page transformer时,不支持设置绘制顺序

使用方法:

其使用方法与传统ViewPager类似:

1.在模块gradle中添加依赖:

1
implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha01'

2.在Layout文件中布局,这里可以设置滚动方向:

1
2
3
4
5
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />

3.创建Adapter,ViewPager2使用FragmentStateAdapter来实现Fragment的管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ViewPager2Adapter extends FragmentStateAdapter {

public ViewPager2Adapter(@NonNull FragmentManager fragmentManager) {
super(fragmentManager);
}

@NonNull
@Override
public Fragment getItem(int position) {
return new SimpleFragment();
}

@Override
public int getItemCount() {
return 10;
}
}

4.应用Adapter:

1
2
3
viewPager2 = findViewById(R.id.viewpager2);
ViewPager2Adapter adapter = new ViewPager2Adapter(getSupportFragmentManager());
viewPager2.setAdapter(adapter);

总结

ViewPager2具有原生支持垂直方向滑动的特性,但是由于目前FragmentStateAdapter还有稳定性问题,以及设置page transformer时,不支持设置绘制顺序,在实际应用时还需要多进行修改和测试。建议等官方推出稳定版本后,再考虑接入。

参考:

[1] https://developer.android.com/jetpack/androidx/releases/viewpager2

[2] https://developer.android.google.cn/reference/androidx/viewpager2/adapter/FragmentStateAdapter

[3] https://developer.android.google.cn/reference/androidx/viewpager2/widget/ViewPager2

[4] https://www.journaldev.com/26148/android-viewpager2

Android Studio的三种类型的模版(Templates)创建

Android Studio的三种类型的模版(Templates)创建

如果说使用快捷键是程序员的刀🔪,那灵活的使用代码模版就应该是程序员的剑。

这里说的模版(Templates),是指在使用开发创建类文件,甚至是某些代码块时,IDE自动按照规定的格式创建出类或代码的功能。如果类中有大量相似代码,使用模版可以极大的提高开发效率,降低出错概率。

下面我们看一下如何在Android Studio上使用模版,我将讲述三种模版的创建,分别是:

  • 使用Live Templates创建代码块模版
  • 使用File and Code Templates创建类模版
  • 基于FreeMarker创建多文件模版

1.代码块模版

java中经常需要定义这样的静态常量:

1
private static final int DEFAULT_VALUE = 1;

使用Live Templates后,就可以直接输入const就可以快速定义静态常量:

其设置方法是,打开File->Setting(⌘+,),搜索“Live Templates”,打开如下的界面:

其中,1指的是缩写和该缩写的描述;2中输入的是模版的内容;3中可以指定该模版应用生效的语言和场所,例如,可以限制该模版只应用到Java语言定义变量(declaration)的时候; 4可以将模版中变化的部分定义为变量,如上图中的${name}${value}

上面例子中的const是默认的模版,你可以点击加号添加自己的模版,例如为kotlin定义一个tag常量的模版:

Read more

对使用ViewPager的一点理解

大家都用过ViewPager吧😄,使用ViewPager时,需要给它配一个Adapter,通常我们要继承下面三个Adapter来,分别是:

———>最基本的PagerAdapter

——————>继承PagerAdapter的FragmentPagerAdapter

——————>继承PagerAdapter的FragmentStatePagerAdapter

那么这三个Adapter有什么区别,我们应该怎么选择呢?

1.关于PagerAdapter

首先,我们知道如果继承了PagerAdapter,需要我们实现下面两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* Return the number of views available.
*/
public abstract int getCount();


/**
* Create the page for the given position. The adapter is responsible
* for adding the view to the container given here, although it only
* must ensure this is done by the time it returns from
* {@link #finishUpdate(ViewGroup)}.
*
* @param container The containing View in which the page will be shown.
* @param position The page position to be instantiated.
* @return Returns an Object representing the new page. This does not
* need to be a View, but can be some other container of the page.
*/
public Object instantiateItem(ViewGroup container, int position) {
return instantiateItem((View) container, position);
}

第一个是要告诉ViewPager一共有几个页面,第二个则是ViewPager需要加载页面了,你需要根据position创建不同的页面对象,这里通常是View对象。

当然,如果你的ViewPager中内容比较复杂,需要用Fragment来自动管理其生命周期,那么可以使用FragmentPagerAdapter和FragmentStatePagerAdapter中的一种,那么他俩有什么区别呢?

首先我们看一下FragmentPagerAdapter的instantiateItemdestroyItem方法:

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
 	@Override
public Object instantiateItem(ViewGroup container, int position) {
...
final long itemId = getItemId(position);

// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
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);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
...
return fragment;
}


@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + ((Fragment)object).getView());
mCurTransaction.detach((Fragment)object);
}

public long getItemId(int position) {
return position;
}


private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}

可以看出instantiateItem方法在添加Fragment时,会带一个Fragment的名字,其实就是Tag:

1
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));

这个Tag是根据一定规则从makeFragmentName中获得的,这里唯一变化的就是id,默认的id可从getItemId(position)中看到,就是当前的position,保证了每个fragment的tag是唯一的。

当ViewPager通过instantiateItem获取Fragment的时候,会先根据之前设置的Tag找一下这个Fragment存不存在:

1
mFragmentManager.findFragmentByTag(name)

根据获取到的Fragment有两种添加方式,如果根据Tag找到了之前存在的Fragment,就attach上去:

1
mCurTransaction.attach(fragment);

如果没找到,那就把新创建的Fragment add上去:

1
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));

当不用这个Fragment的时,会调用destroyItem方法,可以看到这里是detach掉了Fragment,并没有销毁。

综上,一个页面只会创建一次,创建时根据当前的position给Fragment设置一个Tag,当不需要时只是把Fragmetn detach掉,并不会销毁,下次需要时通过Tag复用Fragment。

所以,如果你有大量的Fragment要展示,FragmentPagerAdapter会持有每一个Fragment不释放,最终走向OOM。

所以,无论使用PagerAdapter还是FragmentPagerAdapter,有多少页面,就会创建多少页面对象,页面很多的情况下,会非常占用内存,虽然这样,但是它们也有各自的应用场景,例如,App的首次安装的启动引导页面个数是固定的,而且如果比较复杂,通常做成Fragment,这时使用FragmentPagerAdapter较为合适;而如果页面中有需要自动轮播卡片的地方,则可以使用PagerAdapter实现,因为其页面一般都是View,结构简单,数量也是固定的。

那如果我需要做一个可以无限滑动的卡片,就需要用到FragmentStatePagerAdapter了,我们看一下FragmentStatePagerAdapter的instantiateItemdestroyItem方法:

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

/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);

@Override
public Object instantiateItem(ViewGroup container, int position) {
...

if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}

Fragment fragment = getItem(position);
while (mFragments.size() <= position) {
mFragments.add(null);
}

...
mCurTransaction.add(container.getId(), fragment);

return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
...
mCurTransaction.remove(fragment);
}

可以看到,FragmentStatePagerAdapter在初始化页面时,通过getItem这个抽象方法来获得Fragment,并将其add到容器里,而不用的时候直接remove掉,这样就不会一直占用Fragment,可以实现无限滑动,唯一关心的就是在getItem中创建一个Fragment就可以了。

以上就是我对三种PagerAdapter的简单理解。在使用ViewPager的过程中,除了adapter的选择,还需要到了下面的问题。

2.常见的两个问题小析

1.notifyDataSetChanged()后页面没有刷新的问题

这个问题主要是由于当数据更新时,ViewPager会调用dataSetChanged方法:

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

void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null.

...
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
final int newPos = mAdapter.getItemPosition(ii.object);

if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}

if (newPos == PagerAdapter.POSITION_NONE) {
mItems.remove(i);
i--;

if (!isUpdating) {
mAdapter.startUpdate(this);
isUpdating = true;
}

mAdapter.destroyItem(this, ii.position, ii.object);
needPopulate = true;

if (mCurItem == ii.position) {
// Keep the current item in the valid range
newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
needPopulate = true;
}
continue;
}

...
}

...
}

这个方法会调用adapter的getItemPosition()来获得object的位置情况是否发生变化,如果不发生变化就不更新了,如果而这个方法默认是返回的就是POSITION_UNCHANGED:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 /**
* Called when the host view is attempting to determine if an item's position
* has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
* item has not changed or {@link #POSITION_NONE} if the item is no longer present
* in the adapter.
*
* <p>The default implementation assumes that items will never
* change position and always returns {@link #POSITION_UNCHANGED}.
*
* @param object Object representing an item, previously returned by a call to
* {@link #instantiateItem(View, int)}.
* @return object's new position index from [0, {@link #getCount()}),
* {@link #POSITION_UNCHANGED} if the object's position has not changed,
* or {@link #POSITION_NONE} if the item is no longer present.
*/
public int getItemPosition(Object object) {
return POSITION_UNCHANGED;
}

所以要想每次刷新都让页面更新,需要在adapter中重写getItemPosition方法,并返回POSITION_NONE就可以了:

1
2
3
4
5

@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
2.初始化数据时不会调用OnPageChangeListener

相信大家都遇到过这种情况,就是ViewPager初始化时并不会调用OnPageChangeListener,所以很多时候我们都是手动调用第一页的onPageSelected方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

}

@Override
public void onPageSelected(int position) {

}

@Override
public void onPageScrollStateChanged(int state) {

}
};

public void OnCreate(){
...
mViewPager.setOnPageChangeListener(onPageChangeListener);
onPageChangeListener.onPageSelected(0); // 手动调用第一页
...
}

但是如果如果你使用的是FragmentPagerAdapter时,会发现在onPageSelected(0)时,会发现Fragment还没有创建成功,这时候会出现NPE(NullPointerException)。所以这时候可以这样写:

1
2
3
4
5
6
mViewPager.post(new Runnable(){
@Override
public void run() {
onPageChangeListener.onPageSelected(0); // 手动调用第一页
}
});

这样就会在viewpager的UI事件队列完成后处理onPageSelected方法的内容,这个时候Fragment已经好了。

但是,虽然这样,讲要进行的工作交给post会打乱同步时序,让要做的事充满了不确定性,我不知道它什么时候能调用onPageSelected方法。而且这样做也存在另外一个问题,当使用notifyDataSetChanged()刷新数据时,还是不会调用当前页的onPageSelected来更新当前页面,所以我更倾向于下面这种方法:

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
public class MyPagerAdapter extends FragmentStatePagerAdapter {
...

private Object lastPrimaryItem;

private OnPageSwitchListener onPageSwitchListener;
public MyPagerAdapter(FragmentManager fm, OnPageSwitchListener onPageSwitchListener) {
super(fm);
this.onPageSwitchListener = onPageSwitchListener;
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.finishUpdate(container);
super.setPrimaryItem(container, position, object);
if (object == lastPrimaryItem) {
return; // setPrimaryItem会多次调用,过滤掉多余的调用
}
lastPrimaryItem = object;
MyFragment fragment = (MyFragment)object;
if (onPageSwitchListener != null && fragment != null) {
onPageSwitchListener.onPageSwitch(position, fragment);
}
}

@Override
public void finishUpdate(ViewGroup container) {
// 这里将finishUpdate放到setPrimaryItem之前执行,见上面的方法👆
// 原因是finishUpdate后的Fragment才算正式可以用了
}

public interface OnPageSwitchListener{
void onPageSwitch(int position, MyFragment fragment);
}
}

class MyActivity extends Activity implements MyPagerAdapter.OnPageSwitchListener{

@Override
public void onPageSwitch(int position, MyFragment fragment) {
// Do whatever you like here. :)
}
}

简单说就是,PagerAdapter中有一个重要的方法叫setPrimaryItem,当一个页面显示的时候都会调用这个方法(多次),通过一些简单的处理,可以实现与mViewPager.setOnPageChangeListener()类似的功能,这样就不用每次调用onPageChangeListener.onPageSelected(0)或者使用Post操作了。

以上分析较为肤浅,有问题欢迎指正、补充,谢谢🙏

参考

[1] https://stackoverflow.com/questions/16074058/onpageselected-doesnt-work-for-first-page

[2] https://stackoverflow.com/questions/11794269/onpageselected-isnt-triggered-when-calling-setcurrentitem0

坑--TextView同时使用maxLines=1和ellipsize时偶发崩溃

今天发现项目的报错记录中排名第一的是ArrayIndexOutOfBoundsException错误,影响用户84,发生次数129。

报错堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1 android.text.StaticLayout.getLineTop(StaticLayout.java:878)
2 android.widget.TextView.getExtendedPaddingTop(TextView.java:1966)
3 android.widget.TextView.bringTextIntoView(TextView.java:7291)
4 android.widget.TextView.onPreDraw(TextView.java:5107)
5 android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:944)
6 android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2348)
7 android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1292)
8 android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6598)
9 android.view.Choreographer$CallbackRecord.run(Choreographer.java:800)
10 android.view.Choreographer.doCallbacks(Choreographer.java:603)
11 android.view.Choreographer.doFrame(Choreographer.java:572)
12 android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:786)
13 android.os.Handler.handleCallback(Handler.java:815)
14 android.os.Handler.dispatchMessage(Handler.java:104)
15 android.os.Looper.loop(Looper.java:194)
16 android.app.ActivityThread.main(ActivityThread.java:5682)
17 java.lang.reflect.Method.invoke(Native Method)
18 java.lang.reflect.Method.invoke(Method.java:372)

从错误堆栈里并没有找到特别有用的信息。

于是去看了下报错的设备信息:

| 机型设备 Top 5 | 系统版本 Top 5 | | ——————— | ——————————- | | 华为 TIT-TL00 11.26% | Android 5.1,level 22 31.40% | | 三星 GT-I908 26.48% | Android 4.1.2,level 16 20.48% | | 荣耀 CUN AL00 6.14% | Android 4.0.4,level 15 10.92% | | 华为 TAG-TL00 5.46% | Android 7.0 8.19% | | HTC T327T 4.44% | Android 4.0.3,level 15 7.85% |

可谓是五花八门,看来也不是特定机型或者特定系统的问题。

从日志和设备信息都分析不出问题,只能靠google了,通过不断的搜索,渐渐发现类似的崩溃跟TextView的两个属性有关系,分别是:

  • Android:maxLines
  • Android:ellipsize

根据查询的信息,得出了下面的结论:

如果同时设置了ellipsize和maxLine=1,就会在某些机型上产生崩溃(不是必先),报ArrayIndexOutOfBoundsException的异常。解决办法就是将maxLine=1换成已经废弃的singleLine=true。这是Android系统的一个bug,目前还没有解决。

下面查询到的相关记录:

If you try to use maxLines=1 with ellipsize, you shall get the following lint error (refer to this discussion)

Combining ellipsize and maxLines=1 can lead to crashes on some devices. Earlier versions of lint recommended replacing singleLine=true with maxLines=1 but that should not be done when using ellipsize

来源:https://code.luasoftware.com/tutorials/android/android-textview-singleline-with-ellipsis/


问题 Crash when using ellipsize=”start” (Jelly Bean) 的回复

ri…@gmail.com ri...@gmail.com #3Jul 12, 2012 08:17AM

I do notice that changing android:lines=”1” to android:singleLine=”true” causes the crash not to happen.

来源:https://issuetracker.google.com/issues/36950033


I faced what I suspect is the same problem in my own app. For me, it was happening because I was using android:ellipsize="start" without also using android:singleLine="true".

We had switched all of our android:singleLine="true" attributes to the recommended android:maxLines="1", but it turns out that there’s a bug in how the system calculates the ellipsis that is triggered if singleLine isn’t present.

So I believe you can solve this issue by simply adding android:singleLine="true" to your TextViews that are using ellipsize attrs.

来源:https://stackoverflow.com/questions/45487427/crashlytics-reporting-multiples-issues-for-textview-makesinglelayout

原来我根本不会用Android Studio

背景

最近在工作中解锁了一些使用Android Studio的技巧,在这里记录一下。

1. 为什么明明我的电脑剩余那么多内存,用Android Studio打开多个工程后就开始卡顿了呢?

有一次在使用Android stuido的时候发现会卡顿无比,滚动鼠标都卡,但我的机器明明配置很高,系统剩余的内存也很多啊。

于是,打开内存指示器,查看Android Studio的内存使用情况:

打开设置 -> Appearance -> Window Options -> Show memory indicator

勾选了Show memory indicator 之后,就可以在Android Studio的右下角看到内存指示器了:

双击指示器可以手动进行GC操作。这里显示的就是Android Studio占用的内存大小(左)和分配给Android Studio使用的最大内存(右)。如果发现占用内存基本上快要等于分配的最大内存的时候,说明需要分配更多的内存给Android Studio了。当时我的机器显示的是1020/1024M,这应该就是其默认分配的内存大小,基本上分配的内存已经快要用完了,这也就是为啥明明电脑内存剩余很多,但是Android Studio却很卡顿的原因。

于是,通过下面步骤来提高分配给Android的内存:

打开菜单Help->Edit Custom VM Options,这时,会在新窗口打开一个studo.vmoptions文件(如果之前没有,会新创建一个),在打开的文件中输入下面代码配置虚拟机堆栈的最大分配内存:

1
-Xmx4096m

这里可以根据自己机器的内存大小,对Android Studio进行配置,配置完成后,重启Android Studio,看到右下角的内存指示器显示的最大分配内存已经发生了变化,就说明生效了。

Read more
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×