精华内容
下载资源
问答
  • app列表测试

    千次阅读 2017-02-03 16:19:03
    app列表分为二种类型:不能添加数据的列表,能添加数据的列表 不能添加数据的列表是那种只能从服务器获取数据,但客户端无法添加数据。 能添加数据的列表是那种可以从客户端添加数据的app系统。二种类型的列表测试...
            app列表分为二种类型:不能添加数据的列表,能添加数据的列表

    不能添加数据的列表是那种只能从服务器获取数据,但客户端无法添加数据。
    能添加数据的列表是那种可以从客户端添加数据的app系统。二种类型的列表测试策略有所不同,可以添加数据的列表问题会更多,会更难测试。

    1、不能添加数据的列表从下面几个方面考虑测试点

    (1)列表显示各字段格式正确性,无数据时列表显示默认图

    (2)刷新测试,上划 下拉 顶端下拉 无数据时上滑  弱网情况上划 下拉

    (3)当前页面切换 home键 休眠 后返回列表页面,详情页面返回列表页面 其他页面返回列表页面

    (4)图片 视频点击播放功能

    (5)系统设置字体大小时对列表字体是否有影响  各个机型的适配测试

    (6)各个机型的适配测试

    2、可以添加数据的列表从下面几个方面考虑测试点

    (1)列表显示各字段格式正确性,无数据时列表显示默认图

    (2)刷新测试,上划 下拉 顶端下拉 无数据时上滑  弱网情况上划 下拉

    (3)当前页面切换 home键 休眠 后返回列表页面,详情页面返回列表页面 其他页面返回列表页面 增加数据后系统自动返回到列表页面

    (4)图片 视频点击播放功能

    (5)系统设置字体大小时对列表字体是否有影响  各个机型的适配测试

    (6)各个机型的适配测试


    可以添加的列表与不可以添加数据列表的测试基本一致,只是多了增加数据后,系统自动返回到列表页面这个功能。

    经典问题记录,本系统提供弱网下提交信息,系统返回到列表页面,如提交失败,可再次在列表中点击提交的功能。

    问题1:增加数据提交后,系统返回到列表页面,该信息为上传中状态,点击刷新列表,会多出一条数据。

    问题2:增加数据提交后,系统返回到列表页面,提交成功,下拉或上划刷新列表,会出现闪退,或多出一条数据。

    问题3:增加数据提交后,系统返回到列表页面,该信息为上传中状态,点击刷新列表,信息变为上传失败,请重试,点击,会多出一条数据。

    问题4:   弱网刷新时,有时因为网络的原因,刷新没成功,但程序已经添加刷新页码,再刷新时会少显示一页

    问题5:弱网重新提交时,刷新提交后,会出现2条重复提交数据的情况
    展开全文
  • 这次的分享是关于如何在 AppStore 实现 App 的自动下载,理想中的目标是只需要一部手机,不需要人来干预,就可以模拟用户的真实下载,并在下载完成以后,可以自动更改手机参数,使之变为另外一部苹果手机,进行...

    这次的分享是关于如何在 AppStore 实现 App 的自动下载,理想中的目标是只需要一部手机,不需要人来干预,就可以模拟用户的真实下载,并在下载完成以后,可以自动更改手机参数,使之变为另外一部苹果手机,进行周而复始的下载工作。但是呢,本文的内容只包含如何去模拟用户的操作来完成下载,并不涉及抹机、IP 更换等内容。

    为什么做这个呢?

    可能会有人问,为什么要做这么一个项目。主要是两点原因吧,第一点呢,是出于个人兴趣,逆向其实在开发中的用处还是蛮大的,比如帮助我们分析 Apple 操作系统,帮我们做好安全防御。通过这么一个项目的实践,可以加深自己对逆向开发的理解,第二点呢,就是 App Search Optimization 是一个一直比较热门的话题,有白帽子和黑帽子 ASO 之分,通过关键字和标题优化等手段来进行 ASO 的属于白帽子 ASO,而通过刷榜程序来进行 ASO 的属于黑帽子 ASO,ASO 的刷榜脚本是价值不菲的,可能价值几十万甚至几百万。通过这个项目也是小试牛刀,了解下灰产的一些技术手段。

    什么是 ASO

    ASO 的全称是 App Search Optimization,就是提升你 APP 在 AppStore 排行榜和搜索结果排名的过程。我们经常可以看到 AppStore 有一些奇怪的五星好评,也会遇到搜索关键字,排名第一的是一个看上去完全不相关的 App。这些都是 ASO 优化的手段,帮助提升产品的曝光量。
    这里写图片描述
    白帽子 ASO 常用的手段就是通过数据分析,来优化关键词、标题等,进而提高 App 的排名和曝光率。而黑帽子的手段则是,通过刷榜程序来实现 App 的大量搜索、下载、好评这一系列的过程来提升 App 的排名。

    常见的刷榜手段主要有两种,一种是机刷,就是通过触动精灵或者代码注入的方式来实现模拟用户的真实操作,进而完成搜索、下载、评论等操作。再一种协议刷,就是破解 AppStore 的登陆、下载相关的网络协议,通过模拟真实的网络请求来实现登陆、下载等行为。据说在刷榜过程中,苹果会校验你的 Apple ID、IP 等信息,所以需要购买大量的 Apple ID 和不断更换 IP 地址。

    如何实现 App 的自动下载

    想要的效果:

    进入 AppStore,切换 tab 到搜索界面
    设置搜索关键字、搜索
    进入列表页后,点击 App 进入详情页点击下载
    根据提示完成登陆、下载,并在下载完成以后跳转到推荐 Tab
    进入推荐 Tab 后,退出登陆

    大概实现步骤:

    准备越狱手机和 Mac 电脑
    砸壳 dumpdecrypted,通常 PP助手、iTools 下载的 App 是经过砸壳的,同时 AppStore App 不需要砸壳
    头文件获取:AppStore class-dump,系统库的头文件的获取:dyld_cache class-dump
    定位关键函数:Reveal、Cycript、lldb
    tweak 的注入

    砸壳

    我们的 App 上传到 AppStore 后,苹果会对 App 进行加密,要想去分析可执行文件,就必须要进行脱壳解密的操作,dumpdecrypted 是一款出色的脱壳工具,它的原理是将 App 运行起来,App 启动时,系统会对 Mach-O 文件进行加载,并完成对应的解密操作,dumpdecrypted 就可以在此时将解密后的 Mach-O dump 出来,从而达到解密的效果。

    如果为了省事可以直接从 PP 助手、iTools 上下载对应的 App,一般情况下是已经经过砸壳的。同时,对于 AppStore 这样的系统程序有些特殊,他们 并不需要进行砸壳,可以直接拿来进行分析。

    获取头文件

    拿到一个砸壳后的可执行文件后,就可以使用 class-dump 来获取可执行文件的所有头文件,class-dump 会对 Mach-O 的格式进行分析,并将信息提取出来形成我们想要的头文件。

    AppStore 的可执行文件也略有特殊,class dump之后会发现 AppStore 中包含的代码极少。App Store 的很多关键代码逻辑都不在 AppStore 这个可执行文件当中,而是在系统的动态库中,我们需要分析动态库的头文件信息进而定位到关键函数。可以获取对应系统dyld_cache 中的动态库,然后 dump 出头文件。AppStore UI 有关的逻辑都在 StoreKitUI 动态库中,这个动态库是分析的重点。

    Reveal

    Reveal 是一款 UI 调试工具,官方的定义是:See your iOS application’s view hierarchy at runtime with advanced 2D and 3D visualisations,当然对于逆向安全人员,查看自己 App 的布局是完全不够的,我们可以在 Cydia 中下载 Reveal Loader,在同一网段下,通过 Mac 的 Reveal 和 iOS 上的 Reveal Loader 就可以查看任意 App 的 UI 布局。
    这里写图片描述
    但是,有时候我们不仅想要去看这个 UI 布局,还想要去动态调试这个布局,去看它的 Controller 是谁,去挖掘界面下的真正的代码逻辑。这个就涉及到 Cycript 这个工具。

    Cycript

    Cycript 是由 Cydia 创始人 Saurik 推出的一款脚本语言,它混合了Objective-C 与 JavaScript 两种语法,很容易上手,我们可以通过 Cycript 来进行动态调试,比如查看函数运行的效果,寻找 View 的 Controller 等。
    这里写图片描述

    就拿上面 Reveal 详情页为例, Reveal 可以看到获取按钮是 SKUIOfferView,列表页是一个 SKUICollectionView ,那么就通过 Cycript 来看看控制这个 SKUICollectionView 的 Controller 是谁。首先通过 OpenSSH 来连接 iPhone,通过 cycript -p AppStore 来对 AppStore 进行注入调试,UIApp.keyWindow.recursiveDescription().toString() 来打印视图层级。(注:此截图和后面的地址对不上,因为不是同一次打印,大家了解下大概意思就成)
    这里写图片描述
    可以发现 SKUICollectionView,并且它的内存地址是 0x13fa00e00,可以通过 cycript 脚本来找到它的 Controller 是哪一个,有多种方案,比如通过它的 delegate 来找,或者通过 nextResponder 来找都可以。

    cy# [#0x13fa00e00 delegate]
    
    #"<SKUIStorePageSectionsViewController: 0x140167e00>"
    
    cy# [#0x13fa00e00 nextResponder]
    
    #"<UIView: 0x140f5f540; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x140f771c0>>"
    
    cy# [#0x140f5f540 nextResponder]
    
    #"<SKUIStorePageSectionsViewController: 0x140167e00>"

    同时也可以借助一些私有 API 来实现快速查找 ViewController,使用[[[UIWindow keyWindow] rootViewController] _printHierarchy].toString(),可以发现打印结果中同样可以找到 SKUIStorePageSectionsViewController

    cy# [[[UIWindow keyWindow] rootViewController] _printHierarchy].toString()
    `<SKUITabBarController 0x157815400>, state: appeared, view: <UILayoutContainerView 0x156db38e0>
       | <UINavigationController 0x15784d200>, state: disappeared, view: <UILayoutContainerView 0x156e6b240> not in the window
       |    | <SKUIDocumentContainerViewController 0x1578d3c00>, state: disappeared, view: <UIView 0x1580e1aa0> not in the window
       |    |    | <SKUIStackDocumentViewController 0x15812b740>, state: disappeared, view: <UIView 0x1580dc870> not in the window
       |    |    |    | <SKUIStorePageSectionsViewController 0x1578ec000>, state: disappeared, view: <UIView 0x1580f1a30> not in the window
       |    |    |    |    | <SKUIAccountButtonsViewController 0x158654180>, state: disappeared, view: <SKUIAccountButtonsView 0x158654f60> not in the window
       | <UINavigationController 0x157849c00>, state: disappeared, view: <UILayoutContainerView 0x156ec4df0> not in the window
       | <UINavigationController 0x157803600>, state: disappeared, view: <UILayoutContainerView 0x156e80de0> not in the window
       | <UINavigationController 0x15703ea00>, state: appeared, view: <UILayoutContainerView 0x156f114a0>
       |    | <SKUIDocumentContainerViewController 0x157ab2a00>, state: disappeared, view: <UIView 0x158a25930> not in the window
       |    |    | <SKUIStackDocumentViewController 0x158a50690>, state: disappeared, view: <UIView 0x158a2b360> not in the window
       |    |    |    | <SKUIStorePageSectionsViewController 0x1578e6000>, state: disappeared, view: <UIView 0x158a2d4b0> not in the window
       |    | <SKUIDocumentContainerViewController 0x157b5fa00>, state: appeared, view: <UIView 0x158cf70e0>
       |    |    | <SKUIStackDocumentViewController 0x158cf6690>, state: appeared, view: <UIView 0x158cf72b0>
       |    |    |    | <SKUIStorePageSectionsViewController 0x157b4ae00>, state: appeared, view: <UIView 0x158cfb1e0>
       | <UINavigationController 0x157028000>, state: disappeared, view: <UILayoutContainerView 0x156ef1300> not in the window
       |    | <ASUpdatesViewController 0x156f169e0>, state: disappeared, view: <UIView 0x156dbd590> not in the window`

    从上面的分析可以知道,SKUICollectionView 的控制器是 SKUIStorePageSectionsViewController,「获取」按钮的类是 SKUIOfferView,下一步是分析头文件,看看有没有可以比较明显的方法可以为我们所用。下载是最关键的一步,那么首先来看看 SKUIOfferView 类的情况,它的头文件大致如此。

    #import <StoreKitUI/SKUIItemOfferButtonDelegate-Protocol.h>
    #import <StoreKitUI/SKUIViewElementView-Protocol.h>
    @class NSMapTable, NSMutableArray, NSString;
    @protocol SKUIOfferViewDelegate;
    @interface SKUIOfferView : SKUIViewReuseView <SKUIItemOfferButtonDelegate, SKUIViewElementView> {
        unsigned long long _alignment;
        NSMapTable *_buttonElements;
        NSMapTable *_buyButtonDescriptorToButton;
        struct UIEdgeInsets _contentInset;
    }
    - (void)_buttonAction:(id)arg1;
    - (void)itemOfferButtonWillAnimateTransition:(id)arg1;
    - (void)itemOfferButtonDidAnimateTransition:(id)arg1;
    - (struct CGSize)sizeThatFits:(struct CGSize)arg1;

    可以从头文件中看到一个 _buttonAction 方法,感觉上是 「获取」按钮点击后的响应方法,对于这种猜测,可以使用 Cycript 来进行调试,测试一下这个函数执行的效果到底如何 在终端执行 [#0x156c69cc0 _buttonAction:#0x156cb4d20] 后查看效果如下,App 已经开始进行下载了,说明这个方法的效果我们猜对了,在调试过程中,可以多多使用 Cycript 提高效率。
    这里写图片描述

    lldb

    上面我们使用 Cycript 测试了 _buttonAction 的效果,但是这个方法有一个参数,我们要搞清楚它正确的参数类型,传入正确的值。这时候可以借助 LLDB ,来帮助我们找到这个参数的正确类型。 可以使用 b function 来针对 _buttonAction 方法打断点,然后打印它的参数。

    传统的做法是使用LLDB 和 IDA 等工具找到 ASLR 和 基地址等信息,然后计算出符号的地址,这样做起来比较繁琐,还是可以继续使用一些私有方法快速定位 _buttonAction 的符号地址来进行断点。

    我们想要断点的方法是 _buttonAction,它所在的类是 SKUIOfferView,那么可以使用 LLDB 输入 po [SKUIOfferView _shortMethodDescription] 来看下效果:(更多强大的黑科技私有函数可以参考这里:http://iosre.com/t/powerful-private-methods-for-debugging-in-cycript-lldb/3414

    (lldb) po [SKUIOfferView _shortMethodDescription]
    <SKUIOfferView: 0x1a096ddd8>:
    in SKUIOfferView:
        Class Methods:
            + (void) requestLayoutForViewElement:(id)arg1 width:(double)arg2 context:(id)arg3; (0x194719470)
            + (CGSize) sizeThatFitsWidth:(double)arg1 viewElement:(id)arg2 context:(id)arg3; (0x1947197a8)
        Properties:
            @property (weak, nonatomic) <SKUIOfferViewDelegate>* delegate;  (@synthesize delegate = _delegate;)
            @property (nonatomic) long metadataPosition;  (@synthesize metadataPosition = _metadataPosition;)
            @property (readonly, nonatomic, getter=isShowingConfirmation) BOOL showingConfirmation;  (@synthesize showingConfirmation = _isShowingConfirmation;)
        Instance Methods:
            - (BOOL) setImage:(id)arg1 forArtworkRequest:(id)arg2 context:(id)arg3; (0x19471a8c8)
            - (BOOL) updateWithItemState:(id)arg1 context:(id)arg2 animated:(BOOL)arg3; (0x19471a8d0)
            - (void) _buttonAction:(id)arg1; (0x19471bb5c)
            - (BOOL) _shouldHideNoticesWithBuyButtonDescriptor:(id)arg1 context:(id)arg2; (0x19471c368)
            - (void) _positionNoticeForItemOfferButton:(id)arg1; (0x19471c234)
    (SKUIViewReuseView ...)

    可以看到 - (void) _buttonAction:(id)arg1; (0x19471bb5c),那么直接使用 b 0x19471bb5c为 _buttonAction 加断点即可。断点到以后,再打印它的参数,对于 Objective-C 来说消息有两个隐含参数,也就是 self 和 _cmd,那么我们想要的参数就在第三个位置,可以通过 po $x2 来查看它的具体信息(ARM64 下函数的参数是存放在 X0 到 X7 这 8 个寄存器里面的,如果超过8个参数,就会入栈)。

    Process 7839 stopped
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 2.1 3.1
        frame #0: 0x000000019471bb5c StoreKitUI`-[SKUIOfferView _buttonAction:]
    StoreKitUI`-[SKUIOfferView _buttonAction:]:
    ->  0x19471bb5c <+0>:  stp    x24, x23, [sp, #-0x40]!
        0x19471bb60 <+4>:  stp    x22, x21, [sp, #0x10]
        0x19471bb64 <+8>:  stp    x20, x19, [sp, #0x20]
        0x19471bb68 <+12>: stp    x29, x30, [sp, #0x30]
    Target 0: (AppStore) stopped.
    (lldb) po $x0
    <SKUIOfferView: 0x1596aae00; frame = (279 74; 26 26); layer = <CALayer: 0x1596676b0>>
    (lldb) po $x2
    <SKUIItemOfferButton: 0x1596ab260; baseClass = UIControl; frame = (0 0; 26 26); clipsToBounds = YES; alpha = 0.2; tintColor = UIDeviceRGBColorSpace 0.0862745 0.0156863 0.0156863 1; animations = { opacity=<CABasicAnimation: 0x1592e7b20>; }; layer = <CALayer: 0x15967d9c0>>

    由上可知,参数类型是 SKUIItemOfferButton,也就是 SKUIOfferView 的 subView,其实点击的是 SKUIItemOfferButton,只是 SKUIItemOfferButton 将处理往上抛而已。

    Tweak 注入

    Cydia 创始人 Saurik 同时为我们提供了一个 Cydia Substrate 这么一个工具,官方的定义是:The powerful code modification platform behind Cydia。我们可以基于 Cydia Substrate 来开发具有各种功能的代码注入程序。

    Cydia Substrate 由 MobileHooker、MobileLoader、Safe mode 三个模块组成。MobileHooker 主要用来替换函数的实现,可以想象成 Runtime 的 Method Swizzle。MobileLoader 是用来加载第三方 dylib 的,我们写的破解程序会在目标程序启动时注入到目标程序。Safe mode 就是安全模式,我们写 tweak 的时候可能会造成 Crash,比如万一造成 SpringBoard 无限 Crash 手机岂不是就没法用了,所以提供了这么一个安全模式。

    MobileHooker 提供了一些函数来让我们完成 Hook 的工作,但是我们不直接使用 它们,我们使用基于他们封装的 Logos 工具,Logos 的语法很简单直观,易于上手。比如 %hook 可以指定要 Hook 的类、%orig 可以执行被钩住的函数的原始实现、%new 给一个现成的 class 添加新函数(效果与 class_addMethod 类似)。

    Tweak AppStore

    那我们来使用 Logos 实现下载的功能,当进入 SKUIStorePageSectionsViewController 页面后,找到下载按钮,然后点击下载,当下载按钮的文字由「获取」变为「打开」,代表下载已完成,然后继续执行后续操作。

    %hook SKUIStorePageSectionsViewController
    - (void)viewDidAppear:(BOOL)animated {    
        %log;
        %orig;
        // 遍历所有子 View,找到 offerButton 、offerView
        [self findAllSubviews:self.view];
        if (offerButton && offerView) {
            // 执行下载操作
            [offerView _buttonAction:offerButton];
            // 每秒去 check 一下,是否下载完成
            downloadTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        }       
    }
    %new
    -(void)timerAction {
        if ([offerButton.title isEqualToString:@"打开"]) {
            // 发送下载完成的通知
            [[NSNotificationCenter defaultCenter] postNotificationName:@"textChangedAction" object:nil];
            downloadTimer = nil;
        }
    }
    %new
    -(void)findAllSubviews:(UIView *)view
     {
        for (UIView *subView in view.subviews) {
            if (subView.subviews.count) {
                [self findAllSubviews:subView];
            }
            if ([subView isKindOfClass:NSClassFromString(@"SKUIOfferView")]) {
                offerView = (SKUIOfferView*)subView;
            }
            if ([subView isKindOfClass:NSClassFromString(@"SKUIItemOfferButton")]) {
                offerButton = (SKUIItemOfferButton*)subView;
            }
        }
    }
    %end

    其他的操作,与上述其实很类似,比如搜索、跳转都是利用静态或者动态分析找到关键函数,通过 tweak 来实现想要的效果即可。其中还有一个较难的点,就是弹窗提示我们登陆怎么办?如何实现自动登录功能?

    Tweak SpringBoard

    首先,想到的就是在 AppStore App 中注入代码,Hook UIAlertAction 和 UIAlertController 的代码,会发现并没有产生作用。AppStore 中的弹窗不是它来控制的,而是另外一个进程 SpringBoard,所以要想实现 Hook AppStore 的弹窗,必须对 SpringBoard 进行代码注入。
    这里写图片描述
    我们正常如果要实现一个这种弹窗,代码一般是这么写

    UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:@"标题" message:@"注释信息" preferredStyle:UIAlertControllerStyleActionSheet];  
    UIAlertAction *action1 = [UIAlertAction actionWithTitle:@"标题1" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {  
        NSLog(@"点击了按钮 1");  
    }];  
    UIAlertAction *action2 = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {  
        NSLog(@"点击了按钮 2");  
    }];  
    [actionSheet addAction:action1];  
    [actionSheet addAction:action2];  
    [self presentViewController:actionSheet animated:YES completion:nil];  

    基于上面的代码分析可得,我们要想实现自动登录,就要实现自动点击「使用现有的 Apple ID」执行系统的原 action 操作,然后在账号和密码的 TextField 中填入账号密码,点击「好」执行系统的原始 action 操作。其实可以发现,要执行的 action 其实是在初始化 UIAlertAction 过程中,handler block 中加入的逻辑。那么我们就可以 Hook actionWithTitle:style:handler: 然后将 handler 保存下来,当填写好账号密码后,主动触发 handler 即可。

    上面那种方法也可以奏效,但是需要自己额外处理下 alertView 的出现和消失, 为了简单可以直接尝试第二种方法,在分析 UIKit 框架中 UIAlertController 类的头文件时发现 _dismissWithAction:这个方法,然后我就试了一下发现可以完成 dismiss 和 执行 handler 两项功能,所以我就直接使用了这个 API 来模拟点击。核心代码如下:

    typedef void(^CDUnknownBlockType)(UIAlertAction *action);
    CDUnknownBlockType testBlock;
    static UIAlertAction *keepAction;
    static int atimers;
    %hook UIAlertController
    - (void)viewDidAppear:(BOOL)animated {
        %log;
        %orig;
        if ([keepAction.title isEqualToString:@"使用现有的 Apple ID"]) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                ((void ( *)(id, SEL, UIAlertAction*))objc_msgSend)(self, NSSelectorFromString(@"_dismissWithAction:"),keepAction);
            });
        } 
        if ([keepAction.title isEqualToString:@"好"]) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                if (self.textFields.count > 1) {
                    self.textFields.firstObject.text = @"joyme0104@163.com";
                    self.textFields.lastObject.text = @"Joyme0304&&&";
                    ((void ( *)(id, SEL, UIAlertAction*))objc_msgSend)(self, NSSelectorFromString(@"_dismissWithAction:"),keepAction);
                }
            });
        }
    }
    %end
    %hook UIAlertAction
    + (id)_actionWithTitle:(id)arg1 descriptiveText:(id)arg2 image:(id)arg3 style:(long long)arg4 handler:(CDUnknownBlockType)arg5 shouldDismissHandler:(CDUnknownBlockType)arg6 {
        id obj = %orig;
        UIAlertAction *action = (UIAlertAction *)obj;
        if ([action.title isEqualToString:@"使用现有的 Apple ID"]) {
            testBlock = arg6;
            keepAction = obj;
        } 
        if ([action.title isEqualToString:@"好"]) {
            testBlock = arg6;
            keepAction = obj;
        }
        return obj;
    }
    %end

    从代码可以看出我们在 Hook UIAlertAction 的 _actionWithTitle 方法时,并没有 Hook actionWithTitle:style:handler: ,因为我测试的时候发现在我操作过程中并没有触发,怀疑是苹果没有使用这个 API,直接使用了下面这个方法。

    
    + (id)_actionWithTitle:(id)arg1 descriptiveText:(id)arg2 image:(id)arg3 style:(long long)arg4 handler:(CDUnknownBlockType)arg5 shouldDismissHandler:(CDUnknownBlockType)arg6 {
    }

    Thinking About The Future

    适当增加对 App 安全的精力的投入,像现在业界的很多 App 都处于被破解的状态,网上随处可见各种 App 的破解版,比如爱奇艺会员破解、钉钉远程打卡等。从客户端角度出发,需要增加代码混淆、反调试等手段保证运行环境的安全,同时与后端人员合作增加保证网络数据链路、反作弊的手段。

    Summary

    本文首先介绍了常见的攻击手段:

    通过静态分析和动态分析掌握 App 的内部逻辑,通过代码注入实现我们想要的功能,比如自动下载、自动跳转等功能
    通过分析 App 的网络请求,破解网络协议,模拟真实的网络请求来达到某种目的,比如批量下载,批量评论等功能。

    然后介绍了 ASO 的影响因素都有哪些,以及黑帽子和白帽子都是怎么进行 ASO 优化的。最后重点写了如何一步步通过代码注入,实现 AppStore App 的自动登录。

    君凯商联网-iOS-字唐名僧

    展开全文
  • 获取手机应用列表APP应用信息

    千次阅读 2016-11-02 13:31:20
    appInfo.setApp_version(app_version); //获取应用的使用权限 String[] app_permission = packageInfo.requestedPermissions; Log.e( "appinfo" , app_permission.length + "=数量" ); appInfo....

    各种获取应用信息小功能,做个笔记:

      /**
         * 获取版本名
         *
         * @param context
         * @return 获取版本名
         */
        public static String getVersionName(Context context) {
            PackageInfo packageInfo = null;
            try {
                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
                return packageInfo.versionName;
            } catch (NameNotFoundException e) {
                e.printStackTrace();
            }
            return null;
    
        }
       /**
         * 获取版本号
         *
         * @param context
         * @return
         */
        public static int getVersionCode(Context context) {
            PackageInfo packageInfo = null;
            try {
                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
                return packageInfo.versionCode;
            } catch (NameNotFoundException e) {
                e.printStackTrace();
            }
            return 0;
        }
    /**
         * 获取application中指定的meta-data
         *
         * @return 如果没有获取成功(没有对应值, 或者异常),则返回值为空
         */
        public static String getAppMetaData(Context ctx, String key) {
            if (ctx == null || TextUtils.isEmpty(key)) {
                return null;
            }
            String resultData = "";
            try {
                PackageManager packageManager = ctx.getPackageManager();
                if (packageManager != null) {
                    ApplicationInfo applicationInfo = packageManager.getApplicationInfo(ctx.getPackageName(), PackageManager.GET_META_DATA);
                    if (applicationInfo != null) {
                        if (applicationInfo.metaData != null) {
                            resultData = applicationInfo.metaData.getString(key);
                        }
                    }
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
            return resultData;
        }
     /**
         * 获取安装本应用的手机及手机卡信息
         *
         * @param context
         * @return
         */
        public static String getPhoneInfo(Context context) {
            /**
             * imei,移动设备标识码
             */
            String device_id = "";
            /**
             *手机号码标识
             */
            String imsi = "";
            /**
             * 手机型号
             */
            String phoneModel = "";
            /**
             * 手机操作系统版本
             */
            String phoneVersion = "";
            /**
             * 手机号码
             */
            String phoneNum = "";
            /**
             * 手机物理标识
             */
            String mac = "";
    
            try {
                org.json.JSONObject json = new org.json.JSONObject();
                android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) context
                        .getSystemService(Context.TELEPHONY_SERVICE);
    
                if (checkPermission(context, Manifest.permission.READ_PHONE_STATE)) {
                    device_id = tm.getDeviceId();
                    imsi = tm.getSubscriberId();
                    phoneNum = tm.getLine1Number();
                    phoneModel = android.os.Build.MODEL;
                    phoneVersion = android.os.Build.VERSION.RELEASE;
                }
                android.net.wifi.WifiManager wifi = (android.net.wifi.WifiManager) context
                        .getSystemService(Context.WIFI_SERVICE);
                mac = wifi.getConnectionInfo().getMacAddress();
    
                if (TextUtils.isEmpty(device_id)) {
                    device_id = mac;
                }
                if (TextUtils.isEmpty(device_id)) {//如果Android pad没有IMEI
                    device_id = android.provider.Settings.Secure.getString(context.getContentResolver(),
                            android.provider.Settings.Secure.ANDROID_ID);
                }
    
                json.put("mac", mac);
                json.put("imei", device_id);
                json.put("imsi", imsi);
                json.put("phoneModel", phoneModel);
                json.put("phoneVersion", phoneVersion);
                json.put("phoneNum", phoneNum);
    
                return json.toString();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private static boolean checkPermission(Context context, String permission) {
            boolean result = false;
            if (Build.VERSION.SDK_INT >= 23) {
                if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
                    result = true;
                }
            } else {
                PackageManager pm = context.getPackageManager();
                if (pm.checkPermission(permission, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
                    result = true;
                }
            }
            return result;
        }

    获取手机上安装的所有应用:

    /**
     * 获取手机应用程序
     * Created by songyaru on 16/9/29.
     */
    public class AppInfoService {
        private Context context;
        private PackageManager pm;
    
        public AppInfoService(Context context) {
            this.context = context;
            pm = context.getPackageManager();
        }
    
        /**
         * 得到手机中所有的应用程序信息
         *
         * @return
         */
        public List<AppInfo> getAppInfos() {
            //创建要返回的集合对象
            List<AppInfo> appInfos = new ArrayList();
            //获取手机中所有安装的应用集合
            List<ApplicationInfo> applicationInfos = pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);
            //遍历所有的应用集合
            for (ApplicationInfo info : applicationInfos) {
    
                AppInfo appInfo = new AppInfo();
    
                //获取应用程序的图标
                Drawable app_icon = info.loadIcon(pm);
                appInfo.setApp_icon(app_icon);
    
                //获取应用的名称
                String app_name = info.loadLabel(pm).toString();
                appInfo.setApp_name(app_name);
    
                //获取应用的包名
                String packageName = info.packageName;
                appInfo.setPackagename(packageName);
                try {
                    //获取应用的版本号
    //                PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
                    PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES
                            | PackageManager.GET_PERMISSIONS);
                    String app_version = packageInfo.versionName;
                    appInfo.setApp_version(app_version);
    
                    //获取应用的使用权限
                    String[] app_permission = packageInfo.requestedPermissions;
                    Log.e("appinfo", app_permission.length + "=数量");
                    appInfo.setAppPremission(app_permission);
                } catch (PackageManager.NameNotFoundException e) {
                    e.printStackTrace();
                }
    
                //判断应用程序是否是用户程序
                boolean isUserApp = filterApp(info);
                appInfo.setIsUserApp(isUserApp);
    
    
                appInfos.add(appInfo);
            }
            return appInfos;
        }
    
        //判断应用程序是否是用户程序
        public boolean filterApp(ApplicationInfo info) {
            //原来是系统应用,用户手动升级
            if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
                return true;
                //用户自己安装的应用程序
            } else if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
                return true;
            }
            return false;
        }
    }

    用与存储应用信息的javaBean:

    /**
     * Created by songyaru on 16/9/29.
     * 获取的应用基本信息实体类
     */
    public class AppInfo {
        //图标
        private Drawable app_icon;
        //应用名称
        private String app_name;
        //应用版本号
        private String app_version;
        //应用包名
        private String packagename;
        //是否是用户app
        private boolean isUserApp;
        //应用所需权限
        private String[] appPremission;
    
    
        public AppInfo() {
            super();
        }
    
        public AppInfo(Drawable app_icon, String app_name, String app_version,
                       String packagename) {
            super();
            this.app_icon = app_icon;
            this.app_name = app_name;
            this.app_version = app_version;
            this.packagename = packagename;
        }
    
    
        public AppInfo(Drawable app_icon, String app_name, String app_version,
                       String packagename, boolean isUserApp, String[] appPremission) {
            super();
            this.app_icon = app_icon;
            this.app_name = app_name;
            this.app_version = app_version;
            this.packagename = packagename;
            this.isUserApp = isUserApp;
            this.appPremission = appPremission;
        }
    
        public Drawable getApp_icon() {
            return app_icon;
        }
    
        public void setApp_icon(Drawable app_icon) {
            this.app_icon = app_icon;
        }
    
        public String getApp_name() {
            return app_name;
        }
    
        public void setApp_name(String app_name) {
            this.app_name = app_name;
        }
    
        public String getApp_version() {
            return app_version;
        }
    
        public void setApp_version(String app_version) {
            this.app_version = app_version;
        }
    
        public String getPackagename() {
            return packagename;
        }
    
        public void setPackagename(String packagename) {
            this.packagename = packagename;
        }
    
        public boolean isUserApp() {
            return isUserApp;
        }
    
        public void setIsUserApp(boolean isUserApp) {
            this.isUserApp = isUserApp;
        }
    
        public String[] getAppPremission() {
            return appPremission;
        }
    
        public void setAppPremission(String[] appPremission) {
            this.appPremission = appPremission;
        }
    
        @Override
        public String toString() {
            return "AppInfo{" +
                    "app_icon=" + app_icon +
                    ", app_name='" + app_name + '\'' +
                    ", app_version='" + app_version + '\'' +
                    ", packagename='" + packagename + '\'' +
                    ", isUserApp=" + isUserApp +
                    ", appPremission=" + Arrays.toString(appPremission) +
                    '}';
        }
    }
    展开全文
  • 基于Android开发的天气预报app(源码下载)

    万次阅读 多人点赞 2017-05-25 20:37:35
    基于AndroidStudio环境开发的天气app -系统总体介绍:本天气app使用AndroidStudio这个IDE工具在Windows10系统下进行开发。主要实现了:1、定位城市天气显示;2、城市编辑功能(增、删、改、查)以及对应天气显示...

    基于AndroidStudio环境开发的天气app

    由于需要源码的人特别多,我特地花时间修复了旧的天气接口无法使用的问题并开源了源码,需要的自取。

    • 源码地址:https://github.com/LuoPeiQin/DongWeather
    • 发现很多人有了源码地址,还是在下面留言要源码,应该是新手不会下载和打开GitHub的源码,所以特地写了一篇下载并打开源码教程的文章,帮助大家下载源码并打开和运行
    • 如果觉得对你有帮助,麻烦帮忙点个赞,谢谢!

    主要实现了:
    1、定位城市天气显示;
    2、城市编辑功能(增、删、改、查)以及对应天气显示信息的改变;
    3、天气信息的Widget窗口显示(城市的编辑功能可以远程的更新Widget窗口信息的显示)
    4、下拉刷新、天气显示界面左右滑动、城市拖拽等小模块

    一、 开发需求分析

    1、开发环境搭建 - AndroidStudio

    安装步骤:
    (1)下载。建议在官网下载,因为国内很多映射文件多少都是有点年代的,还是下载最新的比较好,也免去更新的麻烦。不过国内由于网络限制,上Android官网需要翻墙。AndroidStudio下载地址:https://developer.android.google.cn/studio/index.html里面是最新版本的AndroidStudio。
    (2)安装,下载完成之后没有特别要求的话默认选择一直next就好了,一般的话只需要改下安装路径,避免C盘空间不够。因为AndroidStudio自带了JDK和Android SDK,所以Android完成后就可以直接进行开发了。
    

    2、城市信息获取的api

    城市信息这里我直接使用的是Android前辈搭建的一个服务器获取的,数据链接是http://guolin.tech/api/china,访问返回的是JSON数据类型的省份信息(JSON数据类型的解析后面会再详细说明),需要返回城市时只需要在本链接后加上“/对应省份id”即可获取到相应的城市信息,县市信息也是一样的,原链接加上“/对应省份id/对应城市id”即可。
    这里其实也可以从其它天气服务商提供的api接口获取城市信息。

    3、天气信息获取的api

    天气信息的获取我使用的是和风天气提供的免费的api,和风天气每天有提供4000次免费的基础天气查询,用来做开发测试是足够用的了。而且和风天气api接口返回的JSON数据类型也比较简单,作为Android初学者做项目是比较好的。想使用该接口只需要简单注册一个账号就可以了(对返回数据的处理我后面再详细说明)。老手的话可以在网上搜索别的服务商提供的免费接口,现在网上的免费接口少了很多,不过有还是有的。

    4、定位信息获取的api

    我这里使用的是百度提供的免费api接口http://lbsyun.baidu.com/apiconsole/key,因为Android原生定位API在国产手机中一般被阉割了,或者国内网络限制的原因,使用Android原生定位API一般是很难获取到定位信息的,跟手机厂商和网络环境都有关系。所以这边为了避免这种情况的不确定因素,我选择了使用百度提供的免费地位接口,在国内,百度和高德定位服务做得都还是不错的。使用百度定位api接口同样需要注册一个百度开发者账号,因为这不是本篇文章的重点。这边具体的操作就不再说明了。

    二、 系统设计分析

    1、天气信息界面显示设计

    首先先上效果图:
    天气主界面图

    接下来我介绍一下天气显示信息中用到的一些设计:
    首先是功能实现上的:
    1)首先背景图片是每天会更新的,是从必应网上获取到的背景图片。
    2)下拉刷新功能。
    3)天气显示信息左右活动切换已选择要显示的城市。
    4)通过点击右上角的编辑按钮进入城市管理功能。
    5)导航组件功能。
    6)小时天气小时超出屏幕宽度时的当前页面左右滑动。

    其次是具体显示上的(分为一个城市的天气信息一个页面,每个页面又有七个模块)我们从上往下分析:
    1)最上部分是城市名的显示和编辑按钮。
    2)然后是导航原点显示。
    3)其次是当前温度,当天天气和当天最低最高温的显示。(1)(2)部分都是用户比较关心的问题,所以我们放在最前面。
    4)接下来是将来的小时预告,由于和风天气返回的数据只有当天每三小时的天气预告,所以这边的显示实现得比较差,不过我这里做的是兼容可以扩展的,不管数据多少都可以显示。如果将来需要更改数据源,这里的操作将非常简单 。
    5)接着显示的是接下来几天的天气的大体介绍,这里显示的数据同样受限于获取到的数据。
    6)再接着是一些生活指数的显示,由于我艺术细胞不太好,所以这里的图片显示有点丑。。你们可以根据自己的喜好去更改图片。
    7)最后就是一些生活建议的显示了。

    2、已选择城市信息界面显示设计

    先上图吧
    城市编辑显示1
    城市编辑显示2

    这里主要是有点击编辑前后的区别

    下面我们来一一说明:
    点击编辑前
    布局主要分成三个部分:
    1、最上方的:
    *左侧返回按钮,回到天气显示界面
    *中间固定的“城市管理”四个字
    *右侧的编辑按钮,点击之后就可以对城市进行增、删、和更改位置了
    2、中间部分:
    *中间部分是已选择城市信息的显示
    3、最下方部分:
    *最下面是一个添加城市的按钮,点击之后进入城市添加功能

    点击编辑后
    1、最上方的:
    *左侧取消按钮,即放弃本次编辑后的结果,回到非编辑界面
    *中间固定的“城市管理”四个字
    *右侧的保存按钮,即保存本次编辑的结果并回到非编辑界面
    2、中间部分:
    *中间部分是已选择城市信息的显示,与编辑前不同的是增加了左侧拖动改变顺序的按钮和右侧的删除城市按钮
    3、最下方部分:
    *最下面是一个添加城市的按钮,点击之后进入城市添加功能

    所用到的功能点
    1、dragListView:可拖拽的listview
    2、Android自带数据库
    3、重叠按钮的实现
    以上功能模块下面我都会一一说明

    3、添加城市信息界面显示设计

    先上图:
    添加城市显示图1
    添加城市显示图2

    说明
    这里的实现比较简单,就是使用ListView去显示省、市、县三个级别的城市,根据选择的城市去网络或者本地加载数据,然后显示。

    4、Widget设计

    同样先上图
    widget显示设计

    说明
    这里的实现显示上比较简单,但是功能逻辑和实现上相对复杂。

    显示上的设计
    1、背景图片:widget的背景图片同样是网络上下载并且每天会自动更换的,不同的是为了保证用户滑动界面时的流畅性,这里做了图片缩放处理之后再显示。
    2、中间固定文字“当前天气”
    3、下面是一个ListView用来显示简略的已选择城市的信息

    功能上的设计
    1、服务listView改变的server进程
    2、contentProvider提供跨进程间的数据通信
    3、图片下载的异步线程和图片缩放实现
    4、异步线程与UI线程通过handler实现通信

    5、界面转换设计

    有界面转换实现的:
    1、点开app进入到城市天气信息显示界面
    2、点击编辑按钮进入到城市管理界面
    3、城市管理界面中点击添加按钮进入到城市添加界面
    4、城市管理界面中点击返回按钮回到城市天气信息显示界面
    5、添加城市界面中添加完成或者点击返回按钮回到城市天气信息显示界面

    6、系统总体和局部流程设计(流程图)

    这里写图片描述

    由于时间原因,这边就先绘制一个流程图了,别的流程图等后面有时间了再绘制

    三、 系统功能模块实现(代码部分)

    前面介绍了那么多,现在终于到了重点了,前面讲述的功能我在这里都将为大家一一说明。
    首先给大家看一下工程目录的截图:
    工程目录图

    项目总体流程思路

    接下来我根据项目的实现过程来给大家介绍整个项目的总体流程

    1、天气app最重要的是获取城市列表和天气信息,所以首先要解决的问题是在网络上找到合适的api接口,并根据服务商提供的数据转换成自己需要显示的数据。
    2、有了需要的显示信息之后,我们需要自己去设计怎么显示,怎么让用户去有一个好的体验。我的设计是在使用三个Activity去和用户交互,参照我的项目截图,其中WeatherActivity作为启动活动,用于显示天气信息,提供的是多页带导航栏可左右滑动的效果。ChooseAreaActivity是管理城市的活动,用于添加、删除、改变要显示天气信息的城市列表。AddCountyActivity是用于添加城市的活动。
    3、实现了这些基本的城市管理和天气显示之后,接下来就是进阶功能了,首先我们实现百度定位功能,根据定位结果加载当前城市天气。
    4、实现widget功能。

    这个项目总体的思路就是这样的,接下来我们一步一步的去说明

    #城市和天气信息获取模块

    1、获取城市信息

    数据链接http://guolin.tech/api/china,访问返回的是JSON数据类型的省份信息,需要返回城市时只需要在本链接后加上“/对应省份id”即可获取到相应的城市信息,县市信息也是一样的,原链接加上“/对应省份id/对应城市id”即可。
    大家点击网址可以得到这样的响应:
    这里写图片描述
    这里得到的是一个JSON数据,以下是对它的解析代码:

    JSONArray jsonArray = new JSONArray(response);
                    for (int i = 0; i < jsonArray.length(); ++i) {
                        JSONObject jsonObject = jsonArray.getJSONObject(i);
                        Province province = new Province();
                        province.setProvinceCode(jsonObject.getInt("id"));
                        province.setProvinceName(jsonObject.getString("name"));
                        province.save();
                    }
            这里的后几行代码是我使用LItepal存储数据到数据库的操作,response变量就是访问网址得到的原JSON数据。
            网上关于JSON的解析方法很多,这边不再说得过多。
            这边还需要说明的是怎么去网上获取JSON数据。
            首先要说明的是网络操作是不能在UI线程里进行的,否则会程序崩溃。所以这里必须用的异步线程去处理网络加载的问题,并且在加载线程事使用一个进度条来给予用户交互。
            以下是网络加载的代码:
            ```java
            public static void sendRequestOkHttpForGet(final String adress,final MyCallBack myCallBack) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    HttpURLConnection connection = null;
                    try {
                        URL url = new URL(adress);
                        connection = (HttpURLConnection) url.openConnection();
                        connection.setRequestMethod("GET");
                        connection.setConnectTimeout(8000);
                        connection.setReadTimeout(8000);
                        InputStream in = connection.getInputStream();
                        String response = convertStreamToString(in);
                        //回调接口函数,让主线程处理
                        //成功
                        myCallBack.onResponse(response);
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                        //失败
                        myCallBack.onFailure(e);
                    } catch (ProtocolException e) {
                        e.printStackTrace();
                        myCallBack.onFailure(e);
                    } catch (IOException e) {
                        e.printStackTrace();
                        myCallBack.onFailure(e);
                    } finally {
                        if (null != connection) {
                            connection.disconnect();
                        }
                    }
                }
            }).start();
       其中第一个参数是要访问的网络的地址,第二个参数是一个回调接口。
            public interface MyCallBack {
    
            void onFailure(IOException e);
    
            void onResponse(String response) throws IOException;
    }
    
    	第一个参数没有什么好说的(就是我们刚刚使用的 ),第二个参数是异步线程经常会用到的一个和主线程交互的手段。在调用函数时传入一个回调接口的指针,当异步线程完成相应的耗时操作之后,再使用该指针调用回调函数即可实现异步线程与主线程的交互了。
    	城市列表的信息的获取到这里就算结束了。
    

    2、获取天气信息

    获取天气信息的网络操作是和获取城市信息的操作是一样的,使用上面那个网络异步函数即可,如果觉得不好,也可以使用网络开源项目包装的网络访问接口,比如说OKHttp。不同的是天气信息的JSON数据要比城市信息的JSON数据复杂得多。
    这里提供连接给大家感受一下深圳天气
    这里写图片描述
    这里是用Chrome的JSON-handle解析之后的结果。可以看到还是比较复杂的。所以这里我们采用GSON方式来解析JSON,方便我们后面对数据的操作。
    GSON方式是把JSON数据解析成相应的对象的一种方式,主要步骤如下:
    1、根据JSON数据建立不同的类,JSON数据的每一个结点对应一个类,并且根据不同的结点的复杂程度选择是否还要使用内部类。
    2、@SerializedName(“JSON中的结点名”)需要转换成的节点名;
    使用关键字把一些JSON数据中意义晦涩的名词转换成类中名字可以见名知意的属性。
    3、JSON数据转换成对象实例

    JSONObject jsonObject = new JSONObject(response);
                    JSONArray jsonArray = jsonObject.getJSONArray("HeWeather5");
                    String weateherContent = jsonArray.getJSONObject(0).toString();
                    return new Gson().fromJson(weateherContent, 类名.class);
    

    最后,我们把得到的对象的数据对应的添加到要显示的活动的布局当中就可以了。

    #城市和天气信息显示模块

    3、天气信息的显示

    这里相对麻烦一点,因为天气信息的显示中我们做了比较多的功能

    获取背景图片和图片的更新

    这里我使用的是必应主页提供的背景图片作为天气信息显示的背景图片http://guolin.tech/api/bing_pic这个链接是获取必应每日背景图片下载链接的,可以通过该链接获取图片下载地址,然后再去下载。
    由于下载图片是耗时的网络操作,所以我们这里需要使用一个异步线程去下载图片,然后在下载好之后再通知UI线程去加载。
    具体代码:

    		public void updateBingPic() {
            String requestBingPic = "http://guolin.tech/api/bing_pic";
            OkHttp.sendRequestOkHttpForGet(requestBingPic, new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    e.printStackTrace();
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    String bingPic = response.body().string();
                    SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit();
                    editor.putString("bing_pic", bingPic);
                    editor.apply();
                }
            });
        }
    

    这个是获取图片下载地址的代码,变量bingPic的内容就是下载链接
    if (bingPic != null) {
    Glide.with(WeatherActivity.this).load(bingPic).into(bingPicIv);
    }
    当它不为空时,我们使用Glide去下载并加载图片到天气显示背景。Glide 是 Google 员工的开源项目, Google I/O 上被推荐使用Glide具有获取、解码和展示视频剧照、图片、动画等功能,它还有灵活的API,这些API使开发者能够将Glide应用在几乎任何网络协议栈里。创建Glide的主要目的有两个,一个是实现平滑的图片列表滚动效果,另一个是支持远程图片的获取、大小调整和展示。

    天气显示信息左右活动切换已选择要显示的城市(ViewPager)

    ViewPager是android扩展包v4包中的类,主要功能是实现view页面的左右切换。在本项目中,就是一个view包含一个城市的天气信息,然后view又加入到ViewPager中。
    这里说一下ViewPager的使用步骤,ViewPager的实现与ListView有很多相似之处,主要步骤如下:
    1、创建或设置数据源。
    2、根据数据源创建或配置好相应的适配器。
    3、在布局文件中加入ViewPager控件,并在程序给控件设置步骤2中的适配器。
    4、给控件添加监听器。
    PS:其实Android中很多包含多View的控件都是通过以上步骤实现的,很相似,只要我们认真的掌握了其中的一种,那么别的也就很容易去上手了。

    下拉刷新功能

    本项目中的下拉刷新功能是使用SwipeRefreshLayout控件实现的,实现的步骤很简单:
    1、在布局文件中实现下拉刷新功能的地方添加android.support.v4.widget.SwipeRefreshLayout控件,这里推荐使用v4包,因为能够支持低版本的Android手机。
    2、在程序中定义并设置相应属性(样式等等)和监听器。
    3、设置事件的相应响应和启动下拉刷新和结束下拉刷新。

    通过点击右上角的编辑按钮进入城市管理功能

    这里的实现就很基础了,简单讲一下步骤:
    1、在布局文件定义按钮
    2、在程序中找到按钮并设置监听器
    3、在响应事件中做进入城市功能活动的逻辑

    导航组件功能

    本项目的导航栏功能是用Selector实现,Selector主要是用来改变各种view控件的默认背景的。实现步骤如下:
    1、xml文件定义
    ?xml version=“1.0” encoding=“utf-8” ?>
    selector xmlns:android=“http://schemas.android.com/apk/res/android”>
    !-- 默认时的背景图片–>
    item android:drawable="@drawable/pic1" />
    !-- 没有焦点时的背景图片 -->
    item android:state_window_focused=“false”
    android:drawable="@drawable/pic1" />
    !-- 非触摸模式下获得焦点并单击时的背景图片 -->
    item android:state_focused=“true” android:state_pressed=“true” android:drawable= “@drawable/pic2” />
    !-- 触摸模式下单击时的背景图片–>
    item android:state_focused=“false” android:state_pressed=“true” android:drawable="@drawable/pic3" />
    !–选中时的图片背景–>
    item android:state_selected=“true” android:drawable="@drawable/pic4" />
    !–获得焦点时的图片背景–>
    item android:state_focused=“true” android:drawable="@drawable/pic5" />
    /selector>

    2、使用
    LinearLayout layout = (LinearLayout)findViewById(R.id.vp_guide_layout);
    LinearLayout.LayoutParams mParams = new LinearLayout.LayoutParams(20, 20);
    mParams.setMargins(0, 0, 0, 0);//设置小圆点左右之间的间隔

        guideShapeViewArrayList.clear();
        layout.removeAllViews();
        ImageView imageView = new ImageView(this);
        imageView.setLayoutParams(mParams);
        imageView.setImageResource(R.drawable.guide_shape_select);
    

    小时天气小时超出屏幕宽度时的当前页面左右滑动(RecycleListView)

    RecycleListView是Android官方出品的一个可以代替甚至超越ListView的东西。RecycleListView的实现比不优化的ListView麻烦一些,但是功能上比ListView要更强大,因为他的显示不仅可以竖屏,还可以横屏。
    实现步骤:
    1、准备数据源
    2、根据数据源设置适配器
    static class ViewHolder extends RecyclerView.ViewHolder {
    TextView hourlyTimeTV;
    ImageView hourlyWeatherImageV;
    TextView hourlyTemperatureTV;

        public ViewHolder(View view){
            super(view);
            hourlyTimeTV = (TextView) view.findViewById(R.id.hourly_time_tv);
            hourlyWeatherImageV = (ImageView) view.findViewById(R.id.hourly_weather_iv);
            hourlyTemperatureTV = (TextView) view.findViewById(R.id.hourly_temperature_tv);
        }
    }
    
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.hourly_forecast_item, parent, false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }
    
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        HourlyWeather hourlyWeather = hourlyWeatherList.get(position);
        holder.hourlyTimeTV.setText(hourlyWeather.hourlyTime + "时");
        holder.hourlyWeatherImageV.setImageBitmap(hourlyWeather.hourlyImageBit);
        holder.hourlyTemperatureTV.setText(hourlyWeather.hourlyTemperature + "º");
    }
    
    @Override
    public int getItemCount() {
        return hourlyWeatherList.size();
    }
    

    要实现RecyclerView.Adapter主要是要实现三个函数
    onCreateViewHolder()
    onBindViewHolder()
    getItemCount()

    3、在布局文件定义RecycleView控件,并在代码中为控件设置以上适配器。
    4、选择是否要设置监听器。

    有没有发现和ListView,ViewPager的实现步骤很相似呢。

    布局圆角功能

    布局圆角主要是为了让布局中的控件看起来美观一些。
    实现很简单
    1、在drawable中定义xml文件

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <solid android:color="#8000" />
        <corners
            <android:topLeftRadius="10dp"
            <android:topRightRadius="10dp"
            <android:bottomRightRadius="10dp"
            <android:bottomLeftRadius="10dp"/>
    </shape>
    

    2、在需要引入圆角的布局文件中引入本配置作为背景

    android:background="@drawable/corners_bg"
    

    4、城市信息的显示

    这里相对麻烦一点,因为城市信息的显示中我们做了比较多的功能,下面线总体介绍项目使用到的功能模块,然后再一一说明:
    1、活动切换按钮,这里就不再重复说明了。
    2、添加城市。
    3、可拖拽的ListView(DragListView)的城市信息实现
    本模块我们主要讲解DragListview的实现:
    这里写图片描述
    这里实现的主要功能有:删除城市、城市排序切换。
    实现步骤:
    1、准备数据源
    2、设置适配器

    public class CountiesAdapter extends BaseAdapter {
            private Context context;
            //适配器的数据源 selectedCityList
            private List<SelectedCounty> items;
    
            public CountiesAdapter(Context context,List<SelectedCounty> selectedCityList){
                this.context = context;
                this.items = selectedCityList;
                LogUtil.d(TAG, "CountiesAdapter: selectedCityList size:" + selectedCityList.size());
                LogUtil.d(TAG, "CountiesAdapter: selectedCityList items size:" + items.size());
            }
    
            @Override
            public int getCount() {
                return items.size();
            }
    
            @Override
            public Object getItem(int arg0) {
                return items.get(arg0);
            }
    
            @Override
            public long getItemId(int arg0) {
                return arg0;
            }
    
            public void remove(int arg0) {//删除指定位置的item
                items.remove(arg0);
                this.notifyDataSetChanged();//不要忘记更改适配器对象的数据源
            }
    
            public void insert(SelectedCounty item, int arg0) {
                items.add(arg0, item);
                this.notifyDataSetChanged();
            }
    
            public void change(List<SelectedCounty> selectedCityList) {
                items = selectedCityList;
                this.notifyDataSetChanged();
            }
    
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                SelectedCounty item = (SelectedCounty)getItem(position);
                ViewHolder viewHolder;
                if(null == convertView){
                    viewHolder = new ViewHolder();
                    convertView = LayoutInflater.from(context).inflate(R.layout.drag_listview_item, null);
                    viewHolder.dragMoveIv = (ImageView) convertView.findViewById(R.id.drag_move_iv);
                    viewHolder.dragCountyNameTv = (TextView) convertView.findViewById(R.id.drag_county_name_tv);
                    viewHolder.drag_click_remove = (ImageView) convertView.findViewById(R.id.drag_click_remove);
                    moveImageViewList.add(viewHolder.dragMoveIv);
                    deleteImageViewList.add(viewHolder.drag_click_remove);
                    convertView.setTag(viewHolder);
                }else{
                    viewHolder = (ViewHolder) convertView.getTag();
                }
                //是否点击了edit按钮,
                if (isEditClick) {
                    if (!WeatherActivity.isLocationCountyRemove && null != WeatherActivity.locationCountyWeatherId && 0 == position ) {
                        viewHolder.drag_click_remove.setVisibility(View.GONE);
                    } else {
                        viewHolder.dragMoveIv.setVisibility(View.VISIBLE);
                        viewHolder.drag_click_remove.setVisibility(View.VISIBLE);
                    }
                }else {
                    viewHolder.dragMoveIv.setVisibility(View.GONE);
                    viewHolder.drag_click_remove.setVisibility(View.GONE);
                }
                viewHolder.dragCountyNameTv.setText(item.getCountyName());
                return convertView;
            }
    
            class ViewHolder {
                ImageView dragMoveIv;
                TextView dragCountyNameTv;
                ImageView drag_click_remove;
            }
        }
    

    别的地方和ListView是一样的,不同的是多了一个remove和insert函数

    public void remove(int arg0) {//删除指定位置的item
                items.remove(arg0);
                this.notifyDataSetChanged();//不要忘记更改适配器对象的数据源
            }
    
            public void insert(SelectedCounty item, int arg0) {
                items.add(arg0, item);
                this.notifyDataSetChanged();
            }
    

    3、控件绑定适配器
    这里也有区别:首先要定义两个函数:

    //监听器在手机拖动停下的时候触发
        private DragSortListView.DropListener onDrop =
                new DragSortListView.DropListener() {
                    @Override
                    public void drop(int from, int to) {//from to 分别表示 被拖动控件原位置 和目标位置
                        //如果定位城市存在,则去除定位城市的操作
                        if (!WeatherActivity.isLocationCountyRemove && null != WeatherActivity.locationCountyWeatherId) {
                            if (0 == from || 0 == to) {
                                return;
                            }
                        }
                        if (from != to) {
                            SelectedCounty item = (SelectedCounty)countiesAdapter.getItem(from);//得到listview的适配器
                            countiesAdapter.remove(from);//在适配器中”原位置“的数据。
                            countiesAdapter.insert(item, to);//在目标位置中插入被拖动的控件。
                            isSwapCounty = true;
                        }
                    }
                };
        //删除监听器,点击左边差号就触发。删除item操作。
        private DragSortListView.RemoveListener onRemove =
                new DragSortListView.RemoveListener() {
                    @Override
                    public void remove(int which) {
                        delCountyId.add(selectedCityList.get(which).getId());
                        delCountyIndex.add(which);
                        countiesAdapter.remove(which);
                        Log.d(TAG, "onClick: remove position:" + which);
                    }
                };
    

    然后绑定适配器时这两个函数也一起绑定

    //水平滑动显示
            hourlyRecycler = (RecyclerView) currentView.findViewById(R.id.hourly_recycler);
            layoutManager = new LinearLayoutManager(currentView.getContext());
            layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
            hourlyRecycler.setLayoutManager(layoutManager);
            hourlyWeatherAdapter = new HourlyWeatherAdapter(hourlyWeatherList);
            hourlyRecycler.setAdapter(hourlyWeatherAdapter);
    

    有问题或要 者建议的可以在评论留言,需要源码的也可以留言,我看到了都会及时回复的。
    未完待续。。。。。。


    展开全文
  • APP-细说APP网络深度优化与网络安全

    千次阅读 2020-01-20 16:50:03
    下载目前也是APP的大头了,一般APP都是请求下载的数据比较多;我们在加载资源的时候可以选择加载压缩资源,比如微信的朋友圈,小图的时候我们可以加载缩略图,要是点击查看大图的时候才考虑加载大图原图; 交互...
  • 1.在iosAPP下载安装时,如果出现此时无法下载安装APP的字样时,可能是苹果系统进行了支持更新,并需要我们确认条约。至于如何判断是否是苹果系统进行了更改,只需要我们进入开发者账号,进入我的账户(Account)如...
  • 从Safari跳到APP 跳转 既然要想跳到你指定的APP,那么就需要在你的APP中定义一个特殊的标示,也就是一个URL协议。 定义URL协议的如下图TARGETS ——> info ——>URL Types——>添加一个URL协议 Snip...
  • APP Inventor 基于网络微服务器的即时通信APP 最近,老师要求我们用APP Inventor做一个APP,简单地做了一个即时通信的APP(超低配版的QQ,逃~),连肝了几个晚 上,目前已是比较稳定的版本了,虽然还有很多小问题...
  • uni-app至api列表汇总

    千次阅读 2020-01-01 21:58:45
    API 列表 网络 发起请求 API 说明 uni.request 发起网络请求 上传、下载 API 说明 uni.uploadFile 上传文件 uni.downloadFile 下载文件 WebSocket API 说明 uni.connectSocket ...
  • 简单做一个APP检测更新的小工具,有点粗糙。不能断点续传,只用为个人觉得没有必要,自己可根据大家的想法添加更多的功能,这里只是为了想我一样的初学者和比较简约的人所提供。 效果如下: 基本思路先理一理,以...
  • 下面是《Android Studio开发实战 从零基础到App上线(第2版)》一书用到的工具和代码资源: 1、本书使用的Android Studio版本为3.2,最新的安装包可前往Android官网页面下载。 2、本书使用的Android NDK版本为r17,...
  • Android小项目——新闻APP

    万次阅读 多人点赞 2019-02-26 19:25:42
    前言: 在公司学习了一段时间Android知识,决定做一个小项目,目的是学会运用所学的基础知识,在这里记录一下开发历程,大家可以把它看成一款入门级练手的 Demo 应用吧~ ...横向滑动列表显示新闻类别——TabLayout...
  • 2/ 找墙外的朋友代购,具体做法就是在app下载界面有gift app选项,让他付钱以后发送给你的apple id。你收到邮件点击下载链接就可以直接下载安装了。 step 5: 安装软件啦;实在注册不成功的,问我吧。 加入...
  • 客户端上显示csdn上的各类别下的的文章列表 (制作csdn app 三) 继续完善我们的app。 今天的目标是: 1、对文章列表的下拉刷新,上拉加载。 2、没有网络的情况下,依然可以看到缓存在手机上的文章列表,以及上拉...
  • 下面是一些国外比较有人气的APP网站和APP论坛 http://www.ipadown.com/ http://topapp.net/ http://www.appolicious.com/ http://mobile.butterscotch.com/Mobile/IPhone/ http://wwdc.slidetoplay
  • 基于K-均值的app列表聚类分析

    千次阅读 2019-02-27 20:55:14
    目录 一、工程实现 ...其实,除了以上比较明显维度考量,用户app列表也隐藏着用户丰富个性化特征。不同用户群体对app偏爱也不尽相同,例如游戏宅可能更多选择各种类型游戏app,漫画控喜欢二次元和动漫ap...
  • Android App优化之网络优化

    千次阅读 2016-11-10 08:49:09
    Android App优化之性能分析工具Android App优化之提升你的App启动速度之理论基础Android App优化之提升你的App启动速度之实例挑战Android App优化之Layout怎么摆Android App优化之ANR详解Android App优化之消除卡顿...
  • 今天将在Android 使用Fragment,ViewPagerIndicator 制作csdn app主要框架和抓取csdn上的各类别的文章 (制作csdn app 二)这两篇的基础之上,继续完善我们的项目。
  • 下面是《Android Studio开发实战 从零基础到App上线》(第一版)一书用到的工具和代码资源: 1、本书使用的Android Studio版本为2.2.3,因为Android官网现在不提供该版本的下载,所以博主把该版本的64位安装包上传到...
  • 使用AppStore下载应用软件时发现下载过程中提示失败了,让我们使用已购页面再试一次了,出现这种问题主要是DNS问题 App Store 是放在美国了,所以我们多半是网络问题,我们可以尝试修改DNS来解决 添加一个 ...
  • HBuilder开发App教程07-列表

    千次阅读 2015-08-04 20:54:18
    如果看过上一节你就会明白“首页”是由只有头部的index页面和列表list页面组成的, 本节涉及到: 1.几种页面的打开方式 2.websql的应用 3.页面内容的加载 4.页面实现 几种页面打开方式 如果看过以前的...
  • APP弱网络条件下,体验优化之道

    万次阅读 2016-07-16 16:47:21
    APP弱网络条件下,体验优化之道 最近跟朋友聊天刚好聊到这一块,他们是在做电商业务,商品图片及其多,API接口请求频率也高。然而,他们在移动2/3G的网络环境下,APP经常会出现Loading很久的情况,这里我把我们所...
  • 介绍  项目中需要引入腾讯手机管家做安全防护功能,因此需要检测用户手机是否安装了腾讯手机管家,以此来引导用户下载手机管家,这个功能其实也很简单。首先需要拿到手机管家的包名,不用多说,包名是APP的唯一...
  • 鸿蒙系统 Hi3861 实现手机APP功能

    千次阅读 2020-10-09 22:48:59
    本节主要讲如何去实现Hi3861 APP功能。需要说明的是,本人实现这个功能主要是用于学习,所涉及知识包括Hi3861的WiFi操作,AP模式、STA模式、按键功能、网络编程、JSON数据格式、手机APP开发。 所有源码,还有...
  • 如何下载app的历史版本呢?下面我就以“抖音极速版”为例,教大家如何从【豌豆荚】下载“抖音极速版app”的历史版本。 豌豆荚官网:https://www.wandoujia.com 教程步骤: 1、打开上述豌豆荚官网,在搜索框输入...
  • APP弱网络测试--基于WANem

    千次阅读 2017-02-25 16:40:03
    App的弱网络测试,iOS可直接借助手机自带的网络状况模拟工具:设置->开发者(手机需调试过才有开发者选项)->NETWORK LINK CONDITIONER,可设置出/入的带宽、延迟、丢包,DNS延迟等参数,模拟所需的弱网。...
  • APPInventor教程】 网络微数据库tinywebdb的增删查改   教程中使用的网络微数据库地址是: http://tinywebdb.appinventor.space/webdb-share-everyone 其中,share是用户名,everyone是密钥,支持使用API...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 179,188
精华内容 71,675
关键字:

列表网app下载