35 votes

Reprendre NSUrlSession sur iOS10

La sortie d'iOS 10 est imminente, il est donc utile de tester la compatibilité des applications avec ce système. Au cours de ces tests, nous avons découvert que notre application ne pouvait pas reprendre les téléchargements en arrière-plan sur iOS10. Le code qui fonctionnait bien sur les versions précédentes ne fonctionne pas sur la nouvelle, à la fois sur un émulateur et sur un appareil.

Au lieu de réduire notre code à un cas de test fonctionnel minimal, j'ai cherché sur Internet des tutoriels sur NSUrlSession et je les ai testés. Le comportement est le même : la reprise fonctionne sur les versions précédentes d'iOS mais se brise sur la 10ème.

Étapes de la reproduction :

  1. Télécharger un formulaire de projet Tutoriel NSUrlSession https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started
  2. Lien direct : http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip
  3. Construisez-le et lancez-le sous iOS 10. Recherchez quelque chose, par exemple "swift". Commencez le téléchargement, puis appuyez sur "Pause" et "Reprise"

Résultats attendus :

Le téléchargement reprend. Vous pouvez vérifier comment cela fonctionne avec les versions antérieures à iOS10.

Résultats réels :

Télécharger les échecs. Dans la console xcode, vous pouvez voir :

2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
2016-09-02 16:11:24.913 HalfTunes[35205:2279228] Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.

Plus de scénarios :

Si vous activez le mode hors ligne pendant le téléchargement d'un fichier, vous obtenez

Url session completed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL} {
    NSLocalizedDescription = "unsupported URL";
}

lorsque le réseau est coupé et le téléchargement ne reprend jamais lorsque le réseau est rétabli. Les autres cas d'utilisation de la pause, comme le redémarrage, ne fonctionnent pas non plus.

Enquête complémentaire :

J'ai essayé de vérifier si le resumeData renvoyé est valide en utilisant le code suggéré dans le document

Comment vérifier qu'un blob NSData est valide en tant que resumeData pour une NSURLSessionDownloadTask ?

mais le fichier cible est en place. Cependant, le format de resumeData a changé et le nom du fichier est désormais stocké dans NSURLSessionResumeInfoTempFileName et vous devez y ajouter NSTemporaryDirectory().

De plus, j'ai envoyé un rapport de bug à Apple, mais ils n'ont pas encore répondu.

