Google Play 控制台报告应用程序崩溃,我无法重现

Posted

技术标签:

【中文标题】Google Play 控制台报告应用程序崩溃,我无法重现【英文标题】:Google Play console reports app crash, I can not reproduce 【发布时间】:2019-04-01 08:23:03 【问题描述】:

android Vitals 报告中的 Google Play 控制台为我提供了此信息

java.lang.RuntimeException: 
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2955)
  at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3030)
  at android.app.ActivityThread.-wrap11 (Unknown Source)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1696)
  at android.os.Handler.dispatchMessage (Handler.java:105)
  at android.os.Looper.loop (Looper.java:164)
  at android.app.ActivityThread.main (ActivityThread.java:6938)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:327)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1374)
Caused by: java.lang.NullPointerException: 
  at com.golendukhin.whitenoise.GridAdapter.getCount (GridAdapter.java:52)
  at android.widget.GridView.setAdapter (GridView.java:243)
  at com.golendukhin.whitenoise.FoldersGridActivity.onCreate (FoldersGridActivity.java:60)
  at android.app.Activity.performCreate (Activity.java:7183)
  at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1220)
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2908)

据我了解,这是因为网格适配器的 getCount 方法返回空值。 Hense,网格适配器不起作用。因此,没有创造出全部的热情。只有三星设备存在这样的问题。

首先,我尝试创建具有相同 Android 版本、RAM 和 ARM 的相同模拟设备。模拟器工作得非常好。没有错误。

然后我添加了一些测试以确保 getCount 工作并创建活动。

@RunWith(AndroidJUnit4.class)
public class GridViewTest 

    @Rule
    public ActivityTestRule<FoldersGridActivity> foldersGridActivityTestRule  = new  ActivityTestRule<>(FoldersGridActivity.class);

    @Test
    public void ensureGridAdapterIsNotNull() 
        FoldersGridActivity foldersGridActivity = foldersGridActivityTestRule.getActivity();
        View view = foldersGridActivity.findViewById(R.id.grid_view);
        GridView gridView = (GridView) view;
        GridAdapter gridAdapter = (GridAdapter) gridView.getAdapter();
        assertThat(gridAdapter, notNullValue());
    

    @Test
    public void ensureArrayAdapterInGridActivityIsNotEmpty() 
        FoldersGridActivity foldersGridActivity = foldersGridActivityTestRule.getActivity();
        View view = foldersGridActivity.findViewById(R.id.grid_view);
        GridView gridView = (GridView) view;
        GridAdapter gridAdapter = (GridAdapter) gridView.getAdapter();
        assertTrue(gridAdapter.getCount() > 0 );
    

    @Test
    public void gridViewIsVisibleAndClickable() 
        onData(anything()).inAdapterView(withId(R.id.grid_view)).atPosition(0).perform(click());
        onView(withId(R.id.toolbar_title_text_view)).check(matches(withText("rain")));
    

所有模拟设备再次成功通过测试。 然后我添加了 FireBase TestLab 并在装有 Android 8 的 Galaxy S9 真机上启动了我的测试。所有测试都通过了。

适配器代码。是的,我已经覆盖了 getCount() 方法,即使它没有意义。

public class GridAdapter extends ArrayAdapter<TracksFolder> 
    @BindView(R.id.folder_text_view) TextView textView;
    @BindView(R.id.grid_item_image_view) ImageView imageView;

    private ArrayList<TracksFolder> tracksFolders;
    private Context context;

    GridAdapter(Context context, int resource, ArrayList<TracksFolder> tracksFolders) 
        super(context, resource, tracksFolders);
        this.tracksFolders = tracksFolders;
        this.context = context;
    

    /**
     * @return grid item, customized via TracksFolder class instance
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) 
        if (convertView == null) 
            convertView = LayoutInflater.from(context).inflate(R.layout.grid_item, parent, false);
        
        ButterKnife.bind(this, convertView);

        String folderName = tracksFolders.get(position).getFolderName();
        int imageResource = tracksFolders.get(position).getImageResource();
        textView.setText(folderName);
        imageView.setImageResource(imageResource);

        return convertView;
    

    /**
     * Added to possibly avoid NPE on multiple devices
     * Could not reproduce this error
     * @return size of tracksFolders
     */
    @Override
    public int getCount() 
        return tracksFolders.size();
    

所以,我有几个问题: 首先,这个崩溃报告是否意味着每次 Sumsung 所有者启动应用程序时都会发生这种崩溃?由于一些三星设备坚持统计,我认为不会。 其次,我应该怎么做才能以某种方式重现这个错误并最终修复这个错误。

