95535


開発中のアプリで RecyclerView のリストから、ある Activity に遷移し再び RecyclerView に戻ると、常に
LeakCanary で遷移先 Activity のメモリリークが検出されます。

手元に端末が3種類あるのですが、 リークが起こるのは  
AQUOS sense6s (Android13)  
のみで、  
AQUOS sense3 (Android11) と LG AndroidOne X5 (Android11)  
では発生しないようです。

また、試しに android.intent.category.LAUNCHER 指定してアプリ起動後に直接 Activity
を開いた場合は発生しないようです。

そもそもリークの発生源である com.qualcomm.qti.Performance クラスとは何なのでしょうか？  
検索しても情報がほとんどなく困っています。

もしわかる方いらっしゃいましたらよろしくお願いいたします。

以下リークする Activity です。

[code]

    public class TestActivity extends AppCompatActivity {
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
    
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_test); // 空のレイアウト
        }
    }
    
[/code]

RecyclerView のアダプタクラス

[code]

    public class LeadSheetListAdapter extends RecyclerView.Adapter<LeadSheetListViewHolder> {
    
        private List<LeadSheet> list;
    
        /*public ProgressionListAdapter(List<ProgressionRowData> list) {
            this.list = list;
        }*/
    
        public LeadSheetListAdapter() {
            list = new ArrayList<>();
        }
    
        public void setList(List<LeadSheet> list) {
            this.list = list;
            Log.d(this.getClass().getSimpleName(), "setList() list.size:" + list.size());
            notifyDataSetChanged();
        }
    
        @Override
        public LeadSheetListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View inflate = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_leadsheet_list, parent,false);
            LeadSheetListViewHolder vh = new LeadSheetListViewHolder(inflate);
            return vh;
        }
    
        @RequiresApi(api = Build.VERSION_CODES.O)
        @Override
        public void onBindViewHolder(LeadSheetListViewHolder holder, int position) {
    
            LeadSheet sheet = list.get(position);
            holder.titleView.setText(sheet.getTitle());
            String description = "";
            long id = sheet.getLeadSheetId();
            int numOfMeasures = sheet.getMeasures().size();
            description += numOfMeasures + "小節" + " id:" + id;
            holder.summaryView.setText(description);
    
            if (sheet.getTags().size() > 0) {
                for (String tag : sheet.getTags()) {
                    TextView tagView = new TextView(holder.tagsContainer.getContext());
                    tagView.setText("#" + tag);
                    holder.tagsContainer.addView(tagView);
                    LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) tagView.getLayoutParams();
                    layoutParams.rightMargin = 20;
                }
                holder.tagsContainer.setVisibility(View.VISIBLE);
            } else {
                holder.tagsContainer.setVisibility(View.GONE);
            }
    
            holder.rowBody.setId(holder.getAbsoluteAdapterPosition());
            holder.rowBody.setOnClickListener(v -> {
                Context context = v.getContext();
                v.setBackgroundColor(v.getResources().getColor(R.color.theme1_selection_bg));
    
                // TestActivity に遷移
                Intent intent = new Intent(context, TestActivity.class);
                intent.putExtra("leadsheetId", sheet.leadSheetId);
                context.startActivity(intent);
            });
    
            holder.rowBody.setOnLongClickListener(v -> {
                Context vContext = v.getContext();
                v.setBackgroundColor(v.getResources().getColor(R.color.theme1_selection_bg));
    
                String[] choices = {"削除", "タグを編集"};
    
                AlertDialog.Builder builder = new AlertDialog.Builder(vContext);
                builder.setTitle("操作を選択")
                        .setNegativeButton("CANCEL", (dialog, i) -> dialog.dismiss())
                        .setItems(choices, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                if (which == 0) {
                                    // リードシートを削除
                                    showDeleteConfirmationDialog(vContext, v, sheet);
                                } else if (which == 1) {
                                    // タグを追加
                                    showEditTagsDialog(vContext, v, sheet);
                                }
                            }
    
                        });
                builder.create().show();
                return true;
            });
        }
    
        private void showDeleteConfirmationDialog(Context context, View v, LeadSheet sheet) {
            new AlertDialog.Builder(context)
                    .setTitle("Delete This Item?")
                    .setPositiveButton("OK", (dialogInterface, i) -> {
                        String name = sheet.getTitle();
                        BaseApplication.getRepository().deleteLeadSheet(sheet);
                        notifyItemRemoved(v.getId());
                        Snackbar.make(v, name + "を削除しました", Snackbar.LENGTH_LONG)
                                .setAction("復元不可", null).show();
                            }
                    )
                    .setNegativeButton("CANCEL", (dialog, i) -> dialog.dismiss())
                    .show();
        }
    
        private void showEditTagsDialog(Context context, View v, LeadSheet leadSheet) {
            EditText editText = new EditText(context);
            String text = "";
            for (String tag : leadSheet.getTags()) {
                text += tag + " ";
            }
            text = text.trim();
            text = text.replaceAll("\\s+", ", ");
            editText.setText(text);
    
            new AlertDialog.Builder(context)
                    .setTitle("タグを追加")
                    .setMessage("タグを入力してください (コンマ区切りで複数入力可)")
                    .setView(editText)
                    .setPositiveButton("OK", (dialogInterface, i) -> {
                                String string = editText.getText().toString();
                                String[] tags = string.split(",\\s*");
                                leadSheet.tags = Arrays.asList(tags);
                                BaseApplication.getRepository().saveLeadSheet(leadSheet);
                                notifyItemChanged(v.getId());
                            }
                    )
                    .setNegativeButton("CANCEL", (dialog, i) -> dialog.dismiss())
                    .show();
        }
    
        // DEBUG
        private void test(Context context, LeadSheet sheet, int viewId) {
            Log.d(context.getClass().getSimpleName(), "run() leadSheet");
            for (Measure measure : sheet.getMeasures()) {
                for (ChordNotation notation : measure.getNotationsByBeat()) {
                    Log.d(context.getClass().getSimpleName(), "run() notation.getClass:" + notation.getClass().getSimpleName());
                }
            }
            Toast.makeText(context, String.valueOf(sheet.getTitle()) + " listId:" + viewId, Toast.LENGTH_LONG).show();
        }
    
        @Override
        public int getItemCount() {
            if (list == null) {
                Log.d(this.getClass().getSimpleName(), "getItemCount() list = null");
                return 0;
            } else {
                Log.d(this.getClass().getSimpleName(), "getItemCount() list.size:" + list.size());
                return list.size();
            }
        }
    
    }
    
    
