Guide de l'ingénieur Baidu pour éviter les pièges du développement mobile - Fuites de mémoire

image

Auteur | Équipe de l'étoile du matin

Il est inévitable de rencontrer divers problèmes et fosses lors de l'écriture quotidienne de code. Ces problèmes peuvent affecter l'efficacité de notre développement et la qualité du code. Par conséquent, nous devons constamment résumer et apprendre à éviter ces problèmes. Ensuite, nous résumerons les problèmes courants du développement mobile pour améliorer la qualité du développement de chacun. Cette série d'articles se concentrera sur les fuites de mémoire, les considérations de développement du langage, etc. Dans cet article, nous présenterons les problèmes courants de fuite de mémoire dans Android/iOS.

1. Terminal Androïd

La fuite de mémoire (Memory Leak), en termes simples, est que les objets qui ne sont plus utilisés ne peuvent pas être récupérés par GC et que la mémoire occupée ne peut pas être libérée, ce qui fait que l'application occupe de plus en plus de mémoire et que le MOO se bloque en raison d'un espace mémoire insuffisant. ; de plus, comme l'espace mémoire disponible diminue, le GC est plus fréquent et il est plus facile de déclencher FULL GC, d'arrêter le travail des threads et de provoquer le blocage de l'application.

Les fuites de mémoire dans les applications Android sont un problème courant, voici quelques fuites de mémoire Android courantes :

1.1 Classe interne anonyme

La classe interne anonyme contient la référence de la classe externe et l'objet de la classe interne anonyme fuit, ce qui entraîne une fuite de mémoire de l'objet de la classe externe. Les classes internes anonymes communes Handler et Runnable contiennent la référence de l'activité externe. Si le L'activité a été détruite, mais le gestionnaire n'a pas terminé le traitement du message, ce qui entraîne des fuites de mémoire du gestionnaire, ce qui entraîne des fuites de mémoire d'activité.

Exemple 1:

public class TestActivity extends AppCompatActivity {

    private static final int FINISH_CODE = 1;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.what == FINISH_CODE) {
                TestActivity.this.finish();
            }
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handler.sendEmptyMessageDelayed(FINISH_CODE, 60000);
    }
}

Exemple 2 :

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                TestActivity.this.finish();
            }
        }, 60000);
    }
}

L'exemple 1 et l'exemple 2 chronomètrent simplement une minute pour fermer la page. Si la page a été activement fermée et détruite auparavant, il y a toujours un message en attente d'exécution dans le gestionnaire, et il y a une chaîne de référence à l'activité, ce qui provoque l'activité doit être détruite et ne peut pas être recyclée par le GC, ce qui entraîne une perte de mémoire. Fuite ; l'exemple 1 est une classe interne anonyme de Handler, contenant une référence d'activité externe : thread principal —> ThreadLocal —> Looper —> MessageQueue —> Message —> Handler —> Activity ; l'exemple 2 est une classe interne anonyme de Runnable, contenant une référence Activity externe : Message —> Runnable —> Activity.

Méthode de réparation 1 : principalement pour le gestionnaire, supprimez tous les messages du cycle de vie de l'activité.

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
    }

Méthode de réparation 2 : classe interne statique + référence faible, supprimant la relation de référence forte, peut réparer les fuites de mémoire causées par des classes internes anonymes similaires.

    static class FinishRunnable implements Runnable {

        private WeakReference<Activity> activityWeakReference;

        FinishRunnable(Activity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void run() {
            Activity activity = activityWeakReference.get();
            if (activity != null) {
                activity.finish();
            }
        }
    }
    
    new Handler().postDelayed(new FinishRunnable(TestActivity.this), 60000);

1.2 Singleton/variable statique

Une variable singleton/statique contient une référence à l'activité, et même si l'activité a été détruite, sa référence existe toujours, provoquant une fuite de mémoire.

Exemple:

    static class Singleton {

        private static Singleton instance;

        private Context context;

        private Singleton(Context context) {
            this.context = context;
        }

        public static Singleton getInstance(Context context) {
            if (instance == null) {
                instance = new Singleton(context);
            }
            return instance;
        }
    }
    
    Singleton.getInstance(TestActivity.this);
 

Appelez le singleton dans l'exemple, transmettez le paramètre Context et utilisez l'objet Activity. Même si l'Activity est détruite, elle sera toujours référencée par la variable statique Singleton, ce qui entraînera un échec du recyclage et des fuites de mémoire.

Méthode de réparation :

Singleton.getInstance(Application.this);

Essayez d'utiliser le contexte d'application en tant que paramètre singleton, à moins que certaines fonctions qui nécessitent une activité, telles que l'affichage de la boîte de dialogue, si vous devez utiliser l'activité en tant que paramètre singleton, vous pouvez vous référer à la méthode de réparation de classe interne anonyme et la publier à un moment approprié, tel que le cycle de vie onDestroy de Activity Singleton, ou utilisez une référence faible pour maintenir Activity.

1.3 Auditeurs

Exemple : l'écouteur enregistré d'EventBus n'est pas indépendant, ce qui fait que l'enregistrement auprès d'EventBus est toujours référencé et ne peut pas être recyclé.

public class TestActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EventBus.getDefault().register(this);
    }
}