我知道问题太宽泛了,但我非常感谢任何帮助,因为我完全不知道下一步该做什么。

入门级。使用适配器

import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.GridView;

import com.google.firebase.crash.FirebaseCrash;

import java.util.ArrayList;
import butterknife.BindArray;
import butterknife.BindView;
import butterknife.ButterKnife;

import static com.golendukhin.whitenoise.SharedPreferencesUtils.writeToSharedPreferences;

public class FoldersGridActivity extends AppCompatActivity 
    @BindView(R.id.grid_view) GridView gridView;
    @BindArray(R.array.labels) String[] foldersName;
    @BindView(R.id.toolbar_like_button) Button toolbarLikeButton;
    @BindView(R.id.toolbar_back_button) Button toolbarBackButton;

    private ArrayList<TracksFolder> tracksFolders;

    /**
     * Binds all UI views
     * Defines variables to feed adapter and listeners
     * Initializes grid adapter, sets number of columns via phone in portrait or landscape orientation
     * Sets onclick listener for every grid item
     * Sets onclick for toolbar "like" button
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.folders_grid_view);
        ButterKnife.bind(this);

        if (getIntent().getExtras() != null)  //activity is triggered via on back pressed
            Bundle bundle = getIntent().getExtras();
            tracksFolders = (ArrayList<TracksFolder>) bundle.getSerializable("tracksFolders");
         else if (savedInstanceState != null)  //this happens after screen is rotated
            tracksFolders = (ArrayList<TracksFolder>) savedInstanceState.getSerializable("tracksFolders");
         else  //this happens if app is launched by user
            tracksFolders = getTracksFolders();
            initService();
        

        toolbarBackButton.setVisibility(View.GONE);

        GridAdapter gridAdapter;
        gridAdapter = new GridAdapter(this, R.id.grid_view, tracksFolders);
        gridView.setNumColumns(getResources().getInteger(R.integer.columns_number));
        gridView.setAdapter(gridAdapter);
        FirebaseCrash.log("Activity created");

        gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() 
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) 
                Bundle bundle = new Bundle();
                bundle.putSerializable("tracksFolders", tracksFolders);
                bundle.putInt("folderPosition", position);
                Intent intent = new Intent(FoldersGridActivity.this, TracksListActivity.class);
                intent.putExtras(bundle);
                startActivity(intent);
            
        );

        toolbarLikeButton.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                Bundle bundle = new Bundle();
                bundle.putSerializable("tracksFolders", tracksFolders);
                bundle.putInt("activityFrom", Constants.FROM_FOLDERS_GRID_ACTIVITY);
                Intent intent = new Intent(FoldersGridActivity.this, LikedTracksListActivity.class);
                intent.putExtras(bundle);
                startActivity(intent);
            
        );
    

    /**
     * Saves tracksFolders array list to rebuild activity after rotation
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) 
        outState.putSerializable("tracksFolders", tracksFolders);
        writeToSharedPreferences(this, tracksFolders);
        super.onSaveInstanceState(outState);
    

    @Override
    public void onBackPressed() 
        super.onBackPressed();
        moveTaskToBack(true);
    

    /**
     * After app is launched need to initialize audio player service
     */
    private void initService() 
        Intent intent = new Intent(FoldersGridActivity.this, AudioPlayerService.class);
        startService(intent);
    

    /**
     * If tracksFolders is not stored in shared preferences, it is initialized from resources.
     * @return arrayList of folders with arrayList of tracks belonging to every folder
     */
    private ArrayList<TracksFolder> getTracksFolders() 
        Resources resources = getResources();
        TypedArray pictures = resources.obtainTypedArray(R.array.pictures);

        ArrayList<TracksFolder> tracksFolders = new ArrayList<>();
        for (int i = 0; i < foldersName.length; i++) 
            String folderName = foldersName[i];
            int imageResource = pictures.getResourceId(i, -1);
            ArrayList<Track> tracks = getArrayListOfTracks(resources, i, imageResource);
            tracksFolders.add(new TracksFolder(folderName, imageResource, tracks));
        
        pictures.recycle();
        return tracksFolders;
    

    /**
     * @param resources      need to get typedArray to get array of audio files names
     * @param folderPosition passed to Track constructor
     * @param imageResource  passed to Track constructor
     *                       Liked tracks are obtained from shared preferences
     * @return array list of tracks for every folder
     */
    private ArrayList<Track> getArrayListOfTracks(Resources resources, int folderPosition, int imageResource) 
        TypedArray audioTracksTypedArray = resources.obtainTypedArray(R.array.audio_tracks);
        int audioTracksId = audioTracksTypedArray.getResourceId(folderPosition, 0);
        String[] audio = resources.getStringArray(audioTracksId);
        audioTracksTypedArray.recycle();

        TypedArray trackNamesTypedArray = resources.obtainTypedArray(R.array.track_names);
        int trackNamesId = trackNamesTypedArray.getResourceId(folderPosition, 0);
        String[] trackNames = resources.getStringArray(trackNamesId);
        trackNamesTypedArray.recycle();

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

        ArrayList<Track> tracksInFolder = new ArrayList<>();
         for (int i = 0; i < trackNames.length; i++) 
            int audioResource = resources.getIdentifier(audio[i], "raw", this.getPackageName());
            String trackName = trackNames[i].replace("_", " ");
            boolean isLiked = sharedPreferences.getBoolean(String.valueOf(audioResource), false);
            tracksInFolder.add(new Track(audioResource, trackName, imageResource, isLiked));
        
        return tracksInFolder;
    
    //todo убрать полосы в списках треков