[/code]

LeakCanary のログ

[code]

    ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    104704 bytes retained by leaking objects
    Signature: 1f48c2e3c9d42514de20b1df25c05a32d67be916
    ┬───
    │ GC Root: System class
    │
    ├─ com.qualcomm.qti.Performance class
    │    Leaking: NO (a class is never leaking)
    │    ↓ static Performance.mContext
    │                         ~~~~~~~~
    ├─ androidx.appcompat.view.ContextThemeWrapper instance
    │    Leaking: YES (ContextThemeWrapper wraps an Activity with Activity.mDestroyed true)
    │    Retaining 104.7 kB in 1347 objects
    │    mBase instance of com.example.XXXXXXXX.ui.leadsheetedit.view.TestActivity with mDestroyed
    │    = true
    │    ↓ ContextWrapper.mBase
    ╰→ com.example.XXXXXXXX.ui.leadsheetedit.view.TestActivity instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.example.XXXXXXXX.ui.
    ​     leadsheetedit.view.TestActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
    ​     Retaining 6.5 kB in 234 objects
    ​     key = ed3b1035-b5f0-4182-bf15-8adf3cc3ed18
    ​     watchDurationMillis = 11978
    ​     retainedDurationMillis = 6977
    ​     mApplication instance of com.example.XXXXXXXX.BaseApplication
    ​     mBase instance of androidx.appcompat.view.ContextThemeWrapper
    ====================================
    0 LIBRARY LEAKS
    A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
    See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
    ====================================
    0 UNREACHABLE OBJECTS
    An unreachable object is still in memory but LeakCanary could not find a strong reference path
    from GC roots.
    ====================================
    METADATA
    Please include this in bug reports and Stack Overflow questions.
    Build.VERSION.SDK_INT: 33
    Build.MANUFACTURER: SHARP
    LeakCanary version: 2.9.1
    App process name: com.example.XXXXXXXX
    Class count: 25155
    Instance count: 220093
    Primitive array count: 137883
    Object array count: 32014
    Thread count: 28
    Heap total bytes: 27453955
    Bitmap count: 3
    Bitmap total bytes: 77115
    Large bitmap count: 0
    Large bitmap total bytes: 0
    Db 1: open /data/user/0/com.example.XXXXXXXX/databases/chordist.db
    Db 2: open /data/user/0/com.example.XXXXXXXX/databases/google_app_measurement_local.db
    Db 3: open /data/user/0/com.example.XXXXXXXX/no_backup/androidx.work.workdb
    Stats: LruCache[maxSize=3000,hits=118135,misses=221893,hitRate=34%]
    RandomAccess[bytes=11122379,reads=221893,travel=66942137287,range=33184478,size=41032206]
    Heap dump reason: 4 retained objects, app is not visible
    Analysis duration: 60178 ms
    Heap dump file path: /storage/emulated/0/Download/leakcanary-com.example.
    chordsoundcreationandplayingtest/2023-07-08_05-59-02_824.hprof
    Heap dump timestamp: 1688763616723
    Heap dump duration: 6054 ms
    ====================================
    
    
[/code]

こちらは遷移先 Activity から戻るボタンで RecyclerView に戻った時に出力されるログです