Méthode de réparation : délier dans le cycle de vie du moniteur d'enregistrement correspondant, et onCreate correspond à onDestroy.

    @Override
    protected void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }

1.4 Ressources de fichier/base de données

Exemple : Lors de l'ouverture d'une base de données de fichiers ou d'un fichier, une exception se produit et il n'est pas fermé, ce qui fait que les ressources existent toujours, ce qui entraîne des fuites de mémoire.

    public static void copyStream(File inFile, File outFile) {
        try {
            FileInputStream inputStream = new FileInputStream(inFile);
            FileOutputStream outputStream = new FileOutputStream(outFile);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Correction : fermez le flux de fichiers dans le bloc de code finally pour vous assurer qu'il peut être exécuté après qu'une exception se soit produite

    public static void copyStream(File inFile, File outFile) {
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(inFile);
            outputStream = new FileOutputStream(outFile);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(inputStream);
            close(outputStream);
        }
    }

    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

1.5 animations

Exemple : l'animation Android n'a pas annulé la libération des ressources d'animation à temps, ce qui a entraîné des fuites de mémoire.

public class TestActivity extends AppCompatActivity {

    private ImageView imageView;
    private Animation animation;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        imageView = (ImageView) findViewById(R.id.image_view);
        animation = AnimationUtils.loadAnimation(this, R.anim.test_animation);
        imageView.startAnimation(animation);
    }
}

Correction : Annulez l'animation lorsque la page se ferme et se détruit, et libérez les ressources d'animation à temps.

@Override
protected void onDestroy() {
    super.onDestroy();
    if (animation != null) {
        animation.cancel();
        animation = null;
    }
}

2. Côté IOS

À l'heure actuelle, nous avons déjà ARC (comptage automatique des références) pour remplacer le MRC (comptage manuel des références), et l'objet appliqué sera libéré automatiquement lorsqu'il n'est pas fortement référencé. Cependant, dans le cas d'un codage non standard, le compteur de références ne peut pas être remis à zéro à temps, et il existe toujours un risque d'introduire des fuites de mémoire, ce qui peut entraîner des conséquences très graves. En prenant la scène de diffusion en direct comme exemple, si le ViewController du service de diffusion en direct ne peut pas être libéré, les données statistiques ponctuelles dépendant du ViewController seront anormales et l'utilisateur pourra toujours entendre le son de diffusion en direct après avoir fermé la page de diffusion en direct. Il est très important de se familiariser avec les scénarios de fuite de mémoire et de développer l'habitude d'éviter les fuites de mémoire. Voici quelques fuites de mémoire iOS courantes et des solutions.

2.1 Référence circulaire causée par le bloc

Les références circulaires introduites par les blocs sont le type de fuite de mémoire le plus courant. Un cycle de référence commun est objet->bloc->objet. À ce stade, les compteurs de références de l'objet et du bloc sont 1 et ne peuvent pas être libérés.

[self.contentView setActionBlock:^{
    [self doSomething];
}];

Dans l'exemple de code, self fait fortement référence à la variable membre contentView, contentView fait fortement référence à actionBlock et actionBlock fait fortement référence à self, ce qui introduit un problème de fuite de mémoire.

image

Supprimer une référence circulaire revient à supprimer un anneau de référence fort, et vous devez remplacer une référence forte par une référence faible. comme:

__weak typeof(self) weakSelf = self;
[self.contentView setActionBlock:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
}];

À ce stade, actionBlock se réfère faiblement à soi, la référence circulaire est rompue et peut être libérée normalement.

image

Ou utilisez l'écriture plus simple fournie par RAC :

@weakify(self);
[self setTaskActionBlock:^{
    @strongify(self);
    [self doSomething];
}];

Il convient de noter que ce n'est pas seulement self qui peut avoir une référence circulaire au bloc, mais tous les objets d'instance peuvent avoir de tels problèmes, et cela est facilement négligé pendant le processus de développement. Par exemple:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];
    @weakify(self);
    cell.clickItemBlock = ^(CellModel * _Nonnull model) {
        @strongify(self);
        [self didSelectRowMehod:model tableView:tableView];
    };
    return cell;
}