更新类:

@NonNull
    private  ArrayList<TracksFolder> getTracksFoldersAndInitService(Bundle savedInstanceState) 
        if (getIntent().getExtras() != null)  //activity is triggered via on back pressed
            Bundle bundle = getIntent().getExtras();
            tracksFolders = (ArrayList<TracksFolder>) bundle.getSerializable("tracksFolders");
         else if (savedInstanceState != null)  //this happens after screen is rotated
            tracksFolders = (ArrayList<TracksFolder>) savedInstanceState.getSerializable("tracksFolders");
         else  //this happens if app is launched by user
            tracksFolders = getTracksFoldersFromResources();
            initService();
        
        return tracksFolders;
    

@Nick Fortescue、@Ranjan,感谢你们的建议,但 trackFolders 永远不会为 NULL。我已经编辑了类(您可以在“更新的类:”下面看到它)并将tracksFolders实例化到另一个带有@NonNull注释的方法中。我已经使用此更改更新了应用程序,后来获得了具有相同日志的相同 NPE。我知道这是在用户身上测试应用程序的最糟糕的方式,但我别无选择。如果你是对的,日志会说在 getTracksFoldersAndInitService 方法中抛出了异常。但它没有。因此,问题出在适配器上。如果我错了,请纠正我。

【问题讨论】:

很可能您的 GridAdapter 正在使用 trackFolders 的空数组列表进行实例化。您可以在实例化 GridAdapter 的位置发布您的 Activity 代码吗? @Ranjan,是的,这听起来像是可能的原因。我添加了使用适配器的 FoldersGridActivity 您的第三个条件实例化了数组列表。这意味着如果前两个中的任何一个为真,则 arraylist 来自其他地方,其中一个将其设置为 null。您应该尝试调试前两个条件。这也可能是制造商特定的问题。您也可以在定义时实例化列表,这样它至少不会崩溃。 private ArrayList&lt;TracksFolder&gt; tracksFolders = new ArrayList&lt;&gt;(); 实际上在定义时实例化并不重要,因为这两个请求中的任何一个都会将其设置回 null。 【参考方案1】:

GridAdapter.getCount() 中发生的 NullPointerException 并不意味着 getCount() 返回 null。这意味着它正在访问一个空值。由于您上传了代码(太棒了!),我们可以看到只有两个可能的问题:

trackFoldersnull,所以当你访问它时,你会得到一个 NPE trackFolders 返回一个 null 整数,因此当它转换为 int 时,您会得到一个 NPE

第二个选项不可能,因为ArrayList 上的.size() 也返回一个int,所以它必须是第一个选项。

trackFolders 是在构造函数中设置的,它是在你的 onCreate() 方法中从包、保存状态或 getTracksFolders() 中设置的。看起来getTracksFolders() 不能返回null,所以我猜你的活动是从这个设置为null的意图开始的。我会在您的 onCreate() 方法中仔细检查这一点,如果发生,请采取适当的措施,也许将其替换为 new ArrayList()?

【讨论】:

以上是关于Google Play 控制台报告应用程序崩溃,我无法重现的主要内容,如果未能解决你的问题,请参考以下文章

Google Play 控制台报告未发布的应用程序崩溃

在 Google Play 管理中心找不到崩溃报告

Google Play 控制台崩溃报告与我的应用无关

让Android Studio调试版崩溃的Google Play Developer Console?

Google Play 控制台开发者监控错误

Android:如何在Google Play开发者控制台中获取有关已捕获例外的信息?