La question (de la vie, de l'univers et de tout) :

La reprise de NSUrlSession est-elle cassée dans toutes les autres applications ? Cela peut-il être corrigé du côté de l'application ?

40voto

Mousavian Points 1041

Ce problème est dû au fait que currentRequest et originalRequest NSKeyArchived sont encodés avec une racine inhabituelle "NSKeyedArchiveRootObjectKey" au lieu de la constante NSKeyedArchiveRootObjectKey qui est littéralement la "racine" et d'autres problèmes dans le processus d'encodage de NSURL(Mutable)Request.

J'ai détecté cela dans la beta 1 et j'ai déposé un bug (no. 27144153 au cas où vous voudriez le dupliquer). J'ai même envoyé un email à "Quinn the Eskimo" (eskimo1 at apple dot com) qui est le support de l'équipe NSURLSession, pour confirmer qu'ils l'avaient reçu et il m'a dit qu'ils l'avaient reçu et qu'ils étaient au courant du problème.

UPDATE : J'ai finalement trouvé comment résoudre ce problème. Donnez des données à la fonction correctResumeData() et elle renverra des données de CV utilisables.

MISE À JOUR 2 : Vous pouvez utiliser la fonction NSURLSession.correctedDownloadTaskWithResumeData() / URLSession.correctedDownloadTask(withResumeData :) pour obtenir une tâche dont les variables originalRequest et currentRequest sont correctes.

UPDATE 3 : Quinn dit que ce problème est résolu dans iOS 10.2, vous pouvez continuer à utiliser ce code pour être compatible avec iOS 10.0 et 10.1 et il fonctionnera avec la nouvelle version sans aucun problème.

(Pour le code Swift 3, voir ci-dessous, pour l'Objective C, voir leesstar post mais je ne l'ai pas testé)

Swift 2.3 :

func correctRequestData(data: NSData?) -> NSData? {
    guard let data = data else {
        return nil
    }
    // return the same data if it's correct
    if NSKeyedUnarchiver.unarchiveObjectWithData(data) != nil {
        return data
    }
    guard let archive = (try? NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
        return nil
    }
    // Rectify weird __nsurlrequest_proto_props objects to $number pattern
    var k = 0
    while archive["$objects"]?[1].objectForKey("$\(k)") != nil {
        k += 1
    }
    var i = 0
    while archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_prop_obj_\(i)") != nil {
        let arr = archive["$objects"] as? NSMutableArray
        if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] {
            dic.setObject(obj, forKey: "$\(i + k)")
            dic.removeObjectForKey("__nsurlrequest_proto_prop_obj_\(i)")
            arr?[1] = dic
            archive["$objects"] = arr
        }
        i += 1
    }
    if archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_props") != nil {
        let arr = archive["$objects"] as? NSMutableArray
        if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] {
            dic.setObject(obj, forKey: "$\(i + k)")
            dic.removeObjectForKey("__nsurlrequest_proto_props")
            arr?[1] = dic
            archive["$objects"] = arr
        }
    }
    // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root"
    if archive["$top"]?.objectForKey("NSKeyedArchiveRootObjectKey") != nil {
        archive["$top"]?.setObject(archive["$top"]?["NSKeyedArchiveRootObjectKey"], forKey: NSKeyedArchiveRootObjectKey)
        archive["$top"]?.removeObjectForKey("NSKeyedArchiveRootObjectKey")
    }
    // Reencode archived object
    let result = try? NSPropertyListSerialization.dataWithPropertyList(archive, format: NSPropertyListFormat.BinaryFormat_v1_0, options: NSPropertyListWriteOptions())
    return result
}

func getResumeDictionary(data: NSData) -> NSMutableDictionary? {
    var iresumeDictionary: NSMutableDictionary? = nil
    // In beta versions, resumeData is NSKeyedArchive encoded instead of plist
    if #available(iOS 10.0, OSX 10.12, *) {
        var root : AnyObject? = nil
        let keyedUnarchiver = NSKeyedUnarchiver(forReadingWithData: data)

        do {
            root = try keyedUnarchiver.decodeTopLevelObjectForKey("NSKeyedArchiveRootObjectKey") ?? nil
            if root == nil {
                root = try keyedUnarchiver.decodeTopLevelObjectForKey(NSKeyedArchiveRootObjectKey)
            }
        } catch {}
        keyedUnarchiver.finishDecoding()
        iresumeDictionary = root as? NSMutableDictionary

    }

    if iresumeDictionary == nil {
        do {
            iresumeDictionary = try NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil) as? NSMutableDictionary;
        } catch {}
    }

    return iresumeDictionary
}

func correctResumeData(data: NSData?) -> NSData? {
    let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
    let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"

    guard let data = data, let resumeDictionary = getResumeDictionary(data) else {
        return nil
    }

    resumeDictionary[kResumeCurrentRequest] = correctRequestData(resumeDictionary[kResumeCurrentRequest] as? NSData)
    resumeDictionary[kResumeOriginalRequest] = correctRequestData(resumeDictionary[kResumeOriginalRequest] as? NSData)

    let result = try? NSPropertyListSerialization.dataWithPropertyList(resumeDictionary, format: NSPropertyListFormat.XMLFormat_v1_0, options: NSPropertyListWriteOptions())
    return result
}

extension NSURLSession {
    func correctedDownloadTaskWithResumeData(resumeData: NSData) -> NSURLSessionDownloadTask {
        let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
        let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"

        let cData = correctResumeData(resumeData) ?? resumeData
        let task = self.downloadTaskWithResumeData(cData)

        // a compensation for inability to set task requests in CFNetwork.
        // While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error,
        // this section will set them to real objects
        if let resumeDic = getResumeDictionary(cData) {
            if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? NSData, let originalRequest = NSKeyedUnarchiver.unarchiveObjectWithData(originalReqData) as? NSURLRequest {
                task.setValue(originalRequest, forKey: "originalRequest")
            }
            if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? NSData, let currentRequest = NSKeyedUnarchiver.unarchiveObjectWithData(currentReqData) as? NSURLRequest {
                task.setValue(currentRequest, forKey: "currentRequest")
            }
        }

        return task
    }
}