Dans cet exemple, la référence circulaire entre self et block est rompue, et self peut être libéré normalement, mais il convient de noter qu'il existe toujours une chaîne de référence circulaire : tableView référence fortement cell, cell référence fortement block et block référence fortement tableView . Cela empêchera également la tableView et la cellule d'être libérées.

image

La bonne façon de l'écrire est:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];
    @weakify(self);
    @weakify(tableView);
    cell.clickItemBlock = ^(CellModel * _Nonnull model) {
        @strongify(self);
        @strongify(tableView);
        [self didSelectRowMehod:model tableView:tableView];
    };
    return cell;
}

image

2.2 Référence circulaire causée par le délégué

@protocol TestSubClassDelegate <NSObject>

- (void)doSomething;

@end

@interface TestSubClass : NSObject

@property (nonatomic, strong) id<TestSubClassDelegate> delegate;

@end

@interface TestClass : NSObject <TestSubClassDelegate>

@property (nonatomic, strong) TestSubClass *subObj;

@end

Dans l'exemple ci-dessus, TestSubClass utilise le modificateur fort pour le délégué, ce qui fait que l'instance TestClass et l'instance TestSubClass se référencent fortement après avoir défini le proxy, ce qui donne une référence circulaire. Dans la plupart des cas, le délégué doit utiliser le modificateur faible pour éviter les références circulaires.

2.3 Référence forte NSTimer

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
[NSRunLoop.currentRunLoop addTimer:self.timer forMode:NSRunLoopCommonModes];

L'instance NSTimer fera fortement référence à la cible entrante, et il y aura des références mutuelles fortes entre self et timer. À ce stade, l'état de la minuterie doit être maintenu manuellement. Lorsque la minuterie s'arrête ou que la vue est supprimée, la minuterie est activement détruite pour briser la référence circulaire.

Solution 1 : Passez à la méthode de blocage fournie par iOS10 pour éviter une forte référence à la cible par NSTimer.

@weakify(self);
self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    @strongify(self);
    [self doSomething];
}];

Solution 2 : Utiliser NSProxy pour résoudre le problème de référence forte.

// WeakProxy
@interface TestWeakProxy : NSProxy

@property (nullable, nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target;

+ (instancetype)proxyWithTarget:(id)target;

@end

@implementation TestWeakProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[TestWeakProxy alloc] initWithTarget:target];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([self.target respondsToSelector:[invocation selector]]) {
        [invocation invokeWithTarget:self.target];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [self.target methodSignatureForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.target respondsToSelector:aSelector];
}

@end

// 调用
self.timer = [NSTimer timerWithTimeInterval:1 target:[TestWeakProxy proxyWithTarget:self] selector:@selector(doSomething) userInfo:nil repeats:YES];

2.4 Fuites de mémoire de type non référence

La libération automatique d'ARC est implémentée sur la base du comptage de références et ne conserve que les objets oc. La mémoire allouée directement par malloc en langage C n'est pas gérée par ARC et doit être libérée manuellement. Opérations courantes telles que l'utilisation de CoreFoundation, du framework CoreGraphics pour personnaliser le dessin, la lecture de fichiers, etc.

Comme générer UIImage via CVPixelBufferRef :

CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage* bufferImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef frameCGImage = [context createCGImage:bufferImage fromRect:bufferImage.extent];
UIImage *uiImage = [UIImage imageWithCGImage:frameCGImage];
CGImageRelease(frameCGImage);
CFRelease(sampleBuffer);

2.5 Problème de diffusion retardée

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self doSomething];
});

Dans l'exemple ci-dessus, la méthode doSomething est exécutée après un délai de 20 secondes à l'aide de dispatch_after. Cela ne provoque pas de fuite de mémoire de l'objet self. Mais en supposant que self est un UIViewController, même si self a été supprimé de la pile de navigation et n'a plus besoin d'être utilisé, self ne sera pas libéré tant que le bloc ne sera pas exécuté, provoquant un phénomène similaire aux fuites de mémoire dans l'entreprise.

Dans cette exécution retardée à long terme, il est préférable d'ajouter une paire affaiblir-renforcer pour éviter une forte détention.

@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    @strongify(self);
    [self doSomething];
});

---------- FIN ----------

Série de lectures recommandées [Technical Gas Station] :

Guide d'évitement des puits de développement du programmeur Baidu (langage Go)

Guide d'évitement des fosses de développement du programmeur Baidu (3)

Guide d'évitement des fosses de développement du programmeur Baidu (terminal mobile)

Guide d'évitement des fosses de développement du programmeur Baidu (frontal)

image

{{o.name}}
{{m.name}}

Je suppose que tu aimes

Origine my.oschina.net/u/4939618/blog/8797752
conseillé
Classement