[code]

    2023-07-08 14:08:49.959 22593-22593 LeakCanary              com...XXXXXXXXD  Watching instance of androidx.fragment.app.FragmentManagerViewModel (androidx.fragment.app.FragmentManagerViewModel received ViewModel#onCleared() callback) with key 3c19f428-c811-436e-9d2b-b9e4e0ed2f85
    2023-07-08 14:08:49.959 22593-22593 LeakCanary              com...XXXXXXXXD  Watching instance of androidx.lifecycle.SavedStateHandlesVM (androidx.lifecycle.SavedStateHandlesVM received ViewModel#onCleared() callback) with key 521a4f1b-8e7e-44a4-b15b-6d4b730ef911
    2023-07-08 14:08:49.960 22593-22593 LeakCanary              com...XXXXXXXXD  Watching instance of leakcanary.internal.ViewModelClearedWatcher (leakcanary.internal.ViewModelClearedWatcher received ViewModel#onCleared() callback) with key e8357ad8-3f8e-4077-9970-7dec19b9c8ea
    2023-07-08 14:08:49.960 22593-22593 LeakCanary              com...XXXXXXXXD  Watching instance of androidx.lifecycle.ReportFragment (androidx.lifecycle.ReportFragment received Fragment#onDestroy() callback) with key 7db6d49a-969b-423e-9e91-ad2878d30d09
    2023-07-08 14:08:49.960 22593-22593 LeakCanary              com...XXXXXXXXD  Watching instance of com.example.XXXXXXXX.ui.leadsheetedit.view.TestActivity (com.example.XXXXXXXX.ui.leadsheetedit.view.TestActivity received Activity#onDestroy() callback) with key 5116d3bc-5f30-4ee3-b428-33e54626ae7c
    
[/code]

build.gradle(:app)

[code]

    android {
        compileSdkVersion 33
    
        defaultConfig {
            applicationId "com.example.XXXXXXXX"
            minSdkVersion 24
            targetSdkVersion 32
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    
        }
    
        buildTypes {
            release {
                minifyEnabled true
                shrinkResources true
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    
        testOptions {
            unitTests {
                unitTests.returnDefaultValues = true
            }
        }
        buildFeatures {
            viewBinding true
        }
    
        /* Using native library */
        externalNativeBuild {
            cmake {
                path "src/main/cpp/CMakeLists.txt"
            }
        }
    
    }
    
    dependencies {
        implementation fileTree(dir: "libs", include: ["*.jar"])
        implementation 'androidx.appcompat:appcompat:1.6.1'
        implementation 'androidx.legacy:legacy-support-v4:1.0.0'
        implementation 'androidx.window:window:1.0.0-alpha05'
    
        /* MidiDriver */
        implementation project(path: ':MidiDriver-1.1')
        /* Gson */
        implementation 'com.google.code.gson:gson:2.8.6'
        /* UI */
        implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
        implementation 'androidx.gridlayout:gridlayout:1.0.0'
        implementation 'androidx.recyclerview:recyclerview:1.2.1'
        implementation 'com.google.android.material:material:1.7.0'
        implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
        implementation 'androidx.navigation:navigation-fragment:2.5.3'
        implementation 'androidx.navigation:navigation-ui:2.5.3'
        implementation 'com.google.android:flexbox:1.1.0'
        /* ViewModel */
        implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
        implementation 'androidx.lifecycle:lifecycle-livedata:2.5.1'
        implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1"
        /* Room */
        def room_version = "2.3.0"
        implementation "androidx.room:room-runtime:$room_version"
        annotationProcessor "androidx.room:room-compiler:$room_version"
        // optional - Guava support for Room, including Optional and ListenableFuture
        implementation "androidx.room:room-guava:$room_version"
        /* DataStore */
        implementation "androidx.datastore:datastore-preferences:1.0.0"
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"
        /* ad */
        implementation 'com.google.android.gms:play-services-ads:21.3.0'
        implementation platform('com.google.firebase:firebase-bom:29.3.1')
        implementation 'com.google.firebase:firebase-analytics'
        implementation project(':nativetemplates')
        /* test */
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test.ext:junit:1.1.1'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
        fileTree(dir: 'libs', include: ['*.jar'])
        /* debug */
        debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
    }
    
[/code]


CC BY-SA 4.0
2023-07-08T05:30:58.047
95522
2023-07-09T07:48:20.510
2023-07-09T07:48:20.510
57360
57360
question
java android メモリ管理
Android アプリで、特定の端末でのみ RecyclerView から遷移した Activity がメモリリークしてしまいますが原因がわかりません


遷移先の Activity の継承を AppCompatActivity から FragmentActivity
に変更したところメモリリークが発生しなくなりました。原因は全く分かりませんが、とりあえず解決できました。

変更前  
public class TestActivity extends AppCompatActivity  
↓  
変更後  
public class TestActivity extends FragmentActivity

依存関係が原因の可能性もあるため、gradle(:app) も追記しました。  
同じ様な症状ある方はご参考にしてください。


CC BY-SA 4.0
2023-07-09T07:36:04.757
95535
2023-07-09T07:42:59.827
2023-07-09T07:42:59.827
57360
57360
95522
answer