Swift 3 :

func correct(requestData data: Data?) -> Data? {
    guard let data = data else {
        return nil
    }
    if NSKeyedUnarchiver.unarchiveObject(with: data) != nil {
        return data
    }
    guard let archive = (try? PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
        return nil
    }
    // Rectify weird __nsurlrequest_proto_props objects to $number pattern
    var k = 0
    while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "$\(k)") != nil {
        k += 1
    }
    var i = 0
    while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_prop_obj_\(i)") != nil {
        let arr = archive["$objects"] as? NSMutableArray
        if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] {
            dic.setObject(obj, forKey: "$\(i + k)" as NSString)
            dic.removeObject(forKey: "__nsurlrequest_proto_prop_obj_\(i)")
            arr?[1] = dic
            archive["$objects"] = arr
        }
        i += 1
    }
    if ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_props") != nil {
        let arr = archive["$objects"] as? NSMutableArray
        if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] {
            dic.setObject(obj, forKey: "$\(i + k)" as NSString)
            dic.removeObject(forKey: "__nsurlrequest_proto_props")
            arr?[1] = dic
            archive["$objects"] = arr
        }
    }
    /* I think we have no reason to keep this section in effect 
    for item in (archive["$objects"] as? NSMutableArray) ?? [] {
        if let cls = item as? NSMutableDictionary, cls["$classname"] as? NSString == "NSURLRequest" {
            cls["$classname"] = NSString(string: "NSMutableURLRequest")
            (cls["$classes"] as? NSMutableArray)?.insert(NSString(string: "NSMutableURLRequest"), at: 0)
        }
    }*/
    // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root"
    if let obj = (archive["$top"] as? NSMutableDictionary)?.object(forKey: "NSKeyedArchiveRootObjectKey") as AnyObject? {
        (archive["$top"] as? NSMutableDictionary)?.setObject(obj, forKey: NSKeyedArchiveRootObjectKey as NSString)
        (archive["$top"] as? NSMutableDictionary)?.removeObject(forKey: "NSKeyedArchiveRootObjectKey")
    }
    // Reencode archived object
    let result = try? PropertyListSerialization.data(fromPropertyList: archive, format: PropertyListSerialization.PropertyListFormat.binary, options: PropertyListSerialization.WriteOptions())
    return result
}

func getResumeDictionary(_ data: Data) -> NSMutableDictionary? {
    // In beta versions, resumeData is NSKeyedArchive encoded instead of plist
    var iresumeDictionary: NSMutableDictionary? = nil
    if #available(iOS 10.0, OSX 10.12, *) {
        var root : AnyObject? = nil
        let keyedUnarchiver = NSKeyedUnarchiver(forReadingWith: data)

        do {
            root = try keyedUnarchiver.decodeTopLevelObject(forKey: "NSKeyedArchiveRootObjectKey") ?? nil
            if root == nil {
                root = try keyedUnarchiver.decodeTopLevelObject(forKey: NSKeyedArchiveRootObjectKey)
            }
        } catch {}
        keyedUnarchiver.finishDecoding()
        iresumeDictionary = root as? NSMutableDictionary

    }

    if iresumeDictionary == nil {
        do {
            iresumeDictionary = try PropertyListSerialization.propertyList(from: data, options: PropertyListSerialization.ReadOptions(), format: nil) as? NSMutableDictionary;
        } catch {}
    }

    return iresumeDictionary
}

func correctResumeData(_ data: Data?) -> Data? {
    let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
    let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"

    guard let data = data, let resumeDictionary = getResumeDictionary(data) else {
        return nil
    }

    resumeDictionary[kResumeCurrentRequest] = correct(requestData: resumeDictionary[kResumeCurrentRequest] as? Data)
    resumeDictionary[kResumeOriginalRequest] = correct(requestData: resumeDictionary[kResumeOriginalRequest] as? Data)

    let result = try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, format: PropertyListSerialization.PropertyListFormat.xml, options: PropertyListSerialization.WriteOptions())
    return result
}

extension URLSession {
    func correctedDownloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask {
        let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
        let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"

        let cData = correctResumeData(resumeData) ?? resumeData
        let task = self.downloadTask(withResumeData: cData)

        // a compensation for inability to set task requests in CFNetwork.
        // While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error,
        // this section will set them to real objects
        if let resumeDic = getResumeDictionary(cData) {
            if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? Data, let originalRequest = NSKeyedUnarchiver.unarchiveObject(with: originalReqData) as? NSURLRequest {
                task.setValue(originalRequest, forKey: "originalRequest")
            }
            if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? Data, let currentRequest = NSKeyedUnarchiver.unarchiveObject(with: currentReqData) as? NSURLRequest {
                task.setValue(currentRequest, forKey: "currentRequest")
            }
        }

        return task
    }
}

5voto

user1012802 Points 113

En ce qui concerne la partie de la question relative à la unsupported URL et perte de resumeData sur panne de réseau, ou autre panne, j'ai enregistré un TSI avec Apple, et la dernière réponse de Quinn :

Tout d'abord, le comportement que vous observez est sans aucun doute un bogue dans le système NSURLSession. Nous espérons corriger ce problème dans une prochaine mise à prochaine mise à jour du logiciel. Ce travail est suivi par . I n'ai pas d'information à partager quant à la date à laquelle la correction sera livrée aux utilisateurs normaux d iOS.

En ce qui concerne les solutions de contournement, j'ai étudié ce problème en détail hier et j'ai maintenant comprends parfaitement l'échec. IMO il y a un moyen raisonnable de contourner mais je dois soumettre mes idées à l'ingénierie de NSURLSession avant de les partager. avant de pouvoir les partager. J'espère qu'ils me répondront dans les prochaines semaines. prochains jours. Veuillez patienter.

Je publierai des mises à jour au fur et à mesure, mais je suis sûr que cela donne un peu d'espoir aux gens qu'au moins le problème soit pris en compte par Apple.

(Un grand bravo à Mousavian pour la solution qu'il a trouvée pour le comportement de suspension/reprise)

UPDATE :

De Quinn,

En effet. Depuis notre dernière conversation (et je m'excuse d'avoir mis autant de temps à vous répondre ; j'ai été noyé sous les incidents ces derniers temps), j'ai approfondi cette question pour le compte d'autres développeurs et j'ai découvert que : A. Ce problème se manifeste dans deux contextes, caractérisés par les erreurs NSURLErrorCannotWriteToFile et NSURLErrorUnsupportedURL. B. Nous pouvons contourner la première erreur, mais pas la seconde. J'ai joint une mise à jour de mon document qui apporte des précisions. Malheureusement, nous n'avons pas pu trouver de solution pour le second symptôme. La seule solution est que les ingénieurs d'iOS corrigent ce bogue. Nous espérons que cela se produira lors d'une mise à jour du logiciel iOS 10, mais je n'ai pas de détails concrets à partager (autre que le fait que cette correction semble avoir manqué le bus 10.1)- :

Ainsi, malheureusement, le unsupported URL Il n'y a pas de solution pour résoudre ce problème et nous devons attendre que le bogue soit corrigé.

En NSURLErrorCannotWriteToFile est géré par le code de Mousavian ci-dessus.

UNE AUTRE MISE À JOUR :

Quinn a confirmé que la dernière version bêta de la 10.2 tente de résoudre ces problèmes.

A-t-il été testé sur la 10.2 ?

Oui, la correction de ce problème a été incluse dans la première version bêta de la 10.2. A développeurs avec lesquels j'ai travaillé m'ont signalé que ce correctif mais je vous recommande tout de même de l'essayer par vous-même sur la dernière dernière version bêta (actuellement iOS 10.2 bêta 2, 14C5069c). Faites-moi savoir si vous si vous rencontrez des problèmes.

1voto

leavesster Points 71

Voici le code Objective - C de la réponse de Mousavian.

Il fonctionne correctement sous iOS 9.3.5 (appareil) et iOS 10.1 (simulateur).

Corrigez d'abord les données du CV à la manière de Mousavian

 - (NSData *)correctRequestData:(NSData *)data
{
    if (!data) {
        return nil;
    }
    if ([NSKeyedUnarchiver unarchiveObjectWithData:data]) {
        return data;
    }

    NSMutableDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:nil];
    if (!archive) {
        return nil;
    }
    int k = 0;
    while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"$%d", k]]) {
        k += 1;
    }

    int i = 0;
    while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]) {
        NSMutableArray *arr = archive[@"$objects"];
        NSMutableDictionary *dic = [arr objectAtIndex:1];
        id obj;
        if (dic) {
            obj = [dic objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]];
            if (obj) {
                [dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]];
                [dic removeObjectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]];
                arr[1] = dic;
                archive[@"$objects"] = arr;
            }
        }
        i += 1;
    }
    if ([[archive[@"$objects"] objectAtIndex:1] objectForKey:@"__nsurlrequest_proto_props"]) {
        NSMutableArray *arr = archive[@"$objects"];
        NSMutableDictionary *dic = [arr objectAtIndex:1];
        if (dic) {
            id obj;
            obj = [dic objectForKey:@"__nsurlrequest_proto_props"];
            if (obj) {
                [dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]];
                [dic removeObjectForKey:@"__nsurlrequest_proto_props"];
                arr[1] = dic;
                archive[@"$objects"] = arr;
            }
        }
    }

    id obj = [archive[@"$top"] objectForKey:@"NSKeyedArchiveRootObjectKey"];
    if (obj) {
        [archive[@"$top"] setObject:obj forKey:NSKeyedArchiveRootObjectKey];
        [archive[@"$top"] removeObjectForKey:@"NSKeyedArchiveRootObjectKey"];
    }
    NSData *result = [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
    return result;
}

- (NSMutableDictionary *)getResumDictionary:(NSData *)data
{
    NSMutableDictionary *iresumeDictionary;
    if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 10) {
        NSMutableDictionary *root;
        NSKeyedUnarchiver *keyedUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
        NSError *error = nil;
        root = [keyedUnarchiver decodeTopLevelObjectForKey:@"NSKeyedArchiveRootObjectKey" error:&error];
        if (!root) {
            root = [keyedUnarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error];
        }
        [keyedUnarchiver finishDecoding];
        iresumeDictionary = root;
    }

    if (!iresumeDictionary) {
        iresumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:nil];
    }
    return iresumeDictionary;
}

static NSString * kResumeCurrentRequest = @"NSURLSessionResumeCurrentRequest";
static NSString * kResumeOriginalRequest = @"NSURLSessionResumeOriginalRequest";
- (NSData *)correctResumData:(NSData *)data
{
    NSMutableDictionary *resumeDictionary = [self getResumDictionary:data];
    if (!data || !resumeDictionary) {
        return nil;
    }

    resumeDictionary[kResumeCurrentRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeCurrentRequest]];
    resumeDictionary[kResumeOriginalRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeOriginalRequest]];

    NSData *result = [NSPropertyListSerialization dataWithPropertyList:resumeDictionary format:NSPropertyListXMLFormat_v1_0 options:0 error:nil];
    return result;
}

Je n'ai pas créé de catégorie pour NSURLSession, je l'ai juste créée dans My Singleton. Voici le code pour créer NSURLSessionDownloadTask :

    NSData *cData = [self correctResumData:self.resumeData];
    if (!cData) {
        cData = self.resumeData;
    }
    self.downloadTask = [self.session downloadTaskWithResumeData:cData];
    if ([self getResumDictionary:cData]) {
        NSDictionary *dict = [self getResumDictionary:cData];
        if (!self.downloadTask.originalRequest) {
            NSData *originalData = dict[kResumeOriginalRequest];
            [self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:originalData] forKey:@"originalRequest"];
        }
        if (!self.downloadTask.currentRequest) {
            NSData *currentData = dict[kResumeCurrentRequest];
            [self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:currentData] forKey:@"currentRequest"];
        }
    }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X