Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android6.0权限管理到RxPermissions源码分析

$
0
0

在给应用适配6.0版本的时候,我们就需要运行时权限管理。在6.0开始有一套新的运行机制管理用于更友好的保护用户的隐私安全,一般涉及用户隐私的需要实时来提示用户通过允许和拒绝来授权。

如何申请一个权限呢?
1.在AndroidManifest中把我们需要的权限添加,像我那天忘记加了一点就闪退,呼!奔溃了好久。
2.检查权限

if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {
        // 已经申请同意了就直接处理逻辑代码
}else{
    // 走申请流程
}

3.申请这个权限

ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);

4.在Activity中处理这个申请回调

@Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // 如果权限被取消了,这个数组会是空的
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // 权限被允许了  接着处理下一步逻辑

            } else {

                // 权限被拒绝   可以做些友好的引导处理
            }
            return;
        }
    }
}

OK、一整段的流程下来之后我瞬间感觉这么写反正我不喜欢,跟onActivityResult的回调似的,太不喜欢了。然后就在github上搜索一下,发现了RxPermissions这么一个开源库而且是基于RxJava瞬间就被吸引了,接下来我们就聊聊这个库。

RxPermissions简单用法介绍

// 必须在类似onCreate的方法中进行初始化
RxPermissions.getInstance(this)
    .request(Manifest.permission.CAMERA)
    .subscribe(granted -> {
        if (granted) { // M版本之前都是返回true
           // 权限允许
        } else {
           // 权限被拒绝
        }
    });

这是一段简单用法并且基于RxJava以及在Retrolambda条件下看起来的模样,当然这里是你需要的权限每一个都同意之后才回调到回来,当然你也可以一个一个处理回调。

RxPermissions.getInstance(this)
    .requestEach(Manifest.permission.CAMERA,
             Manifest.permission.READ_PHONE_STATE)
    .subscribe(permission -> { // 将会发射多个权限请求
        if (permission.granted) {
           // 权限允许
        }
    });

更多用法可以查看https://github.com/tbruyelle/RxPermissions
接下来点进去查看源码,看他是如何封装权限管理的。

    static RxPermissions sSingleton;

    public static RxPermissions getInstance(Context ctx) {
        if (sSingleton == null) {
            sSingleton = new RxPermissions(ctx.getApplicationContext());
        }
        return sSingleton;
    }

    private Context mCtx;

没错RxPermissions是采用单例模式,传入ctx.getApplicationContext()是因为权限允许与否是回调回来,存在可能得Context泄露所以传入的是ApplicationContext对象,接下我们就连着调用request方法。

/**
     * 立即请求权限,调用必须在应用程序的初始化阶段
     */
    public Observable<Boolean> request(final String... permissions) {
        return Observable.just(null).compose(ensure(permissions));
    }

我们可以看到是走入ensure这个方法,permissions这个参数就是我们传入的权限数组,compose操作法需要一个Observable.Transformer来将我们前面的Observable转成我们想要的Observable类似于flatmap操作符,我们可以看到返回的结果是Observable.Transformer< Object, Boolean >也就是把Observable.just(null)转成我们想要的Observable< Boolean >,所以我们进入到ensure这个方法看是如何转换的。

public Observable.Transformer<Object, Boolean> ensure(final String... permissions) {
        return new Observable.Transformer<Object, Boolean>() {
            @Override
            public Observable<Boolean> call(Observable<Object> o) {
                return request(o, permissions)
                        // 将 Observable<Permission> 转换成 Observable<Boolean>
                        .buffer(permissions.length)
                        .flatMap(new Func1<List<Permission>, Observable<Boolean>>() {
                            @Override
                            public Observable<Boolean> call(List<Permission> permissions) {
                                if (permissions.isEmpty()) {
                                    // Occurs during orientation change, when the subject receives onComplete.
                                    // In that case we don't want to propagate that empty list to the
                                    // subscriber, only the onComplete.
                                    return Observable.empty();
                                }
                                // 所有的权限被允许的时候返回true
                                for (Permission p : permissions) {
                                    if (!p.granted) {
                                        return Observable.just(false);
                                    }
                                }
                                return Observable.just(true);
                            }
                        });
            }
        };
    }

根据传入的参数一开始是进入request这个方法中去,二话不说直接跟着进去看看情况。

    private Observable<Permission> request(final Observable<?> trigger,
                                           final String... permissions) {
        if (permissions == null || permissions.length == 0) {
            throw new IllegalArgumentException("RxPermissions.request/requestEach requires at least one input permission");
        }
        return oneOf(trigger, pending(permissions))
                .flatMap(new Func1<Object, Observable<Permission>>() {
                    @Override
                    public Observable<Permission> call(Object o) {
                        return request_(permissions);
                    }
                });
    }

继续二话不说跳入oneOf这个方法中去,可是我们发现参数中pending(permissions)有这个一个方法,所以我们先进去看这个方法。

    private Observable<?> pending(final String... permissions) {
        for (String p : permissions) {
            if (!mSubjects.containsKey(p)) {
                return Observable.empty();
            }
        }
        return Observable.just(null);
    }

这时候又迷糊了mSubjects这里面是什么东西呢?

    // Contains all the current permission requests.
    // Once granted or denied, they are removed from it.
    private Map<String, PublishSubject<Permission>> mSubjects = new HashMap<>();

从声明的注释里面来看,存放的就是当前的权限请求数组,允许或者拒绝都将从里面移除。这里需要解释一下Observable.empty()表示的是:不发射任何数据并正常结束的Observable。所以pending里面要么返回empty要么返回null的Observable,根据后面的代码可以看到只有我们请求一个不存在的权限的时候会抛出错误从而没有在mSubjects中remove,然后我们再进入oneOf:

    private Observable<?> oneOf(Observable<?> trigger, Observable<?> pending) {
        if (trigger == null) {
            return Observable.just(null);
        }
        return Observable.merge(trigger, pending);
    }

所以这边会返回一个Observable.just(null)、或者返回Observable.merge(trigger, pending)的一个合并的Observable然后一个一个的发射出去。然后进入最重要的request_方法中去:

    private Observable<Permission> request_(final String... permissions) {

        List<Observable<Permission>> list = new ArrayList<>(permissions.length);
        List<String> unrequestedPermissions = new ArrayList<>();

        // In case of multiple permissions, we create a observable for each of them.
        // At the end, the observables are combined to have a unique response.
        for (String permission : permissions) {
            log("Requesting permission " + permission);
            if (isGranted(permission)) {
                // Already granted, or not Android M
                // Return a granted Permission object.
                list.add(Observable.just(new Permission(permission, true)));
                continue;
            }

            if (isRevoked(permission)) {
                // Revoked by a policy, return a denied Permission object.
                list.add(Observable.just(new Permission(permission, false)));
                continue;
            }

            PublishSubject<Permission> subject = mSubjects.get(permission);
            // Create a new subject if not exists
            if (subject == null) {
                unrequestedPermissions.add(permission);
                subject = PublishSubject.create();
                mSubjects.put(permission, subject);
            }

            list.add(subject);
        }

        if (!unrequestedPermissions.isEmpty()) {
            startShadowActivity(unrequestedPermissions
                    .toArray(new String[unrequestedPermissions.size()]));
        }
        return Observable.concat(Observable.from(list));
    }

for循环我们要请求的权限数组,通过isGranted_先判断是否已经权限被允许了。

    private boolean isGranted_(String permission) {
        return mCtx.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
    }

通过isRevoked_方法来确认是否权限被取消了

    private boolean isRevoked_(String permission) {
        return mCtx.getPackageManager().isPermissionRevokedByPolicy(permission, mCtx.getPackageName());
    }

然后会判断是否已经在mSubjects权限列表中了,如果没有加入unrequestedPermissions这个还没被请求的列表中然后去启动ShadowActivity来进行权限授权。

        if (!unrequestedPermissions.isEmpty()) {
            startShadowActivity(unrequestedPermissions
                    .toArray(new String[unrequestedPermissions.size()]));
        }

在Activity中处理Intent请求拿出需要请求的权限

    private void handleIntent(Intent intent) {
        String[] permissions = intent.getStringArrayExtra("permissions");
        requestPermissions(permissions, 42);
    }

然后回调权限请求结果到onRequestPermissionsResult中

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        RxPermissions.getInstance(this).onRequestPermissionsResult(requestCode, permissions, grantResults);
        finish();
    }

然后执行到onRequestPermissionsResult(引用RxPermissions是一个单列所以对象是同一个的)把授权的结果返回出去。

    void onRequestPermissionsResult(int requestCode,
                                    String permissions[], int[] grantResults) {
        for (int i = 0, size = permissions.length; i < size; i++) {
            log("onRequestPermissionsResult  " + permissions[i]);
            // Find the corresponding subject
            PublishSubject<Permission> subject = mSubjects.get(permissions[i]);
            if (subject == null) {
                // No subject found
                throw new IllegalStateException("RxPermissions.onRequestPermissionsResult invoked but didn't find the corresponding permission request.");
            }
            mSubjects.remove(permissions[i]);
            boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
            subject.onNext(new Permission(permissions[i], granted));
            subject.onCompleted();
        }
    }

是不是觉得很奇怪这边发射了数据出去然后我们在ensure这个方法通过subscribe也发射数据,按我的理解呢onRequestPermissionsResult这里面的发射数据我们并没有去做接收,更多的是确认当前这次的权限请求是否被允许了。然后无乱结果与否我们都返回到request_这个方法中去,并且通过concat把结果返回给ensure。

return Observable.concat(Observable.from(list));

concat主要作用是将多个Observable连续发射出去。

buffer(permissions.length)

我们看到ensure中用到这么一个操作符,这个操作符类似一个缓存区也就是说一下子把缓冲区中的数据一下子全部发射出去而不是一个一个发射,而request跟requestEach的区别就是这与这个buffer操作。

flatMap(new Func1<List<Permission>, Observable<Boolean>>() {
                            @Override
                            public Observable<Boolean> call(List<Permission> permissions) {
                                if (permissions.isEmpty()) {
                                    // Occurs during orientation change, when the subject receives onComplete.
                                    // In that case we don't want to propagate that empty list to the
                                    // subscriber, only the onComplete.
                                    return Observable.empty();
                                }
                                // Return true if all permissions are granted.
                                for (Permission p : permissions) {
                                    if (!p.granted) {
                                        return Observable.just(false);
                                    }
                                }
                                return Observable.just(true);
                            }
                        });

然后通过flatMap来判断任何一个权限被拒绝了就会返回false全部通过才返回true。
上面的是对权限请求必须每一个都同意后才能通过,如果我们使用requestEach来权限请求的话就不同了只要有允许就发射出去。

    public Observable.Transformer<Object, Permission> ensureEach(final String... permissions) {
        return new Observable.Transformer<Object, Permission>() {
            @Override
            public Observable<Permission> call(Observable<Object> o) {
                return request(o, permissions);
            }
        };
    }

因为requestEach返回的用concat连接起来的Observable所以会依次发射权限数据出去。
大概的RxPermissons的源码就是这么个流程,当然里面涉及了一些RxJava的操作符而且该库也支持RxJava2.0了,这样封装在项目中使用就更加方便了。

RxPermissions项目地址:https://github.com/tbruyelle/RxPermissions
参考文章:http://blog.csdn.net/lmj623565791/article/details/50709663

作者:Neacy_Zz 发表于2016/10/5 0:16:24 原文链接
阅读:95 评论:0 查看评论

Android开发艺术探索读书笔记 第一章

$
0
0

1. Activity生命周期全面分析

  1. 生命周期
    • onCreate:表示Activity正在被创建
    • onRestart:表示Activity正在重新启动
    • onStart:表示Activity正在启动,Activity已经可见,但是还没有出现在前台
    • onResume:表示Activity已经可见,并且出现在前台开始活动
    • onPause:表示Activity正在停止,可以存储数据、停止动画等,不能太耗时
    • onStop:表示Activity即将停止,可以做一些稍微重量级的回收工作,不能太耗时
    • onDestroy:表示Activity即将被销毁,可以做一些回收工作和最终的资源释放
  2. Activity生命周期切换过程
    1. 针对一个特定的Activity,第一次启动,回调如下:onCreate-onStart-onResume
    2. 当用户打开新的Activity或者切换到桌面的时候,回调如下:onPause-onStop。如果新的Activity采用了透明主题,那么当前Activity不会回调onStop。
    3. 当用户再次回到原Activity,回调如下:onRestart-onStart-onResume。
    4. 当用户按back回退键时,回调如下:onPause-onStop-onDestroy
    5. onStart和onStop是从Activity是否可见这个角度来回调的,而onResume和onPause是从Activity是否位于前台这个角度来回调的。
    6. 从Activity A跳转到Activity B,Activity A的onPause先执行,Activity B的onResume在执行。
    7. onActivityResult() 和onResume()的调用顺序问题,回到调用activity时会马上执行onActivityResult方法,然后才是onResume()方法。
  3. 异常情况下的生命周期分析

    1. 资源相关的系统配置发生改变导致Activity被杀死并重新创建
      在Activity异常终止的情况下,Activity被重新创建后,系统会调用 onSaveInstanceState ,并且把Activity销毁时onSaveInstanceState 方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState和onCreate()方法,我们可以通过上述两个方法判断Activity是否被重建了,如果重建了,我们可以取出数据并恢复,从时序上来讲,onRestoreInstanceState的调用时机在onStart之后。
      在onSaveInstanceState 和onRestoreInstanceState方法中,系统自动为我们做了一定的恢复工作,系统会默认为我们保存当前Activity的视图结构,并且在Activity重启后为我们恢复这些数据,比如文本框输入的数据、ListView滚动的位置等。
      onRestoreInstanceState和onCreate()方法的区别是,onRestoreInstanceState一旦被调用,其参数一定是有值的,官方建议使用这种方法去恢复数据,onCreate()`不行。
    2. 资源内存不足导致低优先级的Activity被杀死
      Activity的优先级从高到低:
      1. 前台Activity — 正在和用户交互的Activity,优先级最高
      2. 可见但非前台的Activity — 比如Activity中弹出一个对话框,导致Activity可见但是位于后台无法和用户直接交互。
      3. 后台Activity — 已经被暂停的Activity,比如执行了onStop,优先级最低。

        当系统内存不足的时候,系统会按照上述优先级杀死Activity所在的进程,并通过onSaveInstanceStateonRestoreInstanceState方法来存储和恢复数据。如果一个进程没有在4大组件执行,进程很快会被系统杀死。
        当系统配置发生改变后,Activity会被重新创建,我们可以给Activity指定configChanges属性,Activity不会被重新创建,没有调用onSaveInstanceStateonRestoreInstanceState方法,取而代之的是调用onConfigurationChanges方法,如下所示:
      android:configChanges="orientation | keyboardHidden
      主要有如下三个重要属性:
      local 设备的本地位置发生改变,一般指切换了系统语言
      orientation屏幕方向发生了改变,比如旋转了屏幕
      keyboardHidden键盘的可访问性发生了改变,比如用户调出键盘

补充:
横竖屏切换时候Activity的生命周期:
  不设置Activity的android:configChanges时,切屏会重新回掉各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
  设置Activity的android:configChanges=”orientation”时,切屏还是会调用各个生命周期,切换横竖屏只会执行一次
  设置Activity的android:configChanges=”orientation |keyboardHidden”时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法

2. Activity的启动模式

  1. standard:标准模式
      系统的默认模式,每次启动一个Activity都会重新创建一个新的实例,不管这个实例是否已经存在。被创建的生命周期符合典型情况下的生命周期,它的onCreate、onStart、onResume都会调用。在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity栈中。比如Activity A启动了Activity B(B是标准模式),那么B就会进入到A所在的栈中。使用场景大多数Activity。
  2. singleTop:栈顶复用模式
      如果新Activity已经位于任务栈的栈顶,那么次Activity不会被重新创建,同时它的onNewIntent方法会被回调,通过此方法我们可以取出当前请求的信息。这个Activity的onCreate、onStart不会被调用,因为它并没有发生改变。使用场景如新闻类或者阅读类App的内容页面。
  3. singleTask:栈内复用模式
      一种单实例模式,只要Activity在一个栈中存在,多次启动Activity都不会重新创建实例,系统会回调onNewIntent方法。当设置此模式后,比如Activity A会首先寻找是否存在A想要的任务栈,如果不存在就重新创建一个任务栈,如果存在,就看A是否在栈中有实例存在,如果有实例存在那么系统就会把A调到栈顶并调用onNewIntent方法,同时由于singleTask默认具有clearTop的效果,就会导致栈内所有在A上面的Activity全部出栈,如果不存在,就创建A的实例并把A压入栈中。使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
  4. singleInstance:单实例模式
      这是一种加强的singleTask模式,除了具有singleTask所有特性外,还具有此种模式的Activity A只能单独的位于一个新的任务栈,然后A单独在这个栈中,由于栈内复用的特性,后续的请求均不会创建新的Activity。使用场景如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。
  5. Activity的Flags
    1. 设置启动模式的两种方式
      通过AndroidMenifest为Activity指定启动模式
      android:launchMode="singleTask"
      Intent中设置标记为来为activity指定启动模式
      intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    2. 常用标记位
      FLAG_ACTIVITY_NEW_TASK
      为Activity指定singleTask启动模式
      FLAG_ACTIVITY_SINGLE_TOP
      为Activity指定singleTop启动模式
      FLAG_ACTIVITY_CLEAN_TOP
      具有此标记位的Activity,当它启动时,同一个任务栈中所有位于它上面的Activity都要出栈。
      FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
      不希望用户通过历史列表回到我们的Activity的时候这个标记比较有用,等同于在XML中指定Activity的属性android:excludFromRecents="true".

3. IntentFilter的匹配规则

为了匹配过滤列表,需要同时匹配过滤列表中的action、category、data信息,否则匹配失效。一个过滤列表中的action、category和data可以有多个。一个Intetn同时匹配action类别、category类别、data类别才算完全匹配,只有完全匹配才能启动目标Activity。一个Activity中可以有多个intent-filter,一个Intent只要匹配其中任何一组intent-filter即可成功启动对应的Activity。
1. action的匹配规则
  action是一个字符串,系统预定义了一些action,我们也可以自定义action,匹配规则是Intent中的action必须能够和过滤规则中的action匹配,一个过滤规则中可以有多个action,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功,Intent中的action存在且必须和过滤规则中的其中一个action相同。
2. category的匹配规则
  category是一个字符串,系统预定义了一些category,我们也可以在应用中定义自己的category。category和action的匹配规则不同,要求Intent中如果含有category,那么所有的category都必须和过滤规则中(即intent filter中设置的category)的其中一个相同。Intent也可以没有category,如果没有category的话,在startActivity或者startActivityForResult的时候会默认为Intent加上"android.intent.category.DEFAULT"
3. data的匹配规则
  如果过滤规则中添加了data,那么Intent中必须也要定义可匹配的data。data的语法如下所示:

 <data 
       android:scheme="string"
       android:host="string"
       android:port="string"
       android:path="string"
       android:pathPattern="string"
       android:pathPrefix="string"
       android:mimeType="string" />

  data由两部分组成,mineType和URI。mineType指媒体类型,比如image/jpeg、audio/mpeg4-generic和video/*等,可以表示图片、文本、视频等不同媒体格式,而URI包含的数比较多,一个URI结构如下所示

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

下面举个实际的例子大家就明白了:

content://com.example.project:200/folder/subfolder/etc

其中:
- Scheme:URI的模式,比如http、file、content等,如果URI中没有指定scheme,那么整个URI的其他参数无效,即URI也是无效的;
- Host:URI的主机名,如com.example.project,如果host未指定,那么整个URI中的其他参数无效,即URI也是无效的;
- Port:URI中的端口号,如200,只有URI中指定了scheme和host参数的时候port参数才有意义;
- Path、PathPattern和PathPrefix:这三个参数表示路径信息,如folder/subfolder/etc,其中path表示完整的路径信息;PathPattern也表示完整的路径信息,但是它里面可包含通符“*”,表示0个或多个任意字符;PathPrefix表示路径的前缀信息。
data匹配规则:

            <intent-filter>
                <data android:mimeType="image/*"/>
                ...
            </intent-filter>

这种规则制定了媒体类型为所有类型的图片,那么Intent中mimeType属性必须为”image/*”才能匹配,这种情况下没有指定URI,但是Intent中的URI部分的schema必须为content或者file才能匹配,我们需要在Intent中这样指定:
intent.setDataAndType(Uri.parse("file://abc"), "image/png")
如果要为Intent指定完整的data,必须要调用setDataAndType方法,不能先调用setData再调用setType,因为这两个方法彼此会清除对方的值。
怎么判断是否有Activity是否能够匹配我们的隐式Intent?
1. 采用PackageManagerresolveActivity方法或者Intent的resolveActivity方法,如果找不到就返回null。
2. PackageManager方法还提供了queryIntentActivities方法,这个方法和resolveActivity方法最大的不同是,它不是返回最佳匹配的Activity的信息,而是返回的是所有成功匹配的Activity信息。我们可以看一下方法的原型:

public abstract List<ResolveInfo> queryIntentActivities(Intent intent,@ResolveInfoFlags int flags);
 public abstract ResolveInfo resolveActivity(Intent intent, @ResolveInfoFlags int flags);

上述两个方法的第二个参数注意,我们要使用MATCH_DEFAULT_ONLY这个标记位,只能匹配intent-filter中声明了<category android:name="android.intent.category.DEFAULT"/>这个category的Activity。使用这个标记位最大的意义是上述两个方法不返回null,那么startActivity一定可以成功。如果不使用这个标记位,就可以吧intent-filter中category不含DEFAULT的那些Activity匹配出来,startActivity有可能失败,不含有DEFAULT这个category的Activity是无法接受隐式Intent的。
3. 在action和category中有一类比较重要,用来标注一个入口:

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

针对Service和BroadcastReceiver等组件,PackageManager同样提供了类似的方法去获取成功匹配的组件信息。
关于隐式Intent更多信息可以参考这两篇官方翻译的博文,也可以直接去看官方的文档:
Android Intent最全面的解析
史上最全的隐式Intent解析

作者:qq_30806949 发表于2016/10/5 0:54:17 原文链接
阅读:100 评论:0 查看评论

JAVA进阶之旅(二)——认识Class类,反射的概念,Constructor,Fiald,Method,反射Main方法,数组的反射和实践

$
0
0

JAVA进阶之旅(二)——认识Class类,反射的概念,Constructor,Fiald,Method,反射Main方法,数组的反射和实践


我们继续聊JAVA,这次比较有意思,那就是反射了

一.认识Class类

想要反射,你就必须要了解一个类——Class,我们知道,java程序中的各个java类都属于同一事物,我们通常用Classliability描述对吧,反射这个概念从JDK1.2就出来了,历史算是比较悠久了,这个Class可不是关键字哦,这个是一个类,他代表的是一类事物;

  • 我们归根结底就是拿到字节码对象

这里我们有三种方式是可以得到对应的实例对象(Class类型)

  • 1.类名.class
  • 2.对象.getClass
  • 3.Class.forName(“类名”);—常用

我们写一个小程序来过一遍

public class CalssTest {

    public static void main(String[] args) throws ClassNotFoundException {
        String str1 = "liu";
        Class class1 = str1.getClass();
        Class class2 = String.class;
        Class class3 = Class.forName("java.lang.String");

        System.out.println(class1 == class2);
        System.out.println(class1 == class3);
        System.out.println(class2 == class3);

    }
}

通过打印的结果我们可以知道我们都是输出同一份字节码,所以结果都是true,我们再来看,我们的Class其实还有一个方法

System.out.println(class1.isPrimitive());
System.out.println(Integer.class == int.class);
System.out.println(int.class == Integer.TYPE);

打印的结果是true,false,true,那这三又是什么意思呢?这个就留给大家猜想一下,应该很容易就知道了的

  • isPrimitive是否是基础类型或者原始类型

那我们的数组是什么类型呢?

宗旨,只要是在源程序中出现的类型,都有各自的Class类型,列入int[],Void等

二.反射的概念

好的。了解了Class类的神奇,我们就可以来看看反射的机制了

反射的概念:反射就是把JAVA类中的各种成分映射成相应的JAVA类,例如,一个java类中用一个Class类的对象来表示,一个类中的组成部分,成员变量,方法,构造方法,包等信息,也用一个个的JAVA类来表示,就像汽车是一个类,汽车中的发动机,变速箱也是一个类,表示JAVA的Class类要提供一系列的方法,来获取其中的变量,方法,构造方法,修饰符,包等信息,这些信息是用来相应类的实例对象表示,衙门是Field,Method,Contructor,Package等

一个类中的每一个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class来调用各种方法,我们接下来一个个讲

三.Constructor

代表的是某一个类的构造方法,我们用代码来看


public class CalssTest {

    public static void main(String[] args)throws NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        //得到String的StringBuffer构造方法
        Constructor<String> constructor = String.class.getConstructor(StringBuffer.class);
        String str = constructor.newInstance(new StringBuffer("str"));
        System.out.println(str);
    }
}

通过获得的方法用到上面相同类型的实例,再看newInstance,得到我们设置参数的构造方法,我们再来看下成员变量的反射

四.Fiald

这里我们就模拟了一个类,作为反射的对象


public class FialdClass {

    public int x;
    publicint y;

    public FialdClass(int x,int y) {
        this.x = x;
        this.y = y;
    }
}

那我们实际的操作就是:

public class CalssTest {

    public static void main(String[] args){

        try {
            //首先为x,y 赋值
            FialdClass fClass = new FialdClass(10, 20);
            //反射成员变量
            Field field1 = fClass.getClass().getField("x");
            Field field2 = fClass.getClass().getField("y");
            //指定对象
            System.out.println(field1.get(fClass));
            System.out.println(field2.get(fClass));
        } catch (NoSuchFieldException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        } catch (SecurityException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

这样我们就可以直接反射到成员变量的值了,这里我们来做一个小练习来测试一下我们成员变量的反射的理解

练习题

  • 假设我们有一个对象中,有许多的String类型成员变量,我们的需求就是把所有对应的字符串内容”b”改成”a”

题目有了我们整理下思路,首先要做的就是定义一个对象咯


public class FialdClass {

    public int x;
    public int y;
    public String str1 = "boy";
    public String str2 = "bbq";
    public String str3 = "hello";

    public FialdClass(int x,int y) {
        this.x = x;
        this.y = y;
    }
    @Override
    public String toString() {
        return "FialdClass [x=" + x + ", y=" + y + ", str1=" + str1 + ", str2="
                + str2 + ", str3=" + str3 + "]";
    }
}

好的,这里有三个string类型的成员变量,那我们该怎么去反射呢?


/**
 * 假设我们有一个对象中,有许多的String类型成员变量,我们
 * 的需求就是把所有对应的字符串内容"b"改成"a"
 * @author LGL
 *
 */
public class CalssTest {

    public static void main(String[] args) throws Exception{

        FialdClass fClass = new FialdClass(5, 10);
        //得到所有的成員变量
        Field[] fields = fClass.getClass().getFields();
        for(Field field : fields){
            //过滤下String类型
            if(field.getType() == String.class){
                //取值
                String oldValue = (String) field.get(fClass);
                String newValue =  oldValue.replace("b", "a");
                field.set(fClass, newValue);
            }
        }
        System.out.println(fClass);
    }
}

这里思路还是比较清晰的,我们一步步的去过滤,最终更改了值

这里写图片描述

五.Method

如果前面的都是不怎么常用的,那这个我相信你绝对会喜欢,也就是方法,Method,我们来看看怎么用吧!

public class CalssTest {

    public static void main(String[] args) throws Exception{

        String str1 = "abc";
        //反射charAt方法
        Method method = String.class.getMethod("charAt", int.class);
        //作用对象 第几个
        System.out.println(method.invoke(str1, 2));
    }
}

这里我反射的是String的方法charAt,取第二个char,所以打印出来是c

这其实套用了一种专家模式,谁调用了这个数据,谁就是这个专家,现在我们思考一下,如果我们代码是这样写:

System.out.println(method.invoke(null, 2));

这里我们不传对象,而是传一个null进去,那我们知道这个Method是调用什么吗?而纵观之下,只有static修饰的静态方法是不需要对象的,那就说明他调用的是对象了;

在JDK1.4和JDK1.5中,invoke的参数是有些区别的,不过我们现在都1.8了,自然而然这个我们不需要去考虑和追究;

六.反射Main方法

我们现在来思考一下,如何通过反射去执行一个类的Main方法,这就比较好玩了,这里就相当于一个练习题了,而题目就是:

  • 写一个程序,这个程序能够根据用户提供的类名,去执行该类的main方法

而我们的在写这段程序之前,我们就要思考一下了,我们观察main方法:

public static void main(String[] args) {
    //main方法
}

你是否已经发现,这个main方法的参数是一个数组,,如果我们通过反射来调用这个方法,那我们该如何为invoke方法传递参数呢?

在以前的老程序员可能会在纠结JDK1.4和JDK1.5的invoke区别,但是我们倒不用去考虑这些,因为我们的版本是JDK1.8,这样吧,我们先写个需要调用的类:

public class MainClass {

    public static void main(String[] args) {

        for (String arg : args) {

            System.out.println("Main:" + arg);
        }
    }
}

OK,这个类就是打印下数组的值,那我们真正的反射应该怎么去写尼?传统的调用方法是这样的:

MainClass.main(new String[] { "Hello" });

我们反射的话,就不是这么简单了,我们需要这样:

public class CalssTest {

    public static void main(String[] args) throws Exception {

        // MainClass.main(new String[] { "Hello" });
        // 指定类名
        // String className = args[0];
        // 指定Main方法
        Method main = Class.forName("MainClass").getMethod("main",
                String[].class);
        // 调用main方法
        main.invoke(null, (Object) new String[] { "Hello", "LGL" });
    }
}

这里为什么需要强转为Object?不是说好的传递数组吗?吗是因为JDK为了兼容老版本,如果你传递的是一个数组有多个值会进行拆包,我现在声明一个Object就是告诉他我这里只是一个对象,避免对象异常,我们看下输出的结果:

这里写图片描述

说明我们是调用成功了的;那这里就牵引出数组的反射和Object的关系了,我们继续来看;

七.数组的反射

数组实际上也是一个对象,这点我们一定要清楚,是吧

  • 具有相同维数和元素类型的数组术语同一类型,即具有相同的Class实例对象
  • 代表数组的Class实例对象的getSuperClass()方法返回的父类Object为对象的Class

这里我们以数组为例子就好了,我们来验证一下字节码:

int[] arr1 = { 3 };
int[] arr2 = { 4 };
//true
System.out.println(arr1.getClass() == arr2.getClass());
//[I
System.out.println(arr1.getClass().getName());

第一个为true也就验证了第一条,第二个输出的结果是[I,这个代表的是什么意思呢?[代表的是数组,I代表的是Int类型,也就是这个是int类型的数组

这里写图片描述

看一下JDK对照文档就行了

接下来,我们来看下数组的反射具体实践是什么样子的!

八.数组反射实践

我们出道题目好了在这样看起来可能更加生动形象一点

  • 题目:怎么得到数组中的元素?

通过查看API文档,我们知道可以反射这个Array

这里写图片描述

那这个Array要怎么用呢?

假设我现在封装一个打印的方法,这个打印的方法很简单,我定义一个Object参数,你传进来什么,我就打印什么,那要是传递数组,我就一个个的打印,这样的理解应该就比较清晰了,那我们实际的操作你怎样的呢?


public class CalssTest {

    public static void main(String[] args) throws Exception {

        int[] arr = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 };
        printObject(arr);
    }

    private static void printObject(Object obj) {
        // 拿到字节码
        Class<? extends Object> classs = obj.getClass();
        // 判断是否是数组
        if (classs.isArray()) {
            // 得到长度
            int len = Array.getLength(obj);
            // 取出每一个
            for (int i = 0; i < len; i++) {
                System.out.print(Array.get(obj, i));
            }
        } else {
            System.out.println(obj);
        }
    }
}

但是如何你要问我这个数组的数据类型,我倒是没有获取到,不知为何!!!

好的,比较浅显的反射基础就偷偷的路过了,有兴趣的朋友可以了解下,也可以进群:555974449 一起来探讨探讨!

作者:qq_26787115 发表于2016/10/5 1:25:28 原文链接
阅读:206 评论:2 查看评论

百度地图学习(一):定位

$
0
0

        百度地图的android的API不像腾讯地图是下载一个库其他就能用了,而是需要用什么功能就去选择什么功能然后下载相应功能组好打包后的库。这样模块化的好处当然是有利于程序的拓展也能缩小app的大小。不过太多灵活容错也低了,比如我为实现定位的功能勾选下载了整套功能导入了工程文件,即下图的所有选项。最后发现官方代码里有些方法和类竟然在自己的工程里都识别不了。最后只下载了基础地图和地处定位才好。

<img src="http://img.blog.csdn.net/20161005093522201?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" width="481" height="280" />

步骤如下:

     一,导入需要的库文件

   二、代码及注释如下:

       1、主布局文件

<span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    tools:context="com.baidumapdemo.curio.baidumapdemo.MainActivity">

    <com.baidu.mapapi.map.MapView
        android:id="@+id/bmapView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:clickable="true" />
</RelativeLayout>
</span>

  
2、主activity

     

<span style="font-size:14px;">package com.baidumapdemo.curio.baidumapdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.baidu.mapapi.SDKInitializer;
import com.baidu.mapapi.map.BaiduMap;
import com.baidu.mapapi.map.BitmapDescriptor;
import com.baidu.mapapi.map.BitmapDescriptorFactory;
import com.baidu.mapapi.map.MapStatusUpdateFactory;
import com.baidu.mapapi.map.MapView;


import com.baidu.location.BDLocation;
import com.baidu.location.BDLocationListener;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;
import com.baidu.location.Poi;
import com.baidu.mapapi.map.MarkerOptions;
import com.baidu.mapapi.map.OverlayOptions;
import com.baidu.mapapi.model.LatLng;

import java.util.List;

public class MainActivity extends AppCompatActivity {
    MapView mMapView = null;
    BaiduMap mBaiduMap = null;
    public LocationClient mLocationClient = null;
    public BDLocationListener myListener = new MyLocationListener();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //在使用SDK各组件之前初始化context信息,传入ApplicationContext
        //注意该方法要再setContentView方法之前实现
        SDKInitializer.initialize(getApplicationContext());
        setContentView(R.layout.activity_main);
        //获取地图控件引用
        mMapView = (MapView) findViewById(R.id.bmapView);
        mBaiduMap = mMapView.getMap();
        //普通地图
        mBaiduMap.setMapType(BaiduMap.MAP_TYPE_NORMAL);

        mLocationClient = new LocationClient(getApplicationContext());     //声明LocationClient类
        mLocationClient.registerLocationListener( myListener );    //注册监听函数
        mLocationClient.start();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //在activity执行onDestroy时执行mMapView.onDestroy(),实现地图生命周期管理
        mMapView.onDestroy();
    }
    @Override
    protected void onResume() {
        super.onResume();
        //在activity执行onResume时执行mMapView. onResume (),实现地图生命周期管理
        mMapView.onResume();
    }
    @Override
    protected void onPause() {
        super.onPause();
        //在activity执行onPause时执行mMapView. onPause (),实现地图生命周期管理
        mMapView.onPause();
    }

    private void initLocation(){
        LocationClientOption option = new LocationClientOption();
        option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy
        );//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
        option.setCoorType("bd09ll");//可选,默认gcj02,设置返回的定位结果坐标系
        int span=1000;
        option.setScanSpan(span);//可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
        option.setIsNeedAddress(true);//可选,设置是否需要地址信息,默认不需要
        option.setOpenGps(true);//可选,默认false,设置是否使用gps
        option.setLocationNotify(true);//可选,默认false,设置是否当gps有效时按照1S1次频率输出GPS结果
        option.setIsNeedLocationDescribe(true);//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
        option.setIsNeedLocationPoiList(true);//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
        option.setIgnoreKillProcess(false);//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
        option.SetIgnoreCacheException(false);//可选,默认false,设置是否收集CRASH信息,默认收集
        option.setEnableSimulateGps(false);//可选,默认false,设置是否需要过滤gps仿真结果,默认需要
        mLocationClient.setLocOption(option);
    }

    public class MyLocationListener implements BDLocationListener {

        @Override
        public void onReceiveLocation(BDLocation location) {
            //Receive Location
            StringBuffer sb = new StringBuffer(256);
            sb.append("time : ");
            sb.append(location.getTime());
            sb.append("\nerror code : ");
            sb.append(location.getLocType());
            sb.append("\nlatitude : ");
            sb.append(location.getLatitude());
            sb.append("\nlontitude : ");
            sb.append(location.getLongitude());
            sb.append("\nradius : ");
            sb.append(location.getRadius());
            if (location.getLocType() == BDLocation.TypeGpsLocation) {// GPS定位结果
                sb.append("\nspeed : ");
                sb.append(location.getSpeed());// 单位:公里每小时
                sb.append("\nsatellite : ");
                sb.append(location.getSatelliteNumber());
                sb.append("\nheight : ");
                sb.append(location.getAltitude());// 单位:米
                sb.append("\ndirection : ");
                sb.append(location.getDirection());// 单位度
                sb.append("\naddr : ");
                sb.append(location.getAddrStr());
                sb.append("\ndescribe : ");
                sb.append("gps定位成功");


            } else if (location.getLocType() == BDLocation.TypeNetWorkLocation) {
            // 网络定位结果
                sb.append("\naddr : ");
                sb.append(location.getAddrStr());
                //运营商信息
                sb.append("\noperationers : ");
                sb.append(location.getOperators());
                sb.append("\ndescribe : ");
                sb.append("网络定位成功");
            } else if (location.getLocType() == BDLocation.TypeOffLineLocation) {// 离线定位结果
                sb.append("\ndescribe : ");
                sb.append("离线定位成功,离线定位结果也是有效的");
            } else if (location.getLocType() == BDLocation.TypeServerError) {
                sb.append("\ndescribe : ");
                sb.append("服务端网络定位失败,可以反馈IMEI号和大体定位时间到loc-bugs@baidu.com,会有人追查原因");
            } else if (location.getLocType() == BDLocation.TypeNetWorkException) {
                sb.append("\ndescribe : ");
                sb.append("网络不同导致定位失败,请检查网络是否通畅");
            } else if (location.getLocType() == BDLocation.TypeCriteriaException) {
                sb.append("\ndescribe : ");
                sb.append("无法获取有效定位依据导致定位失败,一般是由于手机的原因,处于飞行模式下一般会造成这种结果,可以试着重启手机");
            }
            sb.append("\nlocationdescribe : ");
            sb.append(location.getLocationDescribe());// 位置语义化信息
            List<Poi> list = location.getPoiList();// POI数据
            if (list != null) {
                sb.append("\npoilist size = : ");
                sb.append(list.size());
                for (Poi p : list) {
                    sb.append("\npoi= : ");
                    sb.append(p.getId() + " " + p.getName() + " " + p.getRank());
                }
            }
            //定义Maker坐标点
            LatLng point = new LatLng(location.getLatitude(), location.getLongitude());
            //构建Marker图标
            BitmapDescriptor bitmap = BitmapDescriptorFactory
                    .fromResource(R.drawable.icon_marka);
            //构建MarkerOption,用于在地图上添加Marker
            OverlayOptions option = new MarkerOptions()
                    .position(point)
                    .icon(bitmap);
            //在地图上添加Marker,并显示
            mBaiduMap.addOverlay(option);
            mBaiduMap.setMapStatus(MapStatusUpdateFactory.newLatLng(point)); //更新地图状态
            Log.i("BaiduLocationApiDem", sb.toString());
        }
    }
}</span>

git仓库:https://git.coding.net/CuriousXeon/BaiduMapDemo.git



效果图:

      


     

作者:CuriousX 发表于2016/10/5 9:25:21 原文链接
阅读:20 评论:0 查看评论

使用HTTP协议访问网络

$
0
0

1 HttpURLConnection
2 HttpClient

HttpURLConnection使用过程:
1 获取HttpURLConnection的实例:
URL url = new URL(“http://www.baidu.com”);
HttpURLConnection connection = (HttpURLConnection)url.openConnection;
2 设置请求的方法:GET POST 获取/提交
connection.setRequsestMethod(“get”);
3 自由设置
connection.setConnectionTimeout(8000);
connection.setReadTimeout(8000);
4 获取服务器返回的输入流
InputStream in = connection.getInputStream();
5 最后调用connection.disconnect();

实例:get

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/send_request"
        android:text="Send Request"/>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/response_text"/>
    </ScrollView>


</LinearLayout>
package com.example.networktest;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    public static  final  int SHOW_RESPONSE = 0;
    private Button sendRquest;
    private TextView responseText;
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
           switch (msg.what) {
               case SHOW_RESPONSE:
                   String response = (String) msg.obj;
                   //进行UI操作
                   responseText.setText(response);
           }

        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sendRquest = (Button) findViewById(R.id.send_request);
        responseText = (TextView) findViewById(R.id.response_text);
        sendRquest.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if(v.getId() == R.id.send_request){
            sendRequestWithHttpURLConnection();
        }
    }

    private  void sendRequestWithHttpURLConnection(){
        //开启线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                try {
                    URL url = new URL("http://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream in = connection.getInputStream();
                    BufferedReader reader = new BufferedReader((new InputStreamReader(in)));
                    StringBuilder response = new StringBuilder();
                    String line ;
                    while ((line = reader.readLine())!= null){
                        response.append(line);
                    }

                    Message message = new Message() ;
                    message.what = SHOW_RESPONSE ;
                    message.obj = response.toString();
                    handler.sendMessage(message);

                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    if (connection!=null){
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }
}
    <uses-permission android:name="android.permission.INTERNET"/>

post

public static String loginOfPost(String username, String password) {
                HttpURLConnection conn = null;
                try {
                        // 创建一个URL对象
                        URL mURL = new URL("http://192.168.0.100:8080/android/servlet/LoginServlet");
                        // 调用URL的openConnection()方法,获取HttpURLConnection对象
                        conn = (HttpURLConnection) mURL.openConnection();

                        conn.setRequestMethod("POST");// 设置请求方法为post
                        conn.setReadTimeout(5000);// 设置读取超时为5秒
                        conn.setConnectTimeout(10000);// 设置连接网络超时为10秒
                        conn.setDoOutput(true);// 设置此方法,允许向服务器输出内容

                        // post请求的参数
                        String data = "username=" + username + "&password=" + password;
                        // 获得一个输出流,向服务器写数据,默认情况下,系统不允许向服务器输出内容
                        OutputStream out = conn.getOutputStream();// 获得一个输出流,向服务器写数据
                        out.write(data.getBytes());
                        out.flush(); // 刷新对象输出流,将任何字节都写入潜在的流中
                        out.close();

                        int responseCode = conn.getResponseCode();// 调用此方法就不必再使用conn.connect()方法
                        if (responseCode == 200) {

                                InputStream is = conn.getInputStream();
                                String state = getStringFromInputStream(is);
                                return state;
                        } else {
                                Log.i(TAG, "访问失败" + responseCode);
                        }

                } catch (Exception e) {
                        e.printStackTrace();
                } finally {
                        if (conn != null) {
                                conn.disconnect();// 关闭连接
                        }
                }

                return null;
        }

2 使用HttpClient (在Android 6.0(API 23) 中,Google已经移除了Apache HttpClient 想关类,推荐使用HttpUrlConnection,如果要继续使用,在Android studio对应的module下的build.gradle文件中加入:
android {
useLibrary ‘org.apache.http.legacy’
})

package com.example.networktest;

import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.android.gms.appindexing.Action;
import com.google.android.gms.appindexing.AppIndex;
import com.google.android.gms.common.api.GoogleApiClient;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    public static final int SHOW_RESPONSE = 0;
    private Button sendRquest;
    private TextView responseText;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SHOW_RESPONSE:
                    String response = (String) msg.obj;
                    //进行UI操作
                    responseText.setText(response);
            }

        }
    };
    /**
     * ATTENTION: This was auto-generated to implement the App Indexing API.
     * See https://g.co/AppIndexing/AndroidStudio for more information.
     */
    private GoogleApiClient client;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sendRquest = (Button) findViewById(R.id.send_request);
        responseText = (TextView) findViewById(R.id.response_text);
        sendRquest.setOnClickListener(this);
        // ATTENTION: This was auto-generated to implement the App Indexing API.
        // See https://g.co/AppIndexing/AndroidStudio for more information.
        client = new GoogleApiClient.Builder(this).addApi(AppIndex.API).build();
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.send_request) {
            // sendRequestWithHttpURLConnection();
            sendRequestWithHttpClient();
        }
    }

    private void sendRequestWithHttpURLConnection() {
        //开启线程
        new Thread(new Runnable() {
            @Override
            public void run() {

                HttpURLConnection connection = null;
                try {

                    URL url = new URL("http://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream in = connection.getInputStream();
                    BufferedReader reader = new BufferedReader((new InputStreamReader(in)));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }

                    Message message = new Message();
                    message.what = SHOW_RESPONSE;
                    message.obj = response.toString();
                    handler.sendMessage(message);

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }

    private void sendRequestWithHttpClient() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    HttpClient httpClient = new DefaultHttpClient();
                    HttpGet httpGet = new HttpGet("http://www.baidu.com");
                    HttpResponse httpResponse = httpClient.execute(httpGet);
                    if (httpResponse.getStatusLine().getStatusCode() == 200){
                        HttpEntity entity =httpResponse.getEntity();
                        String response = EntityUtils.toString(entity,"utf-8");
                        Message message = new Message();
                        message.what = SHOW_RESPONSE ;
                        message.obj = response.toString();
                        handler.sendMessage(message);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    @Override
    public void onStart() {
        super.onStart();

        // ATTENTION: This was auto-generated to implement the App Indexing API.
        // See https://g.co/AppIndexing/AndroidStudio for more information.
        client.connect();
        Action viewAction = Action.newAction(
                Action.TYPE_VIEW, // TODO: choose an action type.
                "Main Page", // TODO: Define a title for the content shown.
                // TODO: If you have web page content that matches this app activity's content,
                // make sure this auto-generated web page URL is correct.
                // Otherwise, set the URL to null.
                Uri.parse("http://host/path"),
                // TODO: Make sure this auto-generated app URL is correct.
                Uri.parse("android-app://com.example.networktest/http/host/path")
        );
        AppIndex.AppIndexApi.start(client, viewAction);
    }

    @Override
    public void onStop() {
        super.onStop();

        // ATTENTION: This was auto-generated to implement the App Indexing API.
        // See https://g.co/AppIndexing/AndroidStudio for more information.
        Action viewAction = Action.newAction(
                Action.TYPE_VIEW, // TODO: choose an action type.
                "Main Page", // TODO: Define a title for the content shown.
                // TODO: If you have web page content that matches this app activity's content,
                // make sure this auto-generated web page URL is correct.
                // Otherwise, set the URL to null.
                Uri.parse("http://host/path"),
                // TODO: Make sure this auto-generated app URL is correct.
                Uri.parse("android-app://com.example.networktest/http/host/path")
        );
        AppIndex.AppIndexApi.end(client, viewAction);
        client.disconnect();
    }
}
作者:So_discrepancy 发表于2016/10/5 9:37:35 原文链接
阅读:20 评论:0 查看评论

TankWar单机 (JAVA版) 目录

$
0
0

早就想写了  一直都在忙  暑期去了无锡一个月见习  白天电脑没网  晚上网速十几K  csdn都登录不上  。

7.5-7.29暑期结束就回家了三天  看看奶奶  。然后8.3就又到学校了 ,看着15acm成员的集训。然后8.13号数学建模

的培训  培训到8.28号。就开学了。。又报了驾校  我的天。。。浑浑噩噩就到今天了  唉

从今天起要改变这一个月来的状态了。

于是决定写一下坦克大战  单机的 

每天有时间就写一点  绝不TJ

我在网上搜了一个坦克大战的ppt  就根据这个ppt写吧。

java项目坦克大战PPT

每完成一个版本我会写一篇博客  有的简单 勿喷  一起学习而已。谢谢

作者:su20145104009 发表于2016/10/5 10:40:49 原文链接
阅读:28 评论:0 查看评论

Android初级教程:RatingBar的使用

$
0
0

记得淘宝里面买家给卖家评分的时候会有一个星星状的评分条,其实就是基于RatingBar做了自定义使用了。那么本篇文章就对RatingBar的使用做一个基本的认识。

接下来就是正题,那就是对于RatingBar,我到底想要做些什么。一般都有这样的需求:

        1.怎样实现一个RatingBar.这是一个很基本的问题,实现不出来,那么其他的东西自然不用说了,而且里面是有一个附加的子问题,就是我要实现一个RatingBar,到底需要的是什么。这个问题其实是上面问题的另一种问法,但还是要提出来。为什么呢?因为网上教程它们实现的RatingBar的样式是各种各样的,但是我如果需要的仅仅只是一个默认样式的呢?根本就不需要那么麻烦啊!所以,这点还是很有知道的必要的。
        2.如果我想要我的RatingBar实现各种效果,比如说,大小,颜色,位置,甚至是样式,我该怎么办?这个问题是我们经常遇到的,因为就像我上面说过的,android组件更多的问题都是因为我们不满足默认而想要自己定义的,而这些,都可以通过RatingBar的一些方法或布局设置来实现。
       3.RatingBar与其他组件的搭配使用,这就是组件的组合问题。这个问题是很常见的,因为我们在实际设计东西时,几种组件经常是要按我们想要的方式组合在一一起,而且,要命的是,这种组合有时是会出现问题的,因为组件或布局间的兼容问题吧。这些问题有时候是很烦的,甚至是会让人崩溃的。
        既然问题已经罗列出来,那么,现在就是就着这些问题一条一条解决了。
一.RatingBar的基本实现:
       要实现一个基本的RatingBar,其实并不复杂,就是需要一个布局,这个布局再添加RatingBar这个组件就行。如:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical" >

    <RatingBar
        android:id="@+id/ratingBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="5"
        android:rating="3.5"
        android:stepSize="0.5" />

</LinearLayout>

接着就是activity的代码:

package com.itydl.ratingbar;

import android.app.Activity;
import android.os.Bundle;
import android.widget.RatingBar;

public class MainActivity extends Activity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		RatingBar ratingBar = (RatingBar) findViewById(R.id.ratingBar);
	}

}

先做个理论说明,再看一看运行示例。

这里,就对布局文件上几个属性进行说明,因为这是我们构建RatingBar必须要了解的,而且还是我们需要修改的。这里,就要提示各位,如果你的布局中有RatingBar,那么你的layout_width和layout_height最好要设置成wrap_content,至少layout_width是必须这么做的,因为如果没有这么做,那么很可能你的星星显示不出来,当然,你可以修改一下星星的大小,就可以显示更多的星星。那么,接下来,就是介绍RatingBar中的相关属性。

     android:rating = "3.5",这一条是设置默认的分数,可以是浮点数,因为我们的RatingBar其实是评分条,所以星星的数量其实就是用来显示分数的数量,而默认的分数就是应用一开始出现的星星的数量。
     android:numStars
="5",这个是设置显示的星星数量,理论上,是任何整数都可以,但是我的建议就是最好就是5,因为如果多于5的话,我试过,就我的手机,3.7寸大的屏幕,最多只能显示5.8,为什么是这么怪的数据,因为那个第六个星星根本就还有一角没有显示出来。

     android:stepSize = "0.5“,这是显示步长的。什么是步长,就是当你的分数增加时,是按照多大的分数增加的,就像这里,步长是0.5,那么你的手机上显示的星星是可以用你的手指点击来增加分数的,而这里就会按照一次点击增加0.5的步长,就是半颗星星。

      但是,这里有点必须要注意,就是我们有时候,不,应该是大部分情况,都不希望我们的RatingBar是可以改动的,是固定的显示分数的,那么,这时我们应该怎样做呢?就是再增加一条属性,就是android:isIndicator="true",这样分数就会固定,不会改变,所以,步长的设定就完全没有必要。

       当然,上面的某些属性也是有相应的方法是可以更改的,但是一般情况下都是不需要修改的,如果你的设计需要的话,可以看一下文档,里面有介绍,那么我这里就不多说了。

      运行示例:

二.RatingBar样式的修改

很多时候,默认的RatingBar并不能满足我们的要求,因为我们的应用的需求是各种各样的,默认的样式实在是太过于单调了。这时就需要修改RatingBar的属性了。一般而言,我们都只会修改RatingBar的大小,图样,颜色,等等。所以,这里我就只挑几种比较常见的,就是上面说到的三种。

        1.大小。

           默认的RatingBar的星星大小,老实说,实在太大了,因为有时候我们呢,会想要将RatingBar放在其他组件上,如ListView,所以,默认的大小肯定不符合我们的要求。那么,我们需要将星星变小一点,于是就需要在RatingBar的布局中添加这么一句:style="?android:attr/ratingBarStyleSmall"这样,星星的大小就会变小,效果图如下:

很抱歉,就我目前收集到的资料来看,星星的大小就只有默认和变小这两种模式,没有其他的情况,但是就我目前的使用情况来说,已经足够了,如果你们有其他要求,那么,我的建议就是换掉默认的星星图案吧。

             接下来就是颜色和图样的改变。为什么这两条要放在一块讲呢?因为这两条的修改就是自定义自己的RatingBar,所谓的自定义,其实就是用自己的图样换掉默认的图案,这样是最好的情况,因为包括大小,颜色等等你都能使用自己满意的样式。因为我实在是没有找到什么相关的方法和资料能够解决这个问题,基本上,网上都是选择替换掉图案,因为系统中的星星的图案是固定的,它就只是一张图片而已,所以应该真的是没法用代码进行修改。应该吧?因为以后可能就不一样了吧。方法如下:

             2.RatingBar的自定义设置:

                我们还是要在我们的RatingBar的布局中添加这么一句:style="@style/myRatingBar",然后就是开始创建我们的myRatingBar的xml文件了。在res/values添加样式,在res/values目录下创建一个xml文件,然后下面是代码:

 <style name="MyRatingBar" parent="@android:style/Widget.RatingBar">
        <item name="android:progressDrawable">@drawable/myRatingBar</item>
        <item name="android:minHeight">15dip</item>
        <item name="android:maxHeight">15dip</item>
    </style>
item name="android:minHeight"和item name="android:maxHeight"可以自己定义自己的RatingBar的大小。
             接着是在res/drawable下创建我们的myRatingBar.xml文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>    
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">    
    <item android:id="@+android:id/background"    
          android:drawable="@drawable/rating" />    
    <item android:id="@+android:id/progress"    
          android:drawable="@drawable/rating_show" />    
</layer-list>   
这里面,第一个item是设置背景图案的,第二个item是设置RatingBar图案的。              到了这里,基本上的设置已经搞定了,我们可以自定义属于自己的RatingBar了。运行来看看长得啥样吧:

当然,您也可以直接通过引入drable的方式给RatingBar定义样式。例如下面:

在drawable文件下面,加入如下xml文件:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@android:id/background"
        android:drawable="@drawable/rating_small_empty">
    </item>
    <item
        android:id="@android:id/secondaryProgress"
        android:drawable="@drawable/rating_small_half">
    </item>
    <item
        android:id="@android:id/progress"
        android:drawable="@drawable/rating_small_full">
    </item>

</layer-list>

三个item分别表示:1、星星的背景色为rating_small_empty这张图片的颜色;2、星星的进度,或者说是半个星星是rating_small_half的样式;3、星星总进度或者说是全部填充星星为rating_small_full这张图片的背景色。

在布局文件中就应该这么引用了:

<RatingBar
        android:id="@+id/ratingBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:numStars="5"
        android:progressDrawable="@drawable/custom_ratingbar"
        android:rating="3.5"
        android:stepSize="0.5" />

最后再看一下运行结果:

至此,打完收工~~~

作者:qq_32059827 发表于2016/10/5 10:55:15 原文链接
阅读:7 评论:0 查看评论

TankWar 单机(JAVA版) 版本0.1&&版本0.2

$
0
0


上面是项目要求  

继承JFrame  就能实现一个窗口了

没有什么好解释的 具体看代码注释

package tankWar;

import javax.swing.JFrame;

public class TankClient extends JFrame{
	//窗口的高度
	public static int screenHeight=600;
	//窗口的宽度
	public static int screenWidth=800;
	//创建一个窗口
	public TankClient(){
		setTitle("坦克大战");
		//窗口的大小
		setSize(screenWidth, screenHeight);
		//设置窗口的显示位置在屏幕中央
		setLocationRelativeTo(null);
		//关闭窗口的事件管理
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		//不允许窗口的大小改动
		setResizable(false);
		setVisible(true);
	}
	public static void main(String[] args) {
		//启动窗口
		new TankClient();
		
	}

}

运行结果:


作者:su20145104009 发表于2016/10/5 11:00:37 原文链接
阅读:0 评论:0 查看评论

深入理解 iOS 开发中的锁

$
0
0

摘要

本文的目的不是介绍 iOS 中各种锁如何使用,一方面笔者没有大量的实战经验,另一方面这样的文章相当多,比如 iOS中保证线程安全的几种方式与性能对比iOS 常见知识点(三):Lock。本文也不会详细介绍锁的具体实现原理,这会涉及到太多相关知识,笔者不敢误人子弟。

本文要做的就是简单的分析 iOS 开发中常见的几种锁如何实现,以及优缺点是什么,为什么会有性能上的差距,最终会简单的介绍锁的底层实现原理。水平有限,如果不慎有误,欢迎交流指正。同时建议读者在阅读本文以前,对 OC 中各种锁的使用方法先有大概的认识。

在 ibireme 的 不再安全的 OSSpinLock 一文中,有一张图片简单的比较了各种锁的加解锁性能:

来源:ibireme

本文会按照从上至下(速度由快至慢)的顺序分析每个锁的实现原理。需要说明的是,加解锁速度不表示锁的效率,只表示加解锁操作在执行时的复杂程度,下文会通过具体的例子来解释。

OSSpinLock

上述文章中已经介绍了 OSSpinLock 不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

为什么忙等会导致低优先级线程拿不到时间片?这还得从操作系统的线程调度说起。

现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。

自旋锁的实现原理

自旋锁的目的是为了确保临界区只有一个线程可以访问,它的使用可以用下面这段伪代码来描述:

<code class="language-c hljs  has-numbering"><span class="hljs-keyword">do</span> {
    Acquire Lock
        Critical section  <span class="hljs-comment">// 临界区</span>
    Release Lock
        Reminder section <span class="hljs-comment">// 不需要锁保护的代码</span>
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul>

在 Acquire Lock 这一步,我们申请加锁,目的是为了保护临界区(Critical Section) 中的代码不会被多个线程执行。

自旋锁的实现思路很简单,理论上来说只要定义一个全局变量,用来表示锁的可用情况即可,伪代码如下:

<code class="language-c hljs  has-numbering"><span class="hljs-keyword">bool</span> lock = <span class="hljs-keyword">false</span>; <span class="hljs-comment">// 一开始没有锁上,任何线程都可以申请锁</span>
<span class="hljs-keyword">do</span> {
    <span class="hljs-keyword">while</span>(lock); <span class="hljs-comment">// 如果 lock 为 true 就一直死循环,相当于申请锁</span>
    lock = <span class="hljs-keyword">true</span>; <span class="hljs-comment">// 挂上锁,这样别的线程就无法获得锁</span>
        Critical section  <span class="hljs-comment">// 临界区</span>
    lock = <span class="hljs-keyword">false</span>; <span class="hljs-comment">// 相当于释放锁,这样别的线程可以进入临界区</span>
        Reminder section <span class="hljs-comment">// 不需要锁保护的代码        </span>
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul>

注释写得很清楚,就不再逐行分析了。可惜这段代码存在一个问题: 如果一开始有多个线程同时执行 while 循环,他们都不会在这里卡住,而是继续执行,这样就无法保证锁的可靠性了。解决思路也很简单,只要确保申请锁的过程是原子操作即可。

原子操作

狭义上的原子操作表示一条不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。在单处理器环境下,一条汇编指令显然是原子操作,因为中断也要通过指令来实现。

然而在多处理器的情况下,能够被多个处理器同时执行的操作任然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。

这些非常底层的概念无需完全掌握,我们只要知道上述申请锁的过程,可以用一个原子性操作 test_and_set 来完成,它用伪代码可以这样表示:

<code class="language-c hljs  has-numbering"><span class="hljs-keyword">bool</span> test_and_set (<span class="hljs-keyword">bool</span> *target) {
    <span class="hljs-keyword">bool</span> rv = *target; 
    *target = TRUE; 
    <span class="hljs-keyword">return</span> rv;
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>

这段代码的作用是把 target 的值设置为 1,并返回原来的值。当然,在具体实现时,它通过一个原子性的指令来完成。

自旋锁的总结

至此,自旋锁的实现原理就很清楚了:

<code class="language-c hljs  has-numbering"><span class="hljs-keyword">bool</span> lock = <span class="hljs-keyword">false</span>; <span class="hljs-comment">// 一开始没有锁上,任何线程都可以申请锁</span>
<span class="hljs-keyword">do</span> {
    <span class="hljs-keyword">while</span>(test_and_set(&lock); <span class="hljs-comment">// test_and_set 是一个原子操作</span>
        Critical section  <span class="hljs-comment">// 临界区</span>
    lock = <span class="hljs-keyword">false</span>; <span class="hljs-comment">// 相当于释放锁,这样别的线程可以进入临界区</span>
        Reminder section <span class="hljs-comment">// 不需要锁保护的代码        </span>
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul>

如果临界区的执行时间过长,使用自旋锁不是个好主意。之前我们介绍过时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的。

信号量

之前我在 介绍 GCD 底层实现的文章 中简单描述了信号量 dispatch_semaphore_t 的实现原理,它最终会调用到 sem_wait 方法,这个方法在 glibc 中被实现如下:

<code class="language-c hljs  has-numbering"><span class="hljs-keyword">int</span> sem_wait (sem_t *sem) {
  <span class="hljs-keyword">int</span> *futex = (<span class="hljs-keyword">int</span> *) sem;
  <span class="hljs-keyword">if</span> (atomic_decrement_if_positive (futex) > <span class="hljs-number">0</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  <span class="hljs-keyword">int</span> err = lll_futex_wait (futex, <span class="hljs-number">0</span>);
    <span class="hljs-keyword">return</span> -<span class="hljs-number">1</span>;
)</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul>

首先会把信号量的值减一,并判断是否大于零。如果大于零,说明不用等待,所以立刻返回。具体的等待操作在 lll_futex_wait 函数中实现,lll 是 low level lock 的简称。这个函数通过汇编代码实现,调用到 SYS_futex 这个系统调用,使线程进入睡眠状态,主动让出时间片,这个函数在互斥锁的实现中,也有可能被用到。

主动让出时间片并不总是代表效率高。让出时间片会导致操作系统切换到另一个线程,这种上下文切换通常需要 10 微秒左右,而且至少需要两次切换。如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。

可以看到,自旋锁和信号量的实现都非常简单,这也是两者的加解锁耗时分别排在第一和第二的原因。再次强调,加解锁耗时不能准确反应出锁的效率(比如时间片切换就无法发生),它只能从一定程度上衡量锁的实现复杂程度。

pthread_mutex

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 表示互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

互斥锁的常见用法如下:

<code class="language-c hljs  has-numbering">pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  <span class="hljs-comment">// 定义锁的属性</span>

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) <span class="hljs-comment">// 创建锁</span>

pthread_mutex_lock(&mutex); <span class="hljs-comment">// 申请锁</span>
    <span class="hljs-comment">// 临界区</span>
pthread_mutex_unlock(&mutex); <span class="hljs-comment">// 释放锁</span></code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ul>

对于 pthread_mutex 来说,它的用法和之前没有太大的改变,比较重要的是锁的类型,可以有 PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE 等等,具体的特性就不做解释了,网上有很多相关资料。

一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。

然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex 支持递归锁,也就是允许一个线程递归的申请锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可。

互斥锁的实现

互斥锁在申请锁时,调用了 pthread_mutex_lock 方法,它在不同的系统上实现各有不同,有时候它的内部是使用信号量来实现,即使不用信号量,也会调用到 lll_futex_wait 函数,从而导致线程休眠。

上文说到如果临界区很短,忙等的效率也许更高,所以在有些版本的实现中,会首先尝试一定次数(比如 1000 次)的 test_and_test,这样可以在错误使用互斥锁时提高性能。

另外,由于 pthread_mutex 有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。

NSLock

NSLock 是 Objective-C 以对象的形式暴露给开发者的一种锁,它的实现非常简单,通过宏,定义了 lock 方法:

<code class="language-objc hljs cs has-numbering"><span class="hljs-preprocessor">#<span class="hljs-keyword">define</span> MLOCK \</span>
- (<span class="hljs-keyword">void</span>) <span class="hljs-keyword">lock</span>\
{\
  <span class="hljs-keyword">int</span> err = pthread_mutex_lock(&_mutex);\
  <span class="hljs-comment">// 错误处理 ……</span>
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul>

NSLock 只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。

这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,仅仅是内部 pthread_mutex 互斥锁的类型不同。通过宏定义,可以简化方法的定义。

NSLockpthread_mutex 略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。

NSCondition

NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。

如何使用条件变量

很多介绍 pthread_cond_t 的文章都会提到,它需要与互斥锁配合使用:

<code class="language-c hljs  has-numbering"><span class="hljs-keyword">void</span> consumer () { <span class="hljs-comment">// 消费者</span>
    pthread_mutex_lock(&mutex);
    <span class="hljs-keyword">while</span> (data == NULL) {
        pthread_cond_wait(&condition_variable_signal, &mutex); <span class="hljs-comment">// 等待数据</span>
    }
    <span class="hljs-comment">// --- 有新的数据,以下代码负责处理 ↓↓↓↓↓↓</span>
    <span class="hljs-comment">// temp = data;</span>
    <span class="hljs-comment">// --- 有新的数据,以上代码负责处理 ↑↑↑↑↑↑</span>
    pthread_mutex_unlock(&mutex);
}

<span class="hljs-keyword">void</span> producer () {
    pthread_mutex_lock(&mutex);
    <span class="hljs-comment">// 生产数据</span>
    pthread_cond_signal(&condition_variable_signal); <span class="hljs-comment">// 发出信号给消费者,告诉他们有了新的数据</span>
    pthread_mutex_unlock(&mutex);
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li></ul>

自然我们会有疑问:“如果不用互斥锁,只用条件变量会有什么问题呢?”。问题在于,temp = data; 这段代码不是线程安全的,也许在你把 data 读出来以前,已经有别的线程修改了数据。因此我们需要保证消费者拿到的数据是线程安全的。

wait 方法除了会被 signal 方法唤醒,有时还会被虚假唤醒,所以需要这里 while 循环中的判断来做二次确认。

为什么要使用条件变量

介绍条件变量的文章非常多,但大多都对一个一个基本问题避而不谈:“为什么要用条件变量?它仅仅是控制了线程的执行顺序,用信号量或者互斥锁能不能模拟出类似效果?”

网上的相关资料比较少,我简单说一下个人看法。信号量可以一定程度上替代 condition,但是互斥锁不行。在以上给出的生产者-消费者模式的代码中, pthread_cond_wait 方法的本质是锁的转移,消费者放弃锁,然后生产者获得锁,同理,pthread_cond_signal 则是一个锁从生产者到消费者转移的过程。

如果使用互斥锁,我们需要把代码改成这样:

<code class="language-c hljs  has-numbering"><span class="hljs-keyword">void</span> consumer () { <span class="hljs-comment">// 消费者</span>
    pthread_mutex_lock(&mutex);
    <span class="hljs-keyword">while</span> (data == NULL) {
        pthread_mutex_unlock(&mutex);
        pthread_mutex_lock(&another_lock)  <span class="hljs-comment">// 相当于 wait 另一个互斥锁</span>
        pthread_mutex_lock(&mutex);
    }
    pthread_mutex_unlock(&mutex);
}
</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ul>

这样做存在的问题在于,在等待 another_lock 之前, 生产者有可能先执行代码, 从而释放了 another_lock。也就是说,我们无法保证释放锁和等待另一个锁这两个操作是原子性的,也就无法保证“先等待、后释放 another_lock” 这个顺序。

用信号量则不存在这个问题,因为信号量的等待和唤醒并不需要满足先后顺序,信号量只表示有多少个资源可用,因此不存在上述问题。然而与 pthread_cond_wait 保证的原子性锁转移相比,使用信号量似乎存在一定风险(暂时没有查到非原子性操作有何不妥)。

不过,使用 condition 有一个好处,我们可以调用 pthread_cond_broadcast 方法通知所有等待中的消费者,这是使用信号量无法实现的。

NSCondition 的做法

NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者:

<code class="language-objc hljs cs has-numbering">- (<span class="hljs-keyword">void</span>) signal {
  pthread_cond_signal(&_condition);
}

<span class="hljs-comment">// 其实这个函数是通过宏来定义的,展开后就是这样</span>
- (<span class="hljs-keyword">void</span>) <span class="hljs-keyword">lock</span> {
  <span class="hljs-keyword">int</span> err = pthread_mutex_lock(&_mutex);
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul>

它的加解锁过程与 NSLock 几乎一致,理论上来说耗时也应该一样(实际测试也是如此)。在图中显示它耗时略长,我猜测有可能是测试者在每次加解锁的前后还附带了变量的初始化和销毁操作。

NSRecursiveLock

上文已经说过,递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。

NSRecursiveLockNSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE

NSConditionLock

NSConditionLock 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值:

<code class="language-objc hljs objectivec has-numbering"><span class="hljs-comment">// 简化版代码</span>
- (<span class="hljs-keyword">id</span>) initWithCondition: (<span class="hljs-built_in">NSInteger</span>)value {
    <span class="hljs-keyword">if</span> (<span class="hljs-literal">nil</span> != (<span class="hljs-keyword">self</span> = [<span class="hljs-keyword">super</span> init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>;
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li></ul>

它的 lockWhenCondition 方法其实就是消费者方法:

<code class="language-objc hljs cs has-numbering">- (<span class="hljs-keyword">void</span>) lockWhenCondition: (NSInteger)<span class="hljs-keyword">value</span> {
    [_condition <span class="hljs-keyword">lock</span>];
    <span class="hljs-keyword">while</span> (<span class="hljs-keyword">value</span> != _condition_value) {
        [_condition wait];
    }
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li></ul>

对应的 unlockWhenCondition 方法则是生产者,使用了 broadcast 方法通知了所有的消费者:

<code class="language-objc hljs cs has-numbering">- (<span class="hljs-keyword">void</span>) unlockWithCondition: (NSInteger)<span class="hljs-keyword">value</span> {
    _condition_value = <span class="hljs-keyword">value</span>;
    [_condition broadcast];
    [_condition unlock];
}</code><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul><ul class="pre-numbering" style=""><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>

@synchronized

这其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。

我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。

具体的实现原理可以参考这篇文章: 关于 @synchronized,这儿比你想知道的还要多

参考资料

  1. pthread_mutex_lock
  2. ThreadSafety
  3. Difference between binary semaphore and mutex
  4. 关于 @synchronized,这儿比你想知道的还要多
  5. pthread_mutex_lock.c 源码
  6. [Pthread] Linux中的线程同步机制(二)–In Glibc
  7. pthread的各种同步机制
  8. pthread_cond_wait
  9. Conditional Variable vs Semaphore
作者:super_man_ww 发表于2016/10/8 9:10:34 原文链接
阅读:46 评论:0 查看评论

Android tips(十三)-->Android开发过程中使用Lambda表达式

$
0
0

转载请标明出处:一片枫叶的专栏

新的Java8 API中提供了不少新的特性,其中就有Lambda表达式。而本文我们将主要介绍一下在Android开发过程中如何使用Lambda表达式,这里主要是为我们后续介绍RxAndroid、RxJava相关知识做铺垫的。

  • Lambda表达式的概念

Lambda表达式是Java8中提供的一种新的特性,它支持Java也能进行简单的“函数式编程”,即Lambda允许你通过表达式来代替功能接口。其实Lambda表达式的本质只是一个”语法糖”,由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。

Lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)

咋样很厉害吧?下面我们将慢慢看一下Lambda表达式的相关知识。

  • 标准的Lambda表达式写法

那么Lambda表达式具体如何编写呢?下面我们可以看一个具体的Lambda表达式实例。

(int x, int y) -> {
    Log.i("TAG", "x:" + x + "  y:" + y);
    return x + y;
}

这是一个标准的Lambda表达式的写法,一个Lambda表达式通常有三部分组成:

  • 参数:(int a, int b)是这个lambda expression的参数部分,包括参数类型和参数名

  • 箭头:->

  • 代码块:就是用”{}”包含着的那两句代码。

其中由于参数的类型是可以通过系统上下文推断出来的,所以在很多情况下,参数的类型是可以省略的,可以省略的写成:

(x, y) -> {
    Log.i("TAG", "x:" + x + "  y:" + y);
    return x + y;
}

其实不光参数类型是可以省略的,代码块也是可以省略的,比如如果我们的代码块中只有一句代码,我们可以写成:

(x, y) -> 
    return x + y;

也可以写成:

(x, y) -> return x + y

而这个时候其实return关键字也是可以省略的,这时候我们就可以这样写:

(x, y) -> x + y

好精简有木有…

  • Lambda表达式的几个特性

(1)类型推导
编译器负责推导Lambda表达式的类型。它利用Lambda表达式所在上下文所期待的类型进行推导, 这个被期待的类型被称为目标类型。就是说我们传入的参数可以无需写类型了!

(2)变量捕获
对于lambda表达式和内部类, 我们允许在其中捕获那些符合有效只读(Effectively final)的局部变量。简单的说,如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求, 换句话说,加上final后也不会导致编译错误的局部变量就是有效只读变量

(3)方法引用
如果我们想要调用的方法拥有一个名字,我们就可以通过它的名字直接调用它:

Comparator byName = Comparator.comparing(Person::getName); 

此处无需再传入参数,Lambda会自动装配成Person类型进来然后执行getName()方法,而后返回getName()的String

方法引用有很多种,它们的语法如下:

静态方法引用:ClassName::methodName
实例上的实例方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName
类型上的实例方法引用:ClassName::methodName
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new

  • 学习使用Lambda表达式
/**
 * 定义线程测试例子
 */
public static void m1() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.i("TAG", "定义一个简单测试例子...");
            }
        }).start();
    }

以上是我们使用的普通的Java代码实现的一个简单的线程例子,那么如果使用Lambda表达式是什么形式的呢?

/**
 * 使用Lambda表达式实现
 */
public static void m2() {
        new Thread(
                () -> {
                    Log.i("TAG", "使用Lambda表达式的例子...");
                }
        ).start();
    }

可以看到通过Lambda表达式代码还是相当简洁的

(1)我们直接在编辑器里面写Lambda表达式是会报错的,因为Lambda不可以这样使用。在关于Lambda的使用其实需要跟一个叫做函数接口(Functional Interface)的东西绑定在一起。什么是函数接口呢?函数接口是在Java8 中引入的概念,其就是为了Lambda表达式而引入的。我们知道Java中的接口的概念,而函数接口其实就是:

一个只定义了一个抽象方法的接口

比如ClickListener这个接口就只有一个onClick方法,那么它就是一个函数接口。在Android开发过程中我们经常会这样使用OnClickListener:

/**
 * 定义OnClickListener,处理按钮点击事件
 */
View.OnClickListener onClickListener = newView.OnClickListener() {    
    @Override
    public void onClick(View view) {
        // 处理按钮点击事件
        doSomeThing();
    }
});
findViewById(R.id.titleView).setOnClickListener(onClickListener);

其实我们除了上面定义的OnClickListener我们也可以直接使用匿名内部类:

/**
 * 定义匿名内部类处理组件的点击事件
 */
findViewById(R.id.titleView).setOnClickListener(new View.OnClickListener() {    
    @Override
    public void onClick(View view) {
        // 处理组件的点击事件
        doSomeThing();
    }
});

在上面的代码中,我们可以发现其实我们主要是调用其他的doSomeThing()方法,该方法中实现了我们的按钮点击事件,并且这里我们通过一系列的缩进,括号来实现了这个调用操作,有木有觉得很繁琐?在Java 8出现之前,在代码中经常有这样的实现。现在好了,有了Lambda达表示,我们可以这样写了:

/**
 * 自定义OnClickListener按钮点击事件
 */
View.OnClickListener onClickListener = view -> doSomeThing();
findViewById(R.id.titleView).setOnClickListener(onClickListener);

至于匿名内部类我们也可以这样写:

findViewById(R.id.titleView).setOnClickListener(view -> doSomeThing());

通过使用Lambda表达式之后代码就变得相当简洁了。从上面的例子可以看到,Lambda表达式简化了函数接口的实例的创建,可以在代码层次简化函数接口,内部匿名类的写法等。

  • Lambda表达式的优缺点

上面我们简单的介绍了Lambda表达式的概念,写法与特性等,那么它具体有什么优缺点呢?

优点:

  • 使用Lambda表达式可以极大的简化代码。去除了很多无用的Java代码,使得代码更为简洁明了;

缺点:

  • 可读性差。在代码简洁的情况下,另一方面又让大多程序员很难读懂。因为很少程序员接触使用它。

  • 如何在Android Studio中使用Lambda表达式

Android Studio默认使用Lambda表达式是会报错的,即使你使用的是Java 8,升级Android Studio的Language level为1.8

这里写图片描述

如果是非Java8,那么我们如何使用Lambda表达式呢?

幸好有Lambda的gradle插件gradle-retrolambda,通过这个插件我们可以在Android Studio中使用Lambda表达式了,其可以兼容到Java5。

至于具体的使用方式我们可以参考其Github地址:gradle-retrolambda

具体如:

/**
 * 自定义组件点击事件
 */
imageView.setOnClickListener { view ->
            Log.i("TAG", "按钮点击事件...")
        }

总结:

相对来说使用Lambda表达式还是能够优化一些代码的,但是相应的代码的可能性会有相应的下降,在实际的开发过程中可根据具体的情况作出相应的选择。

作者:qq_23547831 发表于2016/10/8 9:11:50 原文链接
阅读:56 评论:0 查看评论

RN开发搭建开环境

$
0
0

第一步:
安装JDK
第二步:
安装安卓SDK,在有墙的环境下,可以选择使用http://androiddevtools.cn/
第三步:
安装C++环境,可以选择windows sdk、cygwin或者mingw等其他的C++环境。
编译node.js的C++模块的时候需要用到。
第四步:
安装node.js和Git
建议设置npm镜像以加速后面的过程,设置全局的指定的镜像:
npm config set registry https://registry.npm.taobao.org
npm config set disturl https://npm.taobao.org/dist
第五步:
安装React Native命令行工具
npm install -g react-native-cli,注意这样下载安装可能比较慢
可以直接去github上下载(搜索react native下载facebook的开源项目)
第六步:创建项目
react-native init MyProject
第七步:运行packager,进入工程目录
react-native start
可以用浏览器访问 http://localhost:8081/index.android.bundle?platform=android
查看是否可以看到打包后的脚本
第八步:使用模拟器或者真机 运行项目
react-native run-android 必须进入到项目执行该命令




目标平台: iOS Android 开发平台: Mac Linux Windows

译注:如果阅读完本文档后还碰到很多环境搭建的问题,我们建议你还可以再看看由本站提供的环境搭建视频教程windows环境搭建文字教程、以及常见问题

安装

必需的软件

Chocolatey

Chocolatey是一个Windows上的包管理器,类似于linux上的yum和 apt-get。 你可以在其官方网站上查看具体的使用说明。一般的安装步骤应该是下面这样:

@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin

一般来说,使用Chocolatey来安装软件的时候,需要以管理员的身份来运行命令提示符窗口。

Python 2

打开命令提示符窗口,使用Chocolatey来安装Python 2.

注意目前不支持Python 3版本。

choco install python2

Node

打开命令提示符窗口,使用Chocolatey来安装NodeJS.

choco install nodejs.install

React Native命令行工具(react-native-cli)

React Native的命令行工具用于执行创建、初始化、更新项目、运行打包服务(packager)等任务。

npm install -g react-native-cli

如果你遇到EACCES: permission denied权限错误,可以尝试运行下面的命令: sudo npm install -g react-native-cli.

Android Studio

Android Studio 2.0 or higher.

React Native目前需要Android Studio2.0或更高版本。

Android Studio需要Java Development Kit [JDK] 1.8或更高版本。你可以在命令行中输入 javac -version来查看你当前安装的JDK版本。如果版本不合要求,则可以到 官网上下载。 或是使用包管理器来安装(比如choco install jdk8或是 apt-get install default-jdk

Android Studio包含了运行和测试React Native应用所需的Android SDK和模拟器。

除非特别注明,请不要改动安装过程中的选项。比如Android Studio默认安装了 Android Support Repository,而这也是React Native必须的(否则在react-native run-android时会报appcompat-v7包找不到的错误)。

  • 确定所有安装都勾选了,尤其是Android SDKAndroid Device Emulator

  • 在初步安装完成后,选择Custom安装项:

custom installation

  • 检查已安装的组件,尤其是模拟器和HAXM加速驱动。

verify installs

  • 安装完成后,在Android Studio的欢迎界面中选择Configure | SDK Manager

configure sdk

  • SDK Platforms窗口中,选择Show Package Details,然后在Android 6.0 (Marshmallow)中勾选Google APIsIntel x86 Atom System ImageIntel x86 Atom_64 System Image以及Google APIs Intel x86 Atom_64 System Image

platforms

  • SDK Tools窗口中,选择Show Package Details,然后在Android SDK Build Tools中勾选Android SDK Build-Tools 23.0.1。(必须是这个版本)

build tools

ANDROID_HOME环境变量

确保ANDROID_HOME环境变量正确地指向了你安装的Android SDK的路径。

打开控制面板 -> 系统和安全 -> 系统 -> 高级系统设置 -> 高级 -> 环境变量 -> 新建

具体的路径可能和下图不一致,请自行确认。

env variable

你需要关闭现有的命令符提示窗口然后重新打开,这样新的环境变量才能生效。

推荐安装的工具

Gradle Daemon

开启Gradle Daemon可以极大地提升java代码的增量编译速度。

(if not exist "%USERPROFILE%/.gradle" mkdir "%USERPROFILE%/.gradle") && (echo org.gradle.daemon=true >> "%USERPROFILE%/.gradle/gradle.properties")

将Android SDK的Tools目录添加到PATH变量中

你可以把Android SDK的tools和platform-tools目录添加到PATH变量中,以便在终端中运行一些Android工具,例如android avd或是adb logcat等。

打开控制面板 -> 系统和安全 -> 系统 -> 高级系统设置 -> 高级 -> 环境变量 -> 选中PATH -> 双击进行编辑

注意你的具体路径可能和下图不同

env variable

可选的安装项

Git

你可以使用Chocolatey来安装git:

choco install git

另外你也可以直接去下载Git for Windows。 在安装过程中注意勾选"Run Git from Windows Command Prompt",这样才会把git命令添加到PATH环境变量中。

Genymotion

比起Android Studio自带的原装模拟器,Genymotion是一个性能更好的选择,但它只对个人用户免费。

  1. 下载和安装Genymotion(译注:不要被里面的价格唬住了,个人免费的链接可能不明显,请仔细寻找!另外,genymotion需要依赖VirtualBox虚拟机,下载选项中提供了包含VirtualBox和不包含的选项,请按需选择)。
  2. 打开Genymotion。如果你还没有安装VirtualBox,则此时会提示你安装。
  3. 创建一个新模拟器并启动。
  4. 启动React Native应用后,可以按下F1来打开开发者菜单。

Visual Studio Emulator for Android

Visual Studio Emulator for Android)是利用了Hyper-V技术进行硬件加速的免费android模拟器。也是Android Studio自带的原装模拟器之外的一个很好的选择。而且你并不需要安装Visual Studio。

在用于React Native开发前,需要先在注册表中进行一些修改:

  1. 打开运行命令(按下Windows+R键)
  2. 输入regedit.exe然后回车
  3. 在注册表编辑器中找到HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Android SDK Tools条目
  4. 右键点击Android SDK Tools,选择新建 > 字符串值
  5. 名称设为Path
  6. 双击Path,将其值设为你的Android SDK的路径。(例如C:\Program Files\Android\sdk

测试安装

react-native init AwesomeProject
cd AwesomeProject
react-native run-android

手动运行Packager

有个常见的问题是在你运行react-native run-android命令后,Packger可能不会自动运行。此时你可以手动启动它:

cd AwesomeProject
react-native start

如果你碰到了ERROR Watcher took too long to load的报错,请尝试将这个文件中的MAX_WAIT_TIME值改得更大一些 (文件在node_modules/react-native/目录下)。

修改项目

现在你已经成功运行了项目,我们可以开始尝试动手改一改了:

  • 使用你喜欢的文本编辑器打开index.android.js并随便改上几行
  • 按两下R键,或是用Menu键(通常是F2,在Genymotion模拟器中是⌘+M)打开开发者菜单,然后选择 Reload JS 就可以看到你的最新修改。
  • 在终端下运行adb logcat *:S ReactNative:V ReactNativeJS:V可以看到你的应用的日志。
作者:u013749540 发表于2016/10/8 9:21:26 原文链接
阅读:45 评论:0 查看评论

Android 高级自定义Toast及源码解析

$
0
0

Toast概述

Toast的作用

不需要和用户交互的提示框。

更多参见官网:https://developer.android.com/guide/topics/ui/notifiers/toasts.html

Toast的简单使用

    Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷学习,日渐消瘦",Toast.LENGTH_SHORT).show()

自定义Toast

    Toast customToast = new Toast(MainActivity.this.getApplicationContext());
    View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);
    ImageView img = (ImageView) customView.findViewById(R.id.img);
    TextView tv = (TextView) customView.findViewById(R.id.tv);
    img.setBackgroundResource(R.drawable.daima);
    tv.setText("沉迷学习,日渐消瘦");
    customToast.setView(customView);
    customToast.setDuration(Toast.LENGTH_SHORT);
    customToast.setGravity(Gravity.CENTER,0,0);
    customToast.show();

布局文件中根元素为LinearLayout,垂直放入一个ImageView和一个TextView。代码就不贴了。

高级自定义Toast

产品狗的需求:点击一个Button,网络请求失败的情况下使用Toast的方式提醒用户。
程序猿:ok~大笔一挥。

Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷学习,日渐消瘦",Toast.LENGTH_SHORT).show()

测试:你这程序写的有问题。每次点击就弹出了气泡,连续点击20次,居然花了一分多钟才显示完。改!
程序猿:系统自带的就这样。爱要不要。
测试:那我用单元测试模拟点击50次之后,它就不显示了,这个怎么说。
程序猿:…
这个时候,高级自定义Toast就要出场了~

activity_main.xml—->上下两个按钮,略。

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    public static final String TAG = "MainActivity";
    private Button customToastBtn;
    private Button singleToastBtn;
    private static int num;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initClick();
        performClick(100);

    }

    private void initView() {
        customToastBtn = (Button) findViewById(R.id.customToastBtn);
        singleToastBtn = (Button) findViewById(R.id.singleToastBtn);
    }

    private void initClick() {
        customToastBtn.setOnClickListener(this);
        singleToastBtn.setOnClickListener(this);
    }

    /**
     * 点击singleToastBtn按钮
     * @param clickFrequency 点击的次数
     */
    private void performClick(int clickFrequency) {
        for (int i = 0; i < clickFrequency; i++){
            singleToastBtn.performClick();
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.customToastBtn:
                showCustomToast();
                break;
            case R.id.singleToastBtn:
                showSingleToast();
                break;
            default:break;
        }
    }

    private void showCustomToast() {
        Toast customToast = new Toast(MainActivity.this.getApplicationContext());
        View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);
        ImageView img = (ImageView) customView.findViewById(R.id.img);
        TextView tv = (TextView) customView.findViewById(R.id.tv);
        img.setBackgroundResource(R.drawable.daima);
        tv.setText("沉迷学习,日渐消瘦");
        customToast.setView(customView);
        customToast.setDuration(Toast.LENGTH_SHORT);
        customToast.setGravity(Gravity.CENTER,0,0);
        customToast.show();
    }

    private void showSingleToast() {
        Toast singleToast = SingleToast.getInstance(MainActivity.this.getApplicationContext());
        View customView = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_toast,null);
        ImageView img = (ImageView) customView.findViewById(R.id.img);
        TextView tv = (TextView) customView.findViewById(R.id.tv);
        img.setBackgroundResource(R.drawable.daima);
        tv.setText("沉迷学习,日渐消瘦 第"+num+++"遍 toast="+singleToast);
        singleToast.setView(customView);
        singleToast.setDuration(Toast.LENGTH_SHORT);
        singleToast.setGravity(Gravity.CENTER,0,0);
        singleToast.show();
    }
}

SingleToast.java

public class SingleToast {

    private static Toast mToast;

    /**双重锁定,使用同一个Toast实例*/
    public static Toast getInstance(Context context){
        if (mToast == null){
            synchronized (SingleToast.class){
                if (mToast == null){
                    mToast = new Toast(context);
                }
            }
        }
        return mToast;
    }
}

那么有的同学会问了:你这样不就是加了个单例吗,好像也没有什么区别。区别大了。仅仅一个单例,既实现了产品狗的需求,又不会有单元测试快速点击50次的之后不显示的问题。为什么?Read The Fucking Source Code。

Toast源码解析

这里以Toast.makeText().show为例,一步步追寻这个过程中源码所做的工作。自定义Toast相当于自己做了makeText()方法的工作,道理是一样一样的,这里就不再分别讲述了~

源码位置:frameworks/base/core/java/android/widght/Toast.java
Toast#makeText()

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        // 获取Toast对象
        Toast result = new Toast(context);
        LayoutInflater inflate = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);    
        // 填充布局
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);
        // 设置View和duration属性
        result.mNextView = v;
        result.mDuration = duration;
        return result;
    }

这里填充的布局transient_notification.xml位于frameworks/base/core/res/res/layout/transient_notification.xml。加分项,对于XML布局文件解析不太了解的同学可以看下这篇博客

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="?android:attr/toastFrameBackground">

    <TextView
        android:id="@android:id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_gravity="center_horizontal"
        android:textAppearance="@style/TextAppearance.Toast"
        android:textColor="@color/bright_foreground_dark"
        android:shadowColor="#BB000000"
        android:shadowRadius="2.75"
        />

</LinearLayout>

可以发现,里面只有一个TextView,平日设置的文本内容就是在这里展示。接下来只有一个show()方法,似乎我们的源码解析到这里就快结束了。不,这只是个开始

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;
        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

这里有三个问题。
1. 通过getService()怎么就获得一个INotificationManager对象?
2. TN类是个什么鬼?
3. 方法最后只有一个service.enqueueToast(),显示和隐藏在哪里?

Toast的精华就在这三个问题里,接下来的内容全部围绕上述三个问题,尤其是第三个。已经全部了解的同学可以去看别的博客了~

1. 通过getService()怎么就获得一个INotificationManager对象?

    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

Binder机制了解的同学看见XXX.Stub.asInterface肯定会很熟悉,这不就是AIDL中获取client嘛!确实是这样。

tips: 本着追本溯源的精神,先看下ServiceManager.getService("notification")。在上上上上篇博客SystemServer启动流程源码解析startOtherServices()涉及到NotificationManagerService的启动,代码如下,这里不再赘述。

mSystemServiceManager.startService(NotificationManagerService.class);

ToastAIDL对应文件的位置。

源码位置:frameworks/base/core/java/android/app/INotificationManager.aidl

Server端:NotificationManagerService.java
源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

篇幅有限,这里不可能将AIDL文件完整的叙述一遍,不了解的同学可以理解为:经过进程间通信(AIDL方式),最后调用NotificationManagerService#enqueueToast()。具体可以看下这篇博客

2. TN类是个什么鬼?

Toast#makeText()中第一行就获取了一个Toast对象

    public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }

源码位置:frameworks/base/core/java/android/widght/Toast$TN.java

    private static class TN extends ITransientNotification.Stub {
        ...
        TN() {
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            ...
        }
        ...
    }

源码中的进程间通信实在太多了,我不想说这方面的内容啊啊啊~。有时间专门再写一片博客。这里提前剧透下TN类除了设置参数的作用之外,更大的作用是Toast显示与隐藏的回调。TN类在这里作为Server端。NotificationManagerService$NotificationListeners类作为client端。这个暂且按下不提,下文会详细讲述。

3. show()方法最后只有一个service.enqueueToast(),显示和隐藏在哪里?

源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

    private final IBinder mService = new INotificationManager.Stub() {

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }
            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
            ...
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index = indexOfToastLocked(pkg, callback);
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i<N; i++) {
                                 final ToastRecord r = mToastQueue.get(i);
                                 if (r.pkg.equals(pkg)) {
                                     count++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        // 将Toast所在的进程设置为前台进程
                        keepProcessAliveLocked(callingPid);
                    }
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }
        ...
    }

Toast#show()最终会进入到这个方法。首先通过indexOfToastLocked()方法获取应用程序对应的ToastRecordmToastQueue中的位置,Toast消失后返回-1,否则返回对应的位置。mToastQueue明明是个ArratList对象,却命名Queue,猜测后面会遵循“后进先出”的原则移除对应的ToastRecord对象~。这里先以返回index=-1查看,也就是进入到else分支。如果不是系统程序,也就是应用程序。那么同一个应用程序瞬时mToastQueue中存在的消息不能超过50条(Toast对象不能超过50个)。否则直接return。这也是上文中为什么快速点击50次之后无法继续显示的原因。既然瞬时Toast不能超过50个,那么运用单例模式使用同一个Toast对象不就可以了嘛?答案是:可行。消息用完了就移除,瞬时存在50个以上的Toast对象相信在正常的程序中也用不上。而且注释中也说这样做是为了放置DOS攻击和防止泄露。其实从这里也可以看出:为了防止内存泄露,创建Toast最好使用getApplicationContext,不建议使用ActivityService等。

回归主题。接下来创建了一个ToastRecord对象并添加进mToastQueue。接下来调用showNextToastLocked()方法显示一个Toast

源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
NotificationManagerService#showNextToastLocked()

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                record.callback.show();
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

这里首先调用record.callback.show(),这里的record.callback其实就是TN类。接下来调用scheduleTimeoutLocked()方法,我们知道Toast显示一段时间后会自己消失,所以这个方法肯定是定时让Toast消失。跟进。

    private void scheduleTimeoutLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }  

果然如此。重点在于使用mHandler.sendMessageDelayed(m, delay)延迟发送消息。这里的delay只有两种值,要么等于LENGTH_LONG,其余统统的等于SHORT_DELAYsetDuration为其他值用正常手段是没有用的(可以反射,不在重点范围内)。
handler收到MESSAGE_TIMEOUT消息后会调用handleTimeout((ToastRecord)msg.obj)。跟进。

    private void handleTimeout(ToastRecord record)
    {
        if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

啥也不说了,跟进吧~

    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            ...
        }
        mToastQueue.remove(index);
        keepProcessAliveLocked(record.pid);
        if (mToastQueue.size() > 0) {
            showNextToastLocked();
        }
    }

延迟调用record.callback.hide()隐藏Toast,前文也提到过:record.callback就是TN对象。到这,第三个问题已经解决一半了,至少我们已经直到Toast的显示和隐藏在哪里被调用了,至于怎么显示怎么隐藏的,客观您接着往下看。

源码位置:frameworks/base/core/java/android/widght/ToastTN.javaToastTN#show()

        final Handler mHandler = new Handler(); 

        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

注意下这里直接使用new Handler获取Handler对象,这也是为什么在子线程中不用Looper弹出Toast会出错的原因。跟进handleShow()

        public void handleShow() {
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ...
                mParams.packageName = packageName;
                if (mView.getParent() != null) {
                    mWM.removeView(mView);
                }
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

原来addViewWindowManager。这样就完成了Toast的显示。至于隐藏就更简单了。

        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }

                mView = null;
            }
        }

直接remove掉。

题外话

今天周末,一天时间完成这篇简单的源码阅读加写作。每次写完源码解析总是成就感伴随着失落感, 成就感来源于我又get到一个原理或者新技能,失落感来自源码也就是那么回事,但是回头想想我得到了什么?其实并不多。但我仍然在乐此不疲的追寻着。或许是我还没“开窍”,没有到那种融会贯通的境界。但我清楚的知道,我在进步。我在努力变的更加优秀。


更多Framework源码解析,请移步 Android6.0 Framework源码解析系列[目录]

作者:qq_17250009 发表于2016/10/8 9:27:41 原文链接
阅读:186 评论:0 查看评论

用Xamarin和Visual Studio编写iOS App

$
0
0

原文:Building iOS Apps with Xamarin and Visual Studio
作者:(Bill Morefield](https://www.raywenderlich.com/u/bmorefield)
译者:kmyhy

一说开发 iOS app,你立马就会想到苹果的开发语言 Objective C/Swift 和 Xcode。但是,这并不是唯一的选择,我们完全可以使用别的语言和框架。

一种主流的替换方案是 Xamarin,这是一个跨平台框架,允许你开发 iOS、Android 和 OSX、Windows app,它使用的是 C# 和 Visual Studio。最大的好处在于,Xamarin 允许你在 iOS 和 Android app 间共享代码。

Xamarin 与其他跨平台框架相比有一个最大的好处:使用 Xamarin,你的项目能够编译出本地代码,并使用本地 APIs。也就是说,用 Xamarin 编写的 app 和用 Xcode 编出来的 app 毫无区别。更多细节,请阅读这篇文章Xamarin vs. Native App Development

但是 Xamarin 还有一个巨大的缺点,就是它的价格。每个平台 1000 美元/年的价格,可能要让你戒掉每天都喝的拿铁或法布奇诺才能负担得起……程序员如果不喝咖啡是件很危险的事情。因为高昂的价格,Xamarin 至今只在预算丰沛的企业项目中才会采用。

但是,自从微软收购 Xamarin 之后这种情况发生了改变,微软将它集成到新版的 Visual Studio 中,甚至是免费的社区版。而社区版对个人开发者和小团队是免费的。

免费?真是太好了!

除了价格以外(或者根本不考虑价格),Xamarin 还拥有其它好处,包括允许程序员:

  • 利用原有的 c# 库和工具编写手机 app。
  • 在不同平台的 app 之间共用代码。
  • 在 ASP.net 后台和前端 app 之间共用代码。

Xamarin 还允许你根据你的需求改变工具。如果想最大化地跨平台共用代码,请使用 Xamarin Forms,它非常适合于不针对特定平台的特性或者需要单独定制 UI 的 app。

如果你的 app 需要调用针对特定平台的功能或界面,请使用 Xamarin.iOS、Xamarin.Android 或其他平台的模块,这样就可以直接调用本地 API 和框架。这些模块能够创建高度定制的 UI,同时在通用代码上支持跨平台。

在本教程中,你将使用 Xamarin.iOS 创建 iPhone app,这个 app 用于列出用户的照片库。

本教程不需要任何 iOS 或 Xamarin 开发经验,但最好具备 C# 基础。

开始

要用 Xamarin 和 Visual Studio 开发 iOS app,理想的情况下你需要两台电脑:

  • 一台 Windows 电脑,用于运行 Visual Studio,并编写代码。
  • 一台装有 Xcode 的 Mac 电脑,用于充当 buid 主机。并不是专门用它来进行编译,而是在开发和调试过程中,用它来接受自 Windows 电脑发出网络请求。

两台电脑的物理距离越近越好,因为当你在 windows 电脑上编译和运行时,iOS 模拟器会在 Mac 电脑上运行。

你可能会问“如果我没有两台电脑怎么办?”

  • 对于只使用 Mac 平台的用户,Xamarin 也提供了 OSX 下的 IDE,但本教程主要目的是演示全新的 Visual Studio。如果你不喜欢这样,你可以在 Mac 上跑一个 Windows 虚拟机。VMWare Fushion 或者免费开源的 VirtualBox 都可以。

    如果使用 Windows 虚拟机,你需要保证 Windows 能够通过网络访问 Mac。也就是说,你需要在 Windows 下 Ping 到 Mac 的 IP 地址。

  • 对于纯 Windows 用户,那么现在、马上去买一台 Mac。我在这里等你!如果不行,就使用 MacinCloud 或 Macminicolo 之类的云服务吧。

本教程假设你有单独的 Mac 和 Windows 电脑,当然,对于在使用 Mac 下使用 Windows 主机的人来说,本教程也是适用的。

安装 Xcode 和 Xamarin

下载、安装 Xcode 到你的 Mac 电脑上,如果你还没有这样做的话。这个和从应用商店安装其他 app 并无不同,只不过有好几 G 大,需要的时间长一点。

装完 Xcode 之后,下载 Xamarin Studio 到 Mac 电脑上。不用填写 email 地址,下载是免费的。插一句:是不是觉得很爽——这不需要牺牲你的任何一种咖啡为代价!

下载完成后后,打开安装包,双击 Install Xamarin.app。接收协议条款,点击 continue。

安装器会自动找到已经安装的工具并检查当前操作系统版本。它会显示一个开发环境列表。勾上 Xamarin.iOS,点击 continue。

然后你会看到一个确认清单,列出了将要安装的内容。点击 continue。然后你会看到一个概要,以及一个启动 Xamarin Studio 的选项。直接点 Quit 完成安装。

安装 Visual Studio 和 Xamarin

在本教程中,你可以使用任意版本的 Visual Studio,甚至是免费的社区版。社区版的功能并不完全,但完全不影响你开发复杂的 app。

你的 Windows 当你必须满足 Visual Studio 的最小系统需求。要获得比较顺畅的开发体验,至少需要 3 GB 的内存。

如果你没有安装过 Visual Studio,你可以从社区版网页上点击绿色的社区版2015按钮,下载社区版的安装器。

运行安装器,开始安装,选择定制安装选项。在特性列表中,展开跨平台手机开发,然后选择 C#/.NET(Xamarin v4.0.3)(本教程编写时的版本,很可能会有不同)。

点击 Next,等待安装完成。时间有点长,你可以站起来走一下,消化掉安装 Xcode 时吃的饼干 :]

如果你已经装过 Visual Studio 但没有 Xamarin tools,进入 Windows 的 Programs and Features,找到 Visual Studio 2015,选择它,点击 Change,然后选择 Modify。

在 Cross Platform Mobile Development 下面找到 Xamarin,即 C#/.NET (Xamarin v4.0.3),勾选它,点击 Upate 以进行安装。

呼——装的东西真多,但总算是搞定了!

编写 app

打开 Visual Studio,选择 File\New\Project。在 Visual C# 下面,展开 iOS,选择 iPhone -> Single View App 模板。这个模板创建一个只有一个 view controller 的 app,view controller 是 iOS app 中用于管理视图的类。

无论 Name 还是 Solution Name,都请输入 ImageLocation。选择项目保存路径,然后点击 OK,新项目就创建好了。

Visual Studio 会提示你需要指定一台 Mac 电脑作为 Xamarin 的 buid 主机:

  1. 在 Mac 电脑上,打开系统偏好设置,选择共享。
  2. 打开远程登录。
  3. 将“允许访问”改为“仅这些用户”,然后添加将用于访问 Mac 上的 Xamarin 和 Xcode 的用户。

  4. 关闭窗口,返回 Windows 电脑。

回到 Visual Studio,在要求你指定一台 Mac 作为 build 主机时,选择你的 Mac 电脑,然后点击 Connect。输入用户名密码,点击 login。

你可以查看工具栏,检查是否已经连接成功。

从 Solution Platform 下拉框中选择 iPhone Simulator,这将自动选择 build 主机的一个模拟器。如果要改变为其他模拟器,可以点击当前模拟器右边的小箭头。

按下绿色的 Debug 箭头或者 F5 快捷键,编译运行程序。

编译完成后,你却不能在 Windows 上看到任何效果。因为它运行在你的 Mac (build 主机) 上。这就是为什么最好将两台机器尽量靠近的理由!

在前几天的 Evolve 大会上,Xamarin 宣布将推出 iOS Simulator Remoting ,它能够让你和运行在苹果 iOS 模拟器上的 app 进行交互,就像在 Windows PC 上运行了一个模拟器一样。但在目前,你仍然需要和运行在 Mac 上的模拟器打交道。

在模拟器上,你会看到一个启动画面闪现,然后显示一个空的窗口。恭喜你!你的 Xamarin 能够正常工作了。

要停止 app,可以点击红色的 Stop 按钮(Shift+F5 快捷键)。

创建 Collection View

这个 app 会显示给用户一个 Collection View,以展示用户相册中的缩略图片。Collection View 是一个 iOS 控件,以网格形式显示多个条目。

要编辑 app 故事板中的“场景”,请从解决方案管理器中打开 Main.storyboard。

在工具箱中的搜索栏中,输入 collection 字样进行过滤。将 Data View 下面的 Collection View 对拖到空白的视图中央。

选择 Collection View,你会看到它四周出现一些空心的小圆圈。如果你看到的是 T 字而不是小圆圈,请再点它一次,即可切换到小圆圈。

点击并拖动每个小圆圈直到看见蓝色线条后放开鼠标按键,控件的边缘就自动对齐到线条所在的地方。

然后设置 Collection View 的自动布局约束,自动布局约束用于告诉 app 当设备旋屏时视图应当如何重新改变大小。在故事板上边的工具栏中,点击 CONSTRAINTS 字样右边的绿色的加号按钮。这将自动为 Collection View 创建约束。

自动创建的约束大部分正确,但也需要对其中一些进行调整。在属性窗口中,切换到 Layout 标签,拉到 Constraints 一栏。

边距中的两个约束是正确的,但宽高约束不正确。删除 Width 和 Height 约束(点击它们右边的 X 按钮)。

注意,Collection View 此时变成橙色。这表明约束不正确。

点击 Collection View 以选中它。如果你看到之前一样的圆圈,再点击它一次切换到绿色的 T 字图标。点击并拖放 Collection View 上端的 T 字直到绿色的名为 Top Layout Guide 的外框处。放开鼠标左键,这将创建一条相对于视图顶部的约束。

然后,点击并向左拖放 Collection View 左边的 T 字直到看到一条蓝色的虚线。放开鼠标左键,这将创建一条相对于视图左边缘的约束。

这时,你的约束应该是这样的:

配置 Collection View Cell

看见 Collection View 中的小方块了吗?在这个方块中有一个红色的惊叹号。这就是一个 Collection View Cell,表示 Collection View 中的一个单元格。

要配置这个 cell 的大小,需要在 Collection View 中进行。选中 Collection View,上拉到 Layout 标签的顶部。在 Cell Size 小节,将其 Width 和 Height 设置为 100。

然后,点击 Collection View Cell 中的红色惊叹号,这将弹出一个提示窗口,说你还没有为 cell 分配一个 reuse identifier。因此,选中 cell,打开 Widget 标签,下拉到 Collection Reusable View 小节,将 Identifier 设置为 ImageCellIdentifier。这将让这个错误消失。

继续下拉,来到 Interaction 小节,将 Background Color 设置为 Predefined 中的蓝色。

现在,场景效果变成:

上拉到 Widget 节顶部,将 Class 设置为 PhotoCollectionImageCell。

Visual Studio 会自动创建同一类名的类,继承 UICollectionViewCell,并自动创建一个 PhotoCollectionImageCell.cs 文件。唉,什么时候 Xcode 才能和 Visual Studio 一样?!

创建 Colleciton View 数据源

你还需要创建一个类,充当 UICollectionViewDataSource,为 Collection View 提供数据。

在解决方案管理器的 ImageLocation 上右击,选择 Add \ Class, 类名为 PhotoCollectionDataSource.cs 然后点击 Add。

打开新创建的 PhotoCollectionDataSource.cs,然后在文件顶部写入:

using UIKit;

这将导入 iOS UIKit 框架。

然后是类定义:

public class PhotoCollectionDataSource : UICollectionViewDataSource
{
}

还记得你为 Collection View Cell 定义的 reuse identifier 吗?在这里我们将会用到它。在类定义中加入:

private static readonly string photoCellIdentifier = "ImageCellIdentifier";

UICollectionViewDataSource 类中有两个抽象方法必须实现。在类中加入:

public override UICollectionViewCell GetCell(UICollectionView collectionView, 
    NSIndexPath indexPath)
{
    var imageCell = collectionView.DequeueReusableCell(photoCellIdentifier, indexPath)
       as PhotoCollectionImageCell;

    return imageCell;
}

public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
    return 7;
}

GetCell() 方法负责提供一个用于在 Collection View 中显示的 cell。

DequeueReusableCell 方法会重用那些不再使用的 cell,例如那些已经不需要在屏幕上显示的 cell,然后返回该 cell。如果没有可重用的 cell,则会创建一个新的 cell。

GetItemsCount 方法负责告诉 Collection View 需要显示多少(7 个) cell。

然后需要在 ViewController 类中添加一个 Collection View 的引用,ViewController 就是管理着 Collection View 的那个 Scene。回到 Main.storyboard,选择 Collection View,来到 Widget 标签,将 Name 设为 collectionView。

Visual Studio 会自动在 ViewController 类中创建一个名为 collectionView 的实例变量。

注意,在 ViewController.cs 中你无法看到这个实例变量。要看到这个变量,你需要点击 ViewController.cs 左边的右箭头,以打开 ViewController.designer.cs。这里才看得见 Visual Studio 为你创建的实例变量。

从解决方案管理器中打开 ViewController.cs,在类中添加如下字段:

private PhotoCollectionDataSource photoDataSource;

在 ViewDidLoad()最后,添加代码,初始化数据源并将它绑定到 Collection View:

photoDataSource = new PhotoCollectionDataSource();
collectionView.DataSource = photoDataSource;

这样,photoDataSource 就能够为 Collection View 提供数据了。

编译运行程序。你会看到 Collection View 显示了 7 个蓝色方块。

好极了—— 一切顺利!

显示照片

蓝色方块搞定了,接下来是将数据源变成从设备中获取的图片,然后在 Collection View 中显示它们。你将用 Photos 框架访问来自 Photos app 的照片、视频。

接下来,你需要在 cell 中加入一个 Image View。打开 Main.stroyboard,选择 Collection View Cell。在 Widget 标签,下拉并设置 Background Color 为默认。

在工具箱中,搜索 image view,然后拖一个 Image View 到 cell 中。

Image View 的默认大小比 cell 大,要修改其大小,选择这个 Image View 然后在 Properties \ Layout 标签的 View 小节下面,将 X 和 Y 设为 0 ,Width 和 Height 设为 100。

切换到 Widget 标签,将 Name 设置为 cellImageView。Visual Studio 会自动创建一个名为 cellImageView 的变量。

拉到 View 小节,将 Mode 设为 Aspect Fill。这将防止图片被缩放。

注意:在 PhotoCollectionImageCell.cs 中无法看到 cellImageView 变量。这个类是分部类,这个变量在另外一个文件中。
在解决方案管理器中,点击 PhotoCollectionImageCell.cs 左边的箭头,展开它。打开 PhotoCollectionImageCell.designer.cs,你将看到 cellImageView 变量声明。

这个文件是自动创建的,不要去改变它。否则,它们会在你不知道的情况下被覆盖,或者导致类和故事板之间的绑定被打断,从而导致运行时错误。

这个变量不是公有的,因此别的类无法访问它。因此,你需要提供一个访问它的方法,以便我们能够改变 Image View 上显示的图片。

打开 PhotoCollectionImageCell.cs 添加如下方法:

public void SetImage(UIImage image)
{
    cellImageView.Image = image;
}

现在你可以让 PhotoCollectionDataSource 去抓取照片了。

在 PhotoCollectionDataSource.cs 的顶部:

using Photos;

在 PhotoCollectionDataSource 增加变量:

private PHFetchResult imageFetchResult;
private PHImageManager imageManager;

imageFetchResult 变量用于存储照片对应 Asset 的数组,然后通过 imageManager 对象来获取照片数据。
在 GetCell() 方法前,添加构造方法:

public PhotoCollectionDataSource()
{
    imageFetchResult = PHAsset.FetchAssets(PHAssetMediaType.Image, null);
    imageManager = new PHImageManager();
}

这个构造方法从 Photos app 中抓取所有图片资源,并将结果放到 imageFetchResult 变量中。然后初始化 imageManager,app 用它来查询每一张照片的具体数据。

在构造方法下面,添加析构方法,将 imageManager 对象释放:

~PhotoCollectionDataSource()
{
    imageManager.Dispose();
}

在 GetItemsCount 和 GetCell 方法中用新数据源中的图片替换原来的空 cell。修改 GetItemsCount() 方法为:

public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
    return imageFetchResult.Count;
}

修改 GetCell 方法为:

public override UICollectionViewCell GetCell(UICollectionView collectionView, 
    NSIndexPath indexPath)
{
    var imageCell = collectionView.DequeueReusableCell(photoCellIdentifier, indexPath) 
        as PhotoCollectionImageCell;

    // 1
    var imageAsset = imageFetchResult[indexPath.Item] as PHAsset;

    // 2
    imageManager.RequestImageForAsset(imageAsset, 
        new CoreGraphics.CGSize(100.0, 100.0), PHImageContentMode.AspectFill,
        new PHImageRequestOptions(),
         // 3
         (UIImage image, NSDictionary info) =>
        {
           // 4
           imageCell.SetImage(image);
        });

    return imageCell;
}

以上代码分别进行说明如下:

  1. indexPath 表明当前将返回哪一个 cell 。其 Item 属性表示了 cell 的索引。 我们根据这个索引获得图片资源并将之转换为 PHAsset 对象。
  2. 用 imageManager 对象去请求获取 PHAsset 所对应的图片,同时指定了所需图片的大小和缩放模式。
  3. 许多 iOS 框架中的方法都会在执行耗时任务时使用延迟执行,当任务完成时再调用委托方法。以 RequestImageForAsset 方法为例,当请求完成时,委托方法将被调用,所请求的图片和相关信息将通过参数传递到委托方法。
  4. 最后,设置 cell 中的图片。

编译运行。你会被询问需要访问权限。

如果你选择 OK,app 什么也不会显示。搞毛啊!

iOS 认为照片属于用户的敏感信息,需要经过用户授权。但是当用户同意授权之后, app 也必须注册接收相应的通知,以便重新刷新视图。也就是你接下来的工作。

注册照片访问授权通知

首先,你需要在 PhotoCollectionDataSource 类中增加一个方法以便当照片库内容发生改变后重新抓取数据。在类中加入以下方法:

public void ReloadPhotos()
{
    imageFetchResult = PHAsset.FetchAssets(PHAssetMediaType.Image, null);
}

然后,打开 ViewController.cs 导入 photos 框架:

using Photos;

在 ViewDidLoad() 方法中:

// 1
PHPhotoLibrary.SharedPhotoLibrary.RegisterChangeObserver((changeObserver) =>
{
    //2
    InvokeOnMainThread(() =>
    {
        // 3
        photoDataSource.ReloadPhotos();
        collectionView.ReloadData();
    });
});

上述代码负责:

  1. 将 app 注册为接收照片库改变通知,当照片库内容改变时调用指定代码。
  2. InvokeOnMainThread() 方法在主线程中刷新 UI,否则会导致 app 崩溃。
  3. 调用 photoDataSource.ReloadPhotos() 重新获取照片,调用 collectionView.ReloadData() 让 Collection View 重绘。

最后,我们来解决前面的问题,在 app 还没有得到相册访问权限时,请求用户授权。

在 ViewDidLoad() 方法中,在初始化 photoDataSource 之前加入:

if (PHPhotoLibrary.AuthorizationStatus == PHAuthorizationStatus.NotDetermined)
{
    PHPhotoLibrary.RequestAuthorization((PHAuthorizationStatus newStatus) =>
    { });
}

这里需要检查当前授权状态,如果用户未授权,提示用户进行授权。
为了再次提示用户授权,你需要通过 Simulator \ Reset Content and Settings 重置模拟器。

编译运行。你会看到照片访问授权的提示,如果你选择 OK,这个 app 会在 Collection View 中显示照片的缩略图!

结束语

你可以从这里下载完整的 Visual Studio 项目。

在本教程中,你学习了如何配置 Xamarin 以及如何用它来创建 iOS app。

Xamarin 指南网站 有几个优秀的学习资源。要了解更多关于创建跨平台 app 的内容,请查看 Xamarin 教程关于创建同一应用的 iOSAndroid app。

微软收购 Xamarin 后做出了一些令人赞叹的改变。 在微软的 Build 打回和 Xamarin Evolve 中你会看到这种倾向。Xamarin 发布了最近 Evolve 大会的会议视频,这些视频详细介绍了关于如何使用 Xamarin 的信息和未来的产品方向。

你会用 Xamarin 创建 app 吗?如果你对本文有任何问题建议,请在下面留言。

作者:kmyhy 发表于2016/10/8 9:27:52 原文链接
阅读:106 评论:0 查看评论

自适应表格单元格

$
0
0

原文:Self-sizing Table View Cells
作者:Bradley Johnson
译者:kmyhy

注:本文被 Bradley 升级为适用于 Xcode 7.3/iOS 9/Swift 2.2。原文作者为 Joshua Greene.

如果你之前使用过自定义单元格,你肯定也在代码中花费大量的精力实现过自适应单元格。你可能习惯于手动计算单元格中的每一样东西的高度,比如 Label、ImageView、TextField 等等。

坦率地说,这种方法非常复杂而且问题多多。

在本文中,你将学习如何创建自适应单元格并根据它的内容动态改变它的大小。你可能会想,“那太费事儿了…!”

你错了。:] 在 iOS 8 中,苹果使这个任务变得非常简单。

注:本文需要 Xcode 7.3 以上,以支持最新的 Swift 语法。本文假设你熟悉自动布局、UITableView 和 Swift 开发。
如果你是新手,你可以浏览本站的其他文章视频

开始

回忆起 iOS 6 的时候,苹果推出了一项神奇的技术:自动布局。开发者欢呼;在街头庆祝;为它的诞生大唱赞歌…

好吧,你可以说它是一个进步,但它仍然带来了一系列问题。

虽然它被开发者寄予了许多希望,但自动布局真的很难用。在 iOS 开发中,手动写出的自动布局代码,仍然是非常的晦涩难懂。而在 Interface Builder 中,创建布局约束一开始也是非常的低效。

回到今天,随着 Interface Builder 的升级和 iOS 8 的推出,用自动布局创建自适应表格单元格不在是件难事了!

撇开这些细枝末节不论,你所需要做的就是:

  • 创建单元格时,启用自动布局
  • 设置 table view 的 rowHeight 为 UITableViewAutomaticDimension
  • 设置 estimatedRowHeight 或者实现 height estimation 委托方法

等等,你现在不想听长篇大论,你只想看代码?那就让我们直接从项目开始吧。

示例 App 一览

假设你的大客户跟你说“我想在 app 上列出最伟大的已故画家以及他们最伟大的作品!”
“我们已经在做这个 app 了,但我们被一个问题难住了:如何在 table view 上显示内容?”这个客户说。“你能帮个忙吗?”

你突然觉得自己有一种想冲到最近的电话亭并披上斗篷的冲动。

当然,你不需要使用任何伎俩就有机会在客户面前充当英雄——你的编程技能足矣!

首先,下载“这个客户的代码”——Artistry-Starter——也就是本文的开始项目。解压缩 zip 包,用 Xcode 打开项目。

打开 Main.storyboard (在 Views 文件组下) ,你将看到有 3 个 scenes:

从左到右分别是:

  • 一个顶级的导航控制器
  • ArtistListViewController,用于显示画家列表
  • ArtistDetailViewController ,用于显示画家的作品以及生平事迹。

运行程序。你会在 ArtistListViewController 中看到画家列表。选择第一个画家(Pablo Picasso),app 将跳转到ArtistDetailViewController,在那里列出其作品:

这个 app 不仅没有显示出画家和作品的图片,而且显示的文字也是不完整的!每段描述和图片的大小都是不同的,因此你不能只是增加 cell 的高度。cell 高度应当是动态的,根据 cell 的内容来算出。

就在 ArtistListViewController 中来实现动态单元格高度吧。

自适应表格单元格

要实现动态计算单元格高度,你需要创建自定义 cell 并正确设置它的自动布局约束。

在项目导航窗口中,选择 Views 目录,按下 Command + N 键,新建一个文件。新建 Cocoa Touch Class,名为 ArtistTableViewCell,并继承 UITableViewCell。

打开 ArtistTableViewCell.swift 文件,删除自动插入的两个方法,添加属性声明:

@IBOutlet var bioLabel: UILabel!

然后,打开 Main.storyboard,选中 ArtistListViewController 的 table view 中的单元格对象。在 Identity 面板中,将 Class 设置为 ArtistTableViewCell:

拖一个 UILabel 到 cell 中,设置 text 属性为 Bio。Lines 属性(Label 能够显示的最大行数)设置为 0。

对于动态高度的单元格来说,将 lines 设置为 0 是个重点。lines 为 0 的标签表明它的高度会随着文字的长度自动增长。如果将 lines 设置为其它数字,则会导致当文字长度超过指定行数时,文字将被截断。

将标签连接到 ArtistTableViewCell 的 bioLabel 出口。比较快的方法是在 Document Outline 出口中右键点击 cell,然后从弹出的 Outlet 列表中,点击 bioLabel 右边的空心圆圈拖到刚才添加的标签对象上:

要让 UITableViewCell 的自动布局能够生效,诀窍就是它的每个 subview 的 4 条边都加一个 pin 约束——也就是说,每个 subview 都必须有 leading、top、trailing 和 bottom 约束。这样,就会用这些 subview 的 intrinsic 高度来计算 cell 的高度。让我们来试一下。

注意:如果你不熟悉自动布局,或者想了解如何创建自动布局约束,请看这里

选中 bioLabel,点击故事板右下角的 Pin 按钮。在弹出菜单中,在顶部点击指向 4 个方向的 4 条虚线,左边和右边的值修改为 8,在点击 Add Constraints:

这样,无论单元格有多大,bioLabel 总是:

  • 距离 cell 顶部、底部 0 个 point
  • 距离 cell 左边、右边 8 个 point

评论:这是否满足前面的自动布局条件?
1. 每个 subview 的 4 边都设置了 pin 约束?yes。
2. 从 contentView 上边到下边是否有约束?yes。
bioLabel 的上、下边距为 0。
因此自动布局完全可以计算出单元格的高度!

好了,ArtistTableViewCell 创建好了!编译运行 app,你会看到:

什么变化都没有。搞毛啊?别担心,你还需要些一小点代码才能让 cell 变成动态的。

设置 Table View

首先,需要设置 table view,让它使用你的自定义 cell。

打开 ArtistListViewController.swift ,将 tableView(_:cellForRowAtIndexPath:) 方法修改为:

func tableView(tableView: UITableView,
               cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
                                                         forIndexPath: indexPath) as! ArtistTableViewCell

  let artist = artists[indexPath.row]
  cell.bioLabel.text = artist.bio
  cell.bioLabel.textColor = UIColor(red: 114 / 255,
                                    green: 114 / 255,
                                    blue: 114 / 255,
                                    alpha: 1.0)
  return cell
}

上述代码非常简单:取出一个缓存的 cell,设置它的文字和颜色,然后返回这个 cell。

再次运行 app,看起来还是没什么变化。你现在用的是 bioLabel,但每个 cell 还是只显示一行文本。尽管你已经将 lines 属性设为 0,约束也设对了,bioLabel 现在已经占据了整个 cell,但你还需要告诉 table view,让自动布局引擎计算每个 cell 的高度。

回到 ArtistListViewController.swift 在 viewDidLoad() 最后加入:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 140

当 rowHeight 被设置为 UITableViewAutomaticDimension 时,table view 会根据自动布局约束和 cell 的内容来计算单元格高度。

编译运行,你现在可以看到每个大画家的生平简介了 :]

加入图片

虽然能够看到每个画家的简洁,但最好还是再显示一些内容。
每个画家都有一张图片和姓名需要显示。加上这些内容会让整个 app 看起来更好。

加一个 image view 到 ArtistTableViewCell,然后加一个 label 用于显示画家的名字。打开 ArtistTableViewCell.swift,加入如下属性:

@IBOutlet var nameLabel: UILabel!
@IBOutlet var artistImageView: UIImageView!

image view 的变量名是 artistImageView 而不是 imageView,是因为在 UITableViewCell 中已经有一个 imageView 属性了。
打开 Main.storyboard,选中 cell,在 Size 面板中,将 Row Height 设为 140;以便你有更多的空间可用:

选中 bioLabel 的左边距约束,你可以在 Document Outline 窗口的 Content View 的 Constraints 下面找到它:

用 delete 键删除这个约束。不要理会正在提示的自动布局警告。用鼠标按住 bioLabel 的左边缘向右拖,让 bioLabel 的宽度大概只占 cell 宽度的一半。你会在空出的左边放置 image view 和名字标签:

拖一个新的 label 到 cell 的下方,水平居中对齐刚刚空出来的地方。设置 text 属性为 Name:

拖一个 image view,放到 nameLabel 的上方:

然后,为 image view 和 nameLabel 创建连接,就像你在 bioLabel 上所做的一样:

接下来创建约束。首先从 nameLabel 开始,依次往上添加:

  • 从 nameLabel 的底部 pin 到 contentView 的底部 0 个 point。
  • 从 nameLabel 的顶部 pin 到 image view 的底部 8 个 point。
  • 从 image view 的顶部 pin 到 contentView 的顶部 0 个 point。
  • 从 image view 的左边 pin 到 contentView 的左边 0 个 point。
  • 从 image view 的右边 pin 到 bioLabel 的左边 16 个 point。

选中 image view,用右键拖到 contentView。然后选择 Equal Widths:

在 Document Outline 窗口中,找到新建的这条宽度约束,将它的 multiplier 修改为 0.5:

这会让 image view 的宽度等于单元格宽度的二分之一。

继续添加约束:

  • 按住 shift 键,点击 image view 和 nameLabel,然后选择 Pin 菜单下的 Equal Width
  • 按住 shift 键,点击 image view 和 nameLabel,然后选择 Align 菜单下的 Horizontal Centers(水平居中对齐)

添加完这些约束,自动布局引擎可能会报几个错误,告诉你某些 frame 是错误的。要消灭这些警告,在 Document Outline 窗口中选择 contentView,然后点击 Resolve Auto Layout Issues 菜单中的 All Views > Update Frames under :

故事板中的操作就完成了。打开 ArtistListViewController.swift,在 tableView(_:cellForRowAtIndexPath:) 方法中,找到设置 bioLabel.text 一句,在后面添加:

cell.artistImageView.image = artist.image
cell.nameLabel.text = artist.name

在设置 textColor 一句后添加:

cell.nameLabel.backgroundColor = UIColor(red: 255 / 255, green: 152 / 255, blue: 1 / 255, alpha: 1.0)
cell.nameLabel.textColor = UIColor.whiteColor()
cell.nameLabel.textAlignment = .Center
cell.selectionStyle = .None

运行 app。这个画面是不是更好看一些?但当你拉到 Georgia O’Keeffe 处,你会发现有点不对劲:

nameLabel 被拉高了(顶部距离 image view 底部 8 point,底部距离 contentView 底部 0 point)。

你可以修改某些约束来解决这个问题。在 Main.storyboard 中,选中 nameLabel 再添加一个约束,从它的底部 pin 到单元格的底部。在 Document Outline 窗口中,选中这条约束,修改它的 Relation 为大于等于:

然后选中 nameLabel 原来的那条底边约束,设置它的优先级为250:

这样,自动布局引擎会在必要的时候放弃老的约束,因为它的优先级比底部边距 >= 0 的那条约束低。运行 app,一切变得更加合理。

显示作品

如果你还记得开始的内容,当点击某个画家时,会跳到另一个 view controller,并显示该画家的作品。在这个 table view 中的 cell 需要使用动态高度,因为在对应的数据中,每个作品都会有不同的大小。

第一步,同之前一样,也是创建一个 UITableViewCell 子类。

在项目导航窗口中选择 views 文件夹,按下 command + N 键,新建一个文件。创建一个名为 WorkTableViewCell 的 Cocoa Touch 类,并继承 UITableViewCell。

打开 WorkTableViewCell.swift,如同之前一样,删除自动生成的两个方法,添加如下属性声明:

@IBOutlet weak var workImageView: UIImageView!
@IBOutlet weak var workTitleLabel: UILabel!
@IBOutlet weak var moreInfoTextView: UITextView!

打开 Main.storyboard ,在 Artist Detail View Controller 中选中位于 table view 中的 cell。将 cell 的 Custom Class 设置为 WorkTableViewCell,然后将 row height 修改为 200 ,以便有更多的空间操作。

拖一个 image view、一个 label、一个 text view,分别如下图放置(text view 放到最下边):

将 text view 的 text 修改为 “Select For More Info >” ,将 label 的 text 设置为 “Name”。将 image view 的mode 属性修改为 Aspect Fit。选择 text view,在属性面板中,修改 alignment 属性为居中,并禁止滚动:

和将 label 的 lines 属性设为 0 一样,禁止 text view 滚动也是一个重点。一旦禁止滚动后,text view 就会根据其内容来增长其 size,因为用户不能通过滚动的方式来查看完整的内容了。

在 Scrolling Enabled 稍往下拉一点,将 User Interaction Enabled 选项清空,这将允许触摸事件穿过 text view 向上传递,并触发单元格的选中事件。

将这 3 个对象与其对应的出口进行连接,就像我们在上一个单元格中所做的一样。

接下来添加约束。首先从 text view 开始依次往上添加:

  • 从 text view 底部 pin 到 contentView 底部 0 个 point。
  • 从 text view 左边、右边分别 pin 到 contentView 左边和右边 8 个 point。
  • 从 text view 顶部 pin 到 label 底部 8 个 point。
  • 从 label 顶部 pin 到 image view 底部 8 个 point。
  • 从 Align 菜单中选择 Horizontally in Container(居中于容器),使 label 居中。
  • 选中 nameLabel 和 image view(用 shift + 鼠标左键),选则 pin 菜单中的 Equal Widths。
  • 从 image view 的顶部 pin 到 contentView 顶部 0 个 point。
  • 从 image view 的左边、右边 pin 分别 pin 到 contentView 的左边、右边 8 个 point。

如果出现自动布局警告,像之前一样,刷新一下 frame。现在故事板中的工作就做完了。如同前面一样,接下来需要编写一点代码。
打开 ArtistDetailViewController.swift 修改 tableView(_:cellForRowAtIndexPath:) 方法为:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! WorkTableViewCell

  let work = selectedArtist.works[indexPath.row]

  cell.workTitleLabel.text = work.title
  cell.workImageView.image = work.image

  cell.workTitleLabel.backgroundColor = UIColor(red: 204 / 255, green: 204 / 255, blue: 204 / 255, alpha: 1.0)
  cell.workTitleLabel.textAlignment = .Center
  cell.moreInfoTextView.textColor = UIColor(red: 114 / 255, green: 114 / 255, blue: 114 / 255, alpha: 1.0)
  cell.selectionStyle = .None

  return cell
}

这些代码看起来非常熟悉。从 cell 缓存中取出一个 cell 转换成自定义 cell,从模型数据中检索要显示的对象,设置 cell 属性然后返回 cell。

在同一类的 viewDidLoad() 方法中,在最后添加如下代码:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 300

这和之前 View controller 中的代码是一样的。运行 app,选择毕加索,现在你可以看到这位大画家的作品了:

干得不错!但接下来我们要添加一个滑出式单元格,以显示每个作品的详细介绍。你的用户一定会喜欢这个!

展开式单元格

因为 cell 高度由自动布局约束及每个 UI 元素的内容来计算,展开式单元格非常简单,其实就是在用户点击单元格时往 text view 中插入更多文字而已。

打开 ArtistDetailViewController.swift 定义一个 extension:

extension ArtistDetailViewController: UITableViewDelegate {

  func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    // 1
    guard let cell = tableView.cellForRowAtIndexPath(indexPath) as? WorkTableViewCell else { return }

    var work = selectedArtist.works[indexPath.row]

    // 2
    work.isExpanded = !work.isExpanded
    selectedArtist.works[indexPath.row] = work

    // 3
    cell.moreInfoTextView.text = work.isExpanded ? work.info : moreInfoText
    cell.moreInfoTextView.textAlignment = work.isExpanded ? .Left : .Center

    // 4
    UIView.animateWithDuration(0.3) {
      cell.contentView.layoutIfNeeded()
    }

    // 5
    tableView.beginUpdates()
    tableView.endUpdates()

    // 6
    tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
  }
}

上述代码解释如下:

  1. 通过用户所选的 index path 从 tableView 中找到指定的 cell,然后根据 index path 检索出对应的作品 work。
  2. 修改 work 的 isExpanded 属性,然后将它重新放回数组(这是必须的,因为结构体是以拷贝的方式传递的)。
  3. 然后,根据 work 的 isExpanded 属性 修改 cell 的 text view 的显示内容:如果为 true,将 text view 的 text 设为 work 的 info 属性并将文本对齐方式设置为左对齐。如果为 false,则设置为 “Select to See More >”并置文本对齐方式为居中对齐。
  4. 修改完 text view 的内容之后,需要刷新 cell 的约束。在动画块中调用 layoutIfNeeded() 将重新计算布局约束。
  5. 除了刷新布局约束,table view 还需要重新计算 cell 高度。通过调用 beginUpdates() 和 endUpdates(),将以动画方式强制让 table view 刷新 cell 高度。
  6. 最后,让 table view 以动画方式将用户所点击的 cell 滑动到 table view 的顶端。

然后是 tableView(_:cellForRowAtIndexPath:) 方法,在方法最后 return 之前加入:

cell.moreInfoTextView.text = work.isExpanded ? work.info : moreInfoText
cell.moreInfoTextView.textAlignment = work.isExpanded ? NSTextAlignment.Left : NSTextAlignment.Center

上述代码让重用单元格正确显示展开或缩起状态。

编译运行 app。当点击某个作品,你会看到它将展开成完整信息显示。但图片的显示不正确。

这个很好搞定!打开 Main.storyboard,选择你的 WorkTableViewCell 中的 image view,打开 Size 面板。修改 content Hugging Priority 和 Content Compression Resistance Priority 为如下值:

将 Vertical Content Hugging Priority 设置为 252,是为了让 image view 固定其内容,并在动画过程中不进行拉伸。设置
Vertical Compression Resistance Priority 为 749,是允许图片在周围元素变大时能够被压缩。这只是为了让 cell 执行展开动画时更加平滑。图片并不会被压缩,因为当 cell 内部的东东长高时 cell 的高度也会随之长高。

运行 app,选择某位画家,然后点击某个作品。你会看到 cell 的展开变得非常平滑,并显示出每个作品的详细介绍:

好极了!

动态字体

将成果拿给用户看吧,他们一定爱死了!但他们最后又提了一个需求。他们想让 app 支持 Larger Text Accessibility 特性。
在 iOS 7 中引入了动态字体,这让这个工作变得轻松。动态字体允许开发者为不同的文本块(比如标题和中文)指定不同的文本风格,同时可以让文本根据用户在设置中指定的大小进行显示。

在 ArtistListViewController.swift 的 tableView(_:cellForRowAtIndexPath:) 方法返回之前加入:

cell.nameLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.bioLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

这里使用动态字体来设置 UI 元素上的文本。preferredFontForTextStyle(style:) 只有一个参数,就是你想在这个文本元素用什么风格来进行显示。你可以用 10 种不同的常量,请参考苹果关于 preferredFontForTextStyle(style:) 的文档

然后你需要在用户修改了字体大小偏好的时候刷新 table view。在 ArtistListViewController 的 viewDidLoad() 后添加方法:

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(animated)

  NSNotificationCenter.defaultCenter().addObserverForName(UIContentSizeCategoryDidChangeNotification, object: nil, queue: NSOperationQueue.mainQueue()) { [weak self] _ in self?.tableView.reloadData()
  }
}

这里注册了 onContentSizeCategoryChange: 通知的观察者,当用户修改了字体大小偏好之后会发送这个通知。

这个观察者使用了一个闭包来通知 table view 进行刷新。这会针对屏幕上所有显示的 cell 调用 tableView(_:cellForRowAtIndexPath:) 方法。在这个方法中又会调用 preferredFontForTextStyle(style:) 方法。现在字体会在收到通知时发生改变。

注意:从 iOS 9 开始,从通知中心移除观察者不再是必须的了。但如果你的 app 部署目标是 iOS 8,你仍然需要移除观察者!

在 ArtistDetailViewController 中增加 动态字体 支持于此类型。打开 ArtistDetailViewController.swift 然后在 tableView(_:cellForRowAtIndexPath:) 方法最后添加:

cell.workTitleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.moreInfoTextView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)

然后在 viewDidAppear(_:) 中和前面一样加入同样的代码。

在 iOS 9.3 模拟器中进行测试是无效的,你必须在设备上进行测试。在设备上运行 app,切换到 Home 屏。打开设置 app,依次点击 通用 > 辅助功能 > 更大字体,向右边拖动滑动条将字体变大:

然后回到 app,你的文本将变得更大了。幸好你使用了自适应单元格,table view 仍然显示正常:

结束语

祝贺你,这个自适应 table view cell 的教程到此结束了!:]
你可以从 这里 下载完整项目。

Table view 大概是 iOS 中最基本的结构化数据视图。随着 app 越来越复杂,你可能用过各种各样的自定义单元格布局。幸运的是,子宫布局和 iOS 8 使这一切变得简单。

如果你有任何问题和建议,请留言。

作者:kmyhy 发表于2016/10/8 9:30:49 原文链接
阅读:54 评论:0 查看评论

android事件分发机制分析

$
0
0
触摸事件相关方法:

ViewGroup
  • dispatchTouchEvent(MotionEvent)          用于分发touch事件
  • onInterceptTouchEvent(MotionEvent)   用于是否中断touch事件
  • onTouchEvent(MotionEvent)                       用于处理touch事件

View、Activity
  • dispatchTouchEvent(MotionEvent)  
  • onTouchEvent(MotionEvent)  
具体代码如下:

1、Utils.java  代码如下:
  1. public class Utils {
  2. /**
  3. * 获取触摸事件的Action名称
  4. * @param ev
  5. */
  6. public static String getActionName(MotionEvent ev) {
  7. String action;
  8. switch (ev.getAction()) {
  9. case MotionEvent.ACTION_CANCEL:
  10. action = "CANCEL";
  11. break;
  12. case MotionEvent.ACTION_DOWN:
  13. action = "DOWN";
  14. break;
  15. case MotionEvent.ACTION_MOVE:
  16. action = "MOVE";
  17. break;
  18. case MotionEvent.ACTION_UP:
  19. action = "UP";
  20. break;
  21. default:
  22. action = "UNKNOWN_ACTION";
  23. break;
  24. }
  25. if (action.length() < 5) {
  26. for (int i = action.length(); i < 5; i++) {
  27. action += " ";
  28. }
  29. }
  30. return action;
  31. }
  32. }

2、ViewGroupA.java 代码如下:
  1. public class ViewGroupA extends LinearLayout {
  2. public ViewGroupA(Context context, AttributeSet attrs) {
  3. super(context, attrs);
  4. }
  5. @Override
  6. public boolean dispatchTouchEvent(MotionEvent ev) {
  7. System.out.println(Utils.getActionName(ev) + ", ViewGroupA.dispatch");
  8. boolean result = super.dispatchTouchEvent(ev);
  9. System.out.println(Utils.getActionName(ev) + ", ViewGroupA.dispatch = " + result);
  10. return result;
  11. }
  12. @Override
  13. public boolean onInterceptTouchEvent(MotionEvent ev) {
  14. boolean result = false;
  15. System.out.println(Utils.getActionName(ev) + ", ViewGroupA.intercept = " + result);
  16. return result;
  17. }
  18. @Override
  19. public boolean onTouchEvent(MotionEvent event) {
  20. boolean result = false;
  21. System.out.println(Utils.getActionName(event) + ", ViewGroupA.touch = " + result);
  22. return result;
  23. }
  24. }

布局效果如下:




运行代码,在蓝色的View上进行按下、移动、抬起,输出的Log如下:

          ---------------------------------------------
DOWN , Activity.dispatch
DOWN , ViewGroupA.dispatch
DOWN , ViewGroupA.intercept = false
DOWN , ViewGroupB.dispatch
DOWN , ViewGroupB.intercept = false
DOWN , ViewC.dispatch
DOWN , ViewC.touch = false
DOWN , ViewC.dispatch = false
DOWN , ViewGroupB.touch = false
DOWN , ViewGroupB.dispatch = false
DOWN , ViewGroupA.touch = false
DOWN , ViewGroupA.dispatch = false
DOWN , Activity.touch = false
DOWN , Activity.dispatch = false
---------------------------------------------
MOVE , Activity.dispatch
MOVE , Activity.touch = false
MOVE , Activity.dispatch = false
---------------------------------------------
UP   , Activity.dispatch
UP   , Activity.touch = false
UP   , Activity.dispatch = false
---------------------------------------------

从上面的Log可分析出:
  • 触摸事件最先是由Activity获得,然后是ViewGroupA、ViewGroupB、ViewC
  • 如果Down事件没有人处理,则事件丢失,后续的事件(如Move、Up)不再传递,直接由Activity进行处理
  • 所有的事件分发都是调用super.dispatchTouchEvent(ev)完成的,所以如果不调用这句代码则事件中止传递。但是要中止事件传递一般不会这么做,一般是在onInterceptTouchEvent(MotionEvent) 方法中处理,如果该方法返回true则中止。既然dispatchTouchEvent(ev)方法可以中止事件传递,为什么还要设计一个onInterceptTouchEvent(MotionEvent) 方法呢? 因为子View可以请求父View不要拦截事件,如ListView是可以上下滑动的,当处于滑动状态时候就会请求禁止父View的拦截触摸事件方法,让ListView可以一直获取到touch事件进行滚动。假设这个时候父View又想响应触摸事件怎么办?可以写到dispatchTouchEvent方法中,因为事件是先传到这个方法,然后再传递给ListView的。
  • 既然Activity最先获得事件,则可在Activity的dispatchTouchEvent方法不调用super.dispatchTouchEvent(ev);代码,则事件就不会进行分发了,可在此方法中调用onTouchEvent(ev)让它去处理。
  • 应用:假如在Activity中要一定要响应一些触摸事件,又怕事件传递后被消费了,也是相同道理,直接在Activity的dispatchTouchEvent方法中处理即可)

修改ViewGroupA的onInterceptTouchEvent让其返回true,代表拦截事件。
运行,然后按下、移动、抬起,Log如下:

---------------------------------------------
DOWN , Activity.dispatch
DOWN , ViewGroupA.dispatch
DOWN , ViewGroupA.intercept = true
DOWN , ViewGroupA.touch = false
DOWN , ViewGroupA.dispatch = false
DOWN , Activity.touch = false
DOWN , Activity.dispatch = false
---------------------------------------------
MOVE , Activity.dispatch
MOVE , Activity.touch = false
MOVE , Activity.dispatch = false
---------------------------------------------
UP   , Activity.dispatch
UP   , Activity.touch = false
UP   , Activity.dispatch = false

从上面的Log可分析出:
  • ViewGroupA在Down事件的时候中断事件传递后直接把Touch事件交由自己的onTouchEvent方法去处理
  • ViewGroupA中断触摸事件后只是子View(ViewGroupB和ViewC)不再获得touch事件,而父View(Activity)可以。
  • 虽然ViewGroupA中断触摸事件传递,由于它的onTouchEvent方法也没有处理Down事件,所以事件也丢失了,后续事件(如Move、Up)不再传给ViewGroupA,由Activity中行处理。所以,如果你想要获取后续事件,在处理了Down事件后一定要记得返回true。这里说的后续事件是指一次整体的操作,一般是由一个Down和多个Move和一个Up组成。按下然后移动然后弹起,这样的操作称为一次整体的操作。
修改ViewGroupA的onInterceptTouchEvent让其返回false,代表不拦截事件
修改ViewGroupB的onInterceptTouchEvent让其返回true,代表拦截事件,并修改onTouchEvent方法如下:
  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. boolean onTouchEvent = super.onTouchEvent(event);;
  4. switch (event.getAction()) {
  5. case MotionEvent.ACTION_DOWN:
  6. onTouchEvent = true;
  7. break;
  8. case MotionEvent.ACTION_MOVE:
  9. onTouchEvent = false;
  10. break;
  11. case MotionEvent.ACTION_UP:
  12. onTouchEvent = false;
  13. break;
  14. default:
  15. break;
  16. }
  17. System.out.println(MotionEventUtil.getMotionEventActionName(event) + ", ViewGroupB.touch = " + onTouchEvent);
  18. return onTouchEvent;
  19. }
运行,然后按下、移动、抬起,Log如下:

        ---------------------------------------------
    DOWN , Activity.dispatch
        DOWN , ViewGroupA.dispatch
        DOWN , ViewGroupA.intercept = false
        DOWN , ViewGroupB.dispatch
        DOWN , ViewGroupB.intercept = true
        DOWN , ViewGroupB.touch = true
        DOWN , ViewGroupB.dispatch = true
        DOWN , ViewGroupA.dispatch = true
        DOWN , Activity.dispatch = true
        ---------------------------------------------
        MOVE , Activity.dispatch
        MOVE , ViewGroupA.dispatch
        MOVE , ViewGroupA.intercept = false
        MOVE , ViewGroupB.dispatch
        MOVE , ViewGroupB.touch = false
        MOVE , ViewGroupB.dispatch = false
        MOVE , ViewGroupA.dispatch = false
        MOVE , Activity.touch = false
        MOVE , Activity.dispatch = false
        ---------------------------------------------
        MOVE , Activity.dispatch
        MOVE , ViewGroupA.dispatch
        MOVE , ViewGroupA.intercept = false
        MOVE , ViewGroupB.dispatch
        MOVE , ViewGroupB.touch = false
        MOVE , ViewGroupB.dispatch = false
        MOVE , ViewGroupA.dispatch = false
        MOVE , Activity.touch = false
        MOVE , Activity.dispatch = false
        ---------------------------------------------
        UP   , Activity.dispatch
        UP   , ViewGroupA.dispatch
        UP   , ViewGroupA.intercept = false
        UP   , ViewGroupB.dispatch
        UP   , ViewGroupB.touch = false
        UP   , ViewGroupB.dispatch = false
        UP   , ViewGroupA.dispatch = false
        UP   , Activity.touch = false
        UP   , Activity.dispatch = false


从上面的Log可分析出:
  • ViewGroupB中断了触摸事件的传递,并把事件交给自己的onTouchEvent方法处理。它的孩子ViewC就接收不到touch事件了
  • ViewGroupB中消费了Down事件(返回true),所以可以接收后续的Move、Up事件,dispatch方法接收到Move事件后并没有去调用onInterceptTouchEvent方法了,因为在接收到Down事件时候已经成为了目标View,非目标View不能接收后续事件,所以后续事件(Move、Up)就不需要再调用了onInterceptTouchEvent,因为不需要再传递了,所以直接把事件交给自己的onTouchEvent方法进行处理。
  • 虽然ViewGroupB在处理Move事件时返回了false,但是父View(ViewGroupA)的onTouchEvent方法并没有执行,这说明,谁消费了Down事件(返回true)谁就能接收后续事件(Move、Up),没有消费Down事件的其它View则接收不到(Activity例外)。
  • 虽然ViewGroupB在处理Move事件时返回了false,但是还是依旧可以接收后续事件的(Move、Up),那在处理Move和UP时返回false或返回true有什么区别吗?答:返回true则消费此事件,Activity的onTouchEvent方法就不会接收到了。
通过这个Demo可以模拟任何的不同情况,如处理Down事件返回true,Move事件返回false,事件拦截,请求禁止拦截等等。

总结:
  • touch事件传递其实就是一连串的方法调用,由Activity的dispatchTouchEvent方法开始调用,当这个方法调用结束时,这个touch事件就结束了。
  • 一个整体的touch事件由1个Donw和0 ~ n个Move和1个Up事件组成
  • 消费了Down事件的View称为目标View,目标View可接收后续事件(非目标View不接收后续事件)
  • Down事件如果没有任何View消费,则后续事件不再传递,直接由Activity的onTouchEvent方法处理,所以,想要处理其它事件首先要消费Down事件,也就是在接收到Down事件的时候返回true。
  • 拦截:
    • 如果在Down事件拦截,则把当前事件交自己的onTouchEvnet方法处理,如果此方法不消费Down事件,则事件丢失,后续事件由Activity的onTouchEvent方法处理
    • 如果在其它事件拦截,则不处理当前事件,且传一个Cancel事件给目标View,后续的事件就会交给自己的onTouchEvent方法处理(此时拦截了事件的View变成了目标View,虽然它没有消费Down事件)
    • 如果在Down事件不拦截,在事件分发返回后,如果Down事件没被消费,则会把事件交给自己的onTouchEvent方法处理。
    • 如果在其它事件不拦截(能接收其它事件说明有目标View),在事件分发返回后,不会把事件交给自己的onTouchEvent,即使目标View在处理(Move、Up)事件时返回false。
  • Activity不管有无目标View,也不管是Down还是Move、Up事件,只要分发调用返回时,如果事件没被消费,则交给自己的onTouchEvent方法处理
  • 可以调用getParent().requestDisallowInterceptTouchEvent(true)方法请求父View禁止拦截事件,这个方法会递归的请求所有的父View禁止拦截事件。
    • 注:如果想要获取到一个整体的touch事件,一定要消费Down事件,如果在Down事件的时候只是请求父View禁止拦截并不消费Down事件,虽然父View不再拦截了,但后续事件也接收不到了,哪个父View消费了Down事件哪个父View就可以接收到后续事件。
  • 有两个View并没有包含关系,但是有重叠,则上面的View先拿到事件,如果消费了,则事件不会传给另一个View。
  • 容器类一般都是调用ViewGroup的dispatchTouchEvent方法进行事件分发,其它类一般是调用View类的dispatchTouchEvent方法进行事件分发;通常默认的onInterceptTouchEvent、onTouchEvent方法都是返回false。
  • 应用技巧:
    • 父类的onTouchEvent方法要想执行,要么是等所有的子View都不消费Down事件,要么是父View把事件拦截。
    • 如果子类消费了Down事件,而父View又不想拦截但是又想处理这个事件,则父View可以在onInterceptTouchEvent或dispatchTouchEvent方法处理touch事件
    • 如果子View请求了禁止父View拦截,且父View还想要拦截的话,可在父View的dispatchTouchEvent方法中不调用super.dispatchTouchEvent则把事件拦截了。
作者:luomoBM 发表于2016/10/8 9:42:52 原文链接
阅读:16 评论:0 查看评论

【腾讯Bugly干货分享】iOS10 SiriKit QQ适配详解

$
0
0

1. 概述

苹果在iOS10开放了siriKit接口给第三方应用。目前,QQ已经率先适配了Siri的发消息和打电话功能。这意味着在iOS10中你可以直接告诉Siri让它帮你发QQ消息和打QQ电话了,听起来是不是很酷炫?

那么第三方应用使用Siri的体验究竟如何?哪些应用可以接入SiriKit?接入SiriKit又需要做哪些工作呢?这篇文章会为你一一解答这些疑惑。

图1 用Siri发QQ消息效果展示

2. SiriKit简介

我们都知道Siri是iphone手机中的智能语音助手,那么什么是SiriKit呢?SiriKit是苹果为第三方应用支持Siri提供的开发框架。在官方文档中,SiriKit将对不同场景的语音支持划分为不同的domain,目前,SiriKit支持的domain包括:VoIP电话、发消息、转账、图片搜索、网约车订车、CarPlay和餐厅预定,也就是说如果你的应用中包含有这些功能之一,就可以考虑将这些功能接入到SiriKit中啦。

实现SiriKit相关功能时,我们并不需要真正对语音进行识别,语音的识别工作会由Siri完成。Siri识别完语音后,会将语音要完成的功能抽象成Intent对象传递给我们,而我们的接入工作主要是与这些Intent对象打交道,并不会涉及到自然语言处理(NLP)的技术。

关于SiriKit的开发网上已有一些文章,也可参考苹果的官方文档SiriKit Programming Guide,本文着重介绍QQ的适配经验。

图2 SiriKit原理

3. SiriKit接入

要实现SiriKit的功能需要在Xcode工程中添加Intents Extension的target,和其他extension一样, Intents Extension是一个独立于Containing App进程运行的插件,主要用于处理和确认来自siri的intent请求。如果想让Siri在处理App相关intent时提供一些自定义的界面,那么你就需要再添加Intents UI Extension的target,Intents UI Extension也是一个独立运行的插件(所以要完整的支持SiriKit其实是需要添加两个target,有点蛋疼)。关于App Extension的开发可以参考苹果的App Extension Programming Guide。

我们以QQ中的发消息功能为例说明一下SiriKit的接入方法:

首先,我们需要在Intents Extentsion的info.plist文件中配置我们需要支持的siri Intents,在IntentsSupported中加入INSendMessageIntent,如果需要在锁屏时禁用某个功能,则再在IntentsRestrictedWhileLocked中加入相应项的Intent,如图3所示。

图3 Intent Extentsion info.plist配置

SiriKit的接入主要分为Intents Extension和Intents UI Extension两部分,下面分别进行介绍。

Intents Extension

当我们对siri说“用QQ发消息给王一然说你好”时,语音的识别将会由Siri自动完成,Siri会将识别好的内容展示在Siri的界面。如图4所示,我们可以看到一个完整的发消息语句主要由四部分组成:

应用名:告诉Siri要使用哪个App,siri会根据app的bundle displayname自动识别app的名称,无需额外注册。

发消息Intent:告诉Siri要使用发消息的功能,我们实测发现说发信息也是能识别,具体还有哪些词汇会识别为发消息的intent苹果没有在文档中说明。

消息接收者:告诉siri消息的接收者是谁,“王一然”是我QQ好友的昵称。

消息内容:告诉Siri你要发的消息内容是什么,这里的消息内容为“我很生气”。

图4 确认发送消息界面

其中应用名和Intent是必须的,不然Siri无法抽象出你的“Intent”。后两项如果缺省的话,我们可以在实现中要求用户进一步提供数据或者忽略。在识别完成后Siri会将消息内容和接收者抽象成一个INSendMessageIntent传递给 QQ的Intent Extension。

我们从图4还可以看到Siri准确从我的语音中识别出我QQ好友中昵称为“王一然”的好友,然而“王一然”并不是一个通用的短语,那么这是怎么做到的呢?奥秘就在于在QQ运行时我们把所有QQ好友的昵称同步到了Siri云端,这样Siri就可以识别出特定用户要使用的特定短语,详细同步方法可参考INVocabulary的setVocabularyStrings:ofType:方法。

每个domain的功能在Siri中都有对应的Intents,而每个intents都对应一个特定的handler协议。对于发消息来讲,对应的Intent和handler协议分别为INSendMessageIntent和INSendMessageIntentHandling。只要实现INSendMessageIntentHandling协议中的相关方法,并在Siri解析出INSendMessageIntent请求时用我们的INSendMessageIntentHandling对象去处理相关的发消息请求。具体的流程如图5:

图5 Siri发QQ消息流程

1)ResolveRecipientsForSendMessage

对siri从Intent中传递过来的接收者名称进行处理和确认,比如可以确认该名称当前是否在QQ好友列表中,并将resolution result反馈给Siri。Resolution result代表了应用对intent处理后的结果,对于发消息来说,表1列举了几种可能的resolution results。

表1 send resolution result

2)ResolveContent

与接收者的处理类似,在这个方法中可以对Siri识别出的消息内容进行“修饰”,并且将resolution result反馈给Siri,比如QQ对一些消息里面的特殊词汇如“生气”做了emoji适配。

3)ConfirmSendMessage

这个方法的作用是确认是否要发送该消息,可以在这一步进行一些鉴权工作,鉴权通过后再确认发送,否则取消。确认可以发送后会调起确认发送界面,如图4所示。如果需要从Containing App共享数据,具体的实现方案参考App Group的Shared Container。

4)HandleSendMessage

如图4,当用户点击了“发送”按钮或者用语音给出了发送指令时会最终进入到这个方法,在这个方法里我们需要实现发消息的逻辑,发送成功后可以调起消息发送成功的界面,如图6。

图6 消息发送成功界面

Intents UI Extension

对于支持自定义界面的Intent类型,可以在Intents UI Extension中提供更美观的自定义界面。 Custom UI的实现相对较简单,和ios app的开发一样,都是通过UIViewController的子类实现。我们需要在Intents UI Extension的info.plist文件中设置initial viewcontroller或者设置main storyboard,对于不同类型的Intent的界面展示通过Child Viewcontrollers的方式实现差异化界面展示。

如图7所示,当接收到来自Intents Extension的response时,系统会唤起Intents UI Extension并加载initial viewcontroller,通过INUIHostedViewSiriProviding协议的configureWithInteraction:context:completion:方法可以获取intent,比如在发消息功能中,在消息确认发送和发送成功后都会回调一次这个方法。根据Intent对象的类型和状态,在收到相关Intent的回调时present对应的Child Viewcontroller即可实现定制化的界面展示。

这里需要注意的是,Intents UI Extension的进程并不会在界面销毁后就退出,很可能只是在后台处于休眠状态,下次response到来时再被唤醒。

图7 Life cycle of an Intents UI extension

4. 总结

总的来说虽然苹果这一次对SiriKit开放的场景有限,但是从我们的适配经历来看苹果对Siri还是非常重视的。另外,这是SiriKit首次对第三方应用开放接口,所以不可避免存在一些问题。我们在开发过程中也确实遇到了一些SiriKit本身的Bug,大部分bug在向苹果反馈后都得到了解决,但是在语言识别方面Siri依然存在一些缺陷,比如对中英文混合的场景识别依旧不太好。期待以后Siri对中文的支持越来越好,也希望Siri能够开放更多的场景给第三方应用适配。

作者:Tencent_Bugly 发表于2016/10/8 9:55:06 原文链接
阅读:18 评论:0 查看评论

【腾讯Bugly干货分享】微信Tinker的一切都在这里,包括源码(一)

$
0
0

最近半年以来,Android热补丁技术热潮继续爆发,各大公司相继推出自己的开源框架。Tinker在最近也顺利完成了公司的审核,并非常荣幸的成为github.com/Tencent上第一个正式公开的项目。

回顾这半年多的历程,这是一条跪着走完,坑坑不息之路。或许只有自己真正经历过,深入研究过, 才会真正的明白

热补丁不是请客吃饭

对热补丁技术本身,还是对使用者来说都是如此。它并不简单,也有着自己的局限性,在使用之前我们需要对它有所了解。我希望通过分享微信在这历程中的思考与经验,能帮助大家更容易的决定是否在自己的项目中使用热补丁技术,以及选择什么样方案。

热补丁技术背景

热补丁是什么以及它的应用场景介绍,大家可以参考文章微信Android热补丁实践演进之路

在笔者看来Android热补丁技术应该分为以下两个流派:

  • Native,代表有阿里的Dexposed、AndFix与腾讯的内部方案KKFix;
  • Java, 代表有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust。

Native流派与Java流派都有着自己的优缺点,它们具体差异大家可参考上文。事实上从来都没有最好的方案,只有最适合自己的。

对于微信来说,我们希望得到一个“高可用”的补丁框架,它应该满足以下几个条件:

  1. 稳定性与兼容性;微信需要在数亿台设备上运行,即使补丁框架带来1%的异常,也将影响到数万用户。保证补丁框架的稳定性与兼容性是我们的第一要务;
  2. 性能;微信对性能要求也非常苛刻,首先补丁框架不能影响应用的性能,这里基于大部分情况下用户不会使用到补丁。其次补丁包应该尽量少,这关系到用户流量与补丁的成功率问题;
  3. 易用性;在解决完以上两个核心问题的前提下,我们希望补丁框架简单易用,并且可以全面支持,甚至可以做到功能发布级别。

在“高可用”这个大前提下,微信对当时存在的两个方案做了大量的研究:

  1. Dexposed/AndFix;最大挑战在于稳定性与兼容性,而且native异常排查难度更高。另一方面,由于无法增加变量与类等限制,无法做到功能发布级别;
  2. Qzone;最大挑战在于性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题;

在2016年3月,微信为了追寻“高可用”这个目标,决定尝试搭建自己的补丁框架——Tinker。Tinker框架的演绎并不是一蹴而就,它大致分为三个阶段,每一阶段需要解决的核心问题并不相同。而Tinker v1.0的核心问题是实现符合性能要求的Dex补丁框架。

Tinker v1.0-性能极致追求之路

为了稳定性与兼容性,微信选择了Java流派。当前最大难点在于如何突破Qzone方案的性能问题,这时通过研究Instant Run的冷插拔与buck的exopackage给了我们灵感。它们的思想都是全量替换新的Dex。

简单来说,我们通过完全使用了新的Dex,那样既不出现Art地址错乱的问题,在Dalvik也无须插桩。当然考虑到补丁包的体积,我们不能直接将新的Dex放在里面。但我们可以将新旧两个Dex的差异放到补丁包中,这里我们可以调研的方法有以下几个:

  1. BsDiff;它格式无关,但对Dex效果不是特别好,而且非常不稳定。当前微信对于so与部分资源,依然使用bsdiff算法;
  2. DexMerge;它主要问题在于合成时内存占用过大,一个12M的dex,峰值内存可能达到70多M;
  3. DexDiff;通过深入Dex格式,实现一套diff差异小,内存占用少以及支持增删改的算法。

如何选择?在“高可用”的核心诉求下,性能问题也尤为重要。非常庆幸微信在当时那个节点坚决的选择了自研DexDiff算法,这过程虽然有苦有泪,但也正是有它,才有现在的Tinker。

一. DexDiff技术实践

在不断的深入研究[Dex格式](https://source.android.com/devices/tech/dalvik/dex-format.html
)后,我们发现自己跳进了一个深坑,主要难点有以下三个:

  1. Dex格式复杂;Dex大致分为像StringID,TypeID这些Index区域以及使用Offset的Data区域。它们有大量的互相引用,一个小小的改变可能导致大量的Index与Offset变化;
  2. dex2opt与dex2oat校验;在这两个过程系统会做例如四字节对齐,部分元素排序等校验,例如StringID按照内容的Unicode排序,TypeID按照StringID排序…
  3. 低内存,快速;这要求我们对Dex每一块做到一次读写,无法像baksmali与dexmerge那样完全结构化。

这不仅要求我们需要研究透Dex的格式,也要把dex2opt与dex2oat的代码全部研究透。现在回想起来,这的确是一条跪着走完的路。与研究Dalvik与Art执行一致,这是经历一次次翻看源码,一次次编Rom查看日志,一次次dump内存结构换来的结果。

下面以最简单的Index区域举例:

要想将从左边序列更改成右边序列,Diff算法的核心在于如何生成最小操作序列,同时修正Index与Offset,实现增删改的功能。

  1. Del 2;”b”元素被删除,它对应的Index是2,为了减少补丁包体积,除了新增的元素其他一律只存Index;
  2. “c”, “d”, “e”元素自动前移,无须操作;
  3. Addf(5); 在第五个位置增加”f”这个元素。

对于Offset区,由于每个Section可能有非常多的元素,这里会更加复杂。最后我们得到最终的操作队列,为什么DexDiff可以做到内存非常少?这是因为DexDiff算法是每一个操作的处理,它无需一次性读入所有的数据。DexDiff的各项数据如下:

通过DexDiff算法的实现,我们既解决了Dalvik平台的性能损耗问题,又解决了Art平台补丁包过大的问题。但这套方案的缺点在于占Rom体积比较大,微信考虑到移动设备的存储空间提升比较快,增加几十M的Rom空间这个代价可以接受。

二. Android N的挑战

信心满满上线后,却很快收到华为反馈的一个Crash:

而且这个Crash只在Android N上出现,在当时对我们震动非常大,难道Android N不支持Java方式热补丁了?难道这两个月的辛苦都白费了吗?一切想象都苍白无力,只有继续去源码里面找原因。

在之前的基础上,这一块的研究并没有花太多的时间,主要是Android N的混合编译模式导致。更多的详细分析可参考文章Android N混合编译与对热补丁影响解析

三. 厂商OTA的挑战

刚刚解决完Android N的问题,还在沉醉在自己的胜利的愉悦中。前线很快又传来噩耗,小米反馈开发版的一些用户在微信启动时黑屏,甚至ANR.

当时第一反应是不可能,所有的DexOpt操作都是放到单独的进程,为什么只在Art平台出现?为什么小米开发版用户反馈比较多?经过分析,我们发现优化后odex文件存在有效性的检查:

  • Dalvik平台:modtime/crc…
  • Art平台: checksum/image_checksum/image_offset…

这就非常好理解了,因为OTA之后系统image改变了,odex文件用到image的偏移地址很可能已经错误。对于ClassN.dex文件,在OTA升级系统已完成重新dex2oat,而补丁是动态加载的,只能在第一次执行时同步执行。

这个耗时可能高达十几秒,黑屏甚至ANR也是非常好理解。那为什么只有小米用户反馈比较多呢?这也是因为小米开发版每周都会推送系统升级的原因。

在当时那个节点上,我们重新的审视了全量合成这一思路,再次对方案原理本身产生怀疑,它在Art平台上面带来了以下几个代价:

  1. OTA后黑屏问题;这里或许可以通过lLoading界面实现,但并不是很好的方案;
  2. Rom体积问题;一个10M的Dex,在Dalvik下odex产物只有11M左右,但在Art平台,可以达到30多M;
  3. Android N的问题;Android N在混合编译上努力,被补丁全量合成机制所废弃了。这是因为动态加载的Dex,依然是全量编译。

回想起来,Qzone方案它只把需要的类打包成补丁推送,在Art平台上可能导致补丁很大,但它肯定比全量合成10M的Dex少很多很多。在此我们提出分平台合成的想法,即在Dalvik平台合成全量Dex,在Art平台合成需要的Dex

DexDiff算法已经非常复杂,事实上要实现分平台合成并不容易。

主要难点有以下几个方面:

  • small dex的类收集;什么类应该放在这个小的Dex中呢?
  • ClassN处理;对于ClassN怎么样处理,可能出现类从一个Dex移动到另外一个Dex?
  • 偏移二次修正; 补丁包中的操作序列如何二次修正?
  • Art.info的大小; 为了修正偏移所引入的info文件的大小?

庆幸的是,面对困难我们并没有畏惧,最后实现了这一套方案,这也是其他全量合成方案所不能做到的:

  1. Dalvik全量合成,解决了插桩带来的性能损耗
  2. Art平台合成small dex,解决了全量合成方案占用Rom体积大, OTA升级以及Android N的问题
  3. 大部分情况下Art.info仅仅1-20K, 解决由于补丁包可能过大的问题

事实上,DexDiff算法变的如此复杂,怎么样保证它的正确性呢?微信为此做了以下三件事情:

  1. 随机组成Dex校验,覆盖大部分case;
  2. 微信200个版本的随机Diff校验, 覆盖日常使用情况;
  3. Dex文件合成产物有效性校验,即使算法出现问题,也只是编译不出补丁包。

每一次DexDiff算法的更新,都需要经过以上三个Test才可以提交,这样DexDiff的这套算法已完成了整个闭环。

四. 其他技术挑战

在实现过程,我们还发现其他的一些问题:

  1. Xposed等微信插件; 市面上有各种各样的微信插件,它们在微信启动前会提前加载微信中的类,这会导致两个问题:

    a. Dalvik平台:出现Class ref in pre-verified class resolved to unexpected implementation的crash;

    b. Art平台:出现部分类使用了旧的代码,这可能导致补丁无效,或者地址错乱的问题。

    微信在这里的处理方式是若crash时发现安装了Xposed,即清除并不再应用补丁。

    1. Dex反射成功但是不生效;部分三星android-19版本存在Dex反射成功,但出现类重复时,查找顺序始终从base.apk开始。
      微信在这里的处理方式是增加Dex反射成功校验,具体通过在框架中埋入某个类的isPatch变量为false。在补丁时,我们自动将这个变量改为true。通过这个变量最终的数值,我们可以知道反射成功与否。

Tinker v1.0总结

一. 关于性能

通过Tinker v1,0的努力,我们解决了Qzone方案的性能问题,得到一个符合“高可用”性能要求的补丁框架。

  • 它补丁包大小非常少,通常都是10k以内;
  • 对性能几乎没有影响, 2%的性能影响主要原因是微信运行时校验补丁Dex文件的md5导致(虽然文件在/data/data/目录,微信为了更高级别的安全);
  • Art平台通过革命性的分平台合成,既解决了地址偏移的问题,占Rom体积与Qzone方案一致。

二. 关于成功率

也许有人会质疑微信成功率为什么这么低,其他方案都是99%以上。事实上,我们的成功率计算方式是:

应用成功率= 补丁版本转化人数/基准版本安装人数

即三天后,94.1%的基础版本都成功升级到补丁版本,由于基础版本人数也是持续增长,同时可能存在基准或补丁版本用户安装了其他版本,所以本统计结果应略为偏低,但它能现实的反应补丁的线上总体覆盖情况。

事实上,采用Qzone方案,3天的成功率大约为96.3%,这里还是有很多的优化空间。

三. Tinker v2.0-稳定性的探寻之路

在v1.0阶段,大部分的异常都是通过厂商反馈而来,Tinker并没有解决“高可用”下最核心的稳定性与兼容性问题。我们需要建立完整的监控与补丁回退机制,监控每一个阶段的异常情况。这也是Tinker v2.0的核心任务,由于边幅问题这部分内容将放在下一篇文章。


关注Tinker,来Github给我们star吧

https://github.com/Tencent/tinker

作者:Tencent_Bugly 发表于2016/10/8 9:56:36 原文链接
阅读:17 评论:0 查看评论

【腾讯Bugly干货分享】聊一聊微信“小程序”

$
0
0

Dev Club 是一个交流移动开发技术,结交朋友,扩展人脉的社群,成员都是经过审核的移动开发工程师。定期会举行嘉宾分享,话题讨论等活动。

本期讨论话题为:聊一聊微信“小程序”。

引言

2016年9月21日,微信开始陆续对外发送小程序(应用号)内测邀请,而小程序即被外界广为关注的微信应用号。

微信小程序是什么?

小程序是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或者搜一下即可打开应用。也体现了“用完即走”的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。

本期,Dev Club 召集了近500位一线移动开发人员,大家畅聊对微信小程序的看法。

下面是本期讨论内容整理:

一、 小程序有什么优势?

1. 依托于微信的大流量

@kevinkong:我觉得小程序最大的好处就是,获取流量会更容易了。

@Ben:这个入口不得不占啊,应该所有的App厂商都会涉足应用号开发吧?大应用也不能完全忽视这个流量入口,拉新实在是太方便了。

@承香墨影:流量依托微信,确实可以更容易获得。已有成熟产品的公司,可能也会抢占,毕竟你不占可能就落后了。

2. 相对较低的开发和运营推广成本

@M君:我认为对于中小企业主来说,应该算是一个好消息

@kevinkong:一个应用前期刚刚开始的时候,先做一个应用号,获取用户的成本相对低一些,先试试看。尤其是刚开始,应用号还不像公众账号那么多的时候,看看自己的产品是否真的能有用户价值。

@就是我啊:对哈,小程序可以作为验证市场的快速原型。

@承香墨影:其实还是看效果,而且对于一切初创来说,从0到1,可能会选择应用号。有些小团队,维护iOS和Android两个端,还要保持同步更新,很难做的很好的。

@张楷:
1.对于用户:我觉得那种小应用或者相对于原来的公共账号可能用户体验会好很多。而且获取成本也低一些。
2.对于开发者,尤其是小的开发者来说试错成本低,开发成本也低,推广成本也低一些吧(比如Android市场这么多,现在只要对接微信就好了。但是可能强势渠道)

3. 近乎于原生的性能体验

@傑丶:不是说H5的体验没有原生的好?总感觉H5的不是很友好。

@空空一筑:小程序如果是mini rn有点不好,以后有H5端可能要2套代码。

@Ben:据说是原生渲染,那体验问题就不必太担心了。

@土豆:我刚看了demo效果很好!

@jasonchqian:确定是native做渲染吗?

@h3r3x3:是类react native,微信自己写的解释引擎,确定是native。解析所谓的wxtlm wxcss,然后映射到微信提供的原生组件。相当于wx给你写组件,肯定稳定。

二、小程序适合哪些类型的应用?

@子嘉:做游戏!

@赵洪武:游戏明令禁止…

@iWater:安静地购物的小清新应用。

**@M君:**web资讯类的,小型的。像知乎啊,豆瓣啊,简书啊,多看啊,微信读书啊,应该都可以搞小程序。

@夜尽天:资讯也不太可能,有公众号和服务号了。

@承香墨影:工具可能不合适,工具还是原生的靠谱些。

@就是我啊:小工具嘛。航班查询,微信订票之类的。

@kevinkong:比如今天跟朋友聊的,像航旅纵横,天气类的应用,主打低频刚需的应用。

@徐春:用户的角度讲,高频的会去下载应用,低频的用这种小应用就足够了,比如订机票之类的。

@h3r3x3:确实,低频应用单一安装没有价值了。

@宋亮:我是快递行业的,我们有很多扫描用pda,是android,感觉好像目前小程序还不能满足我们业务需求。

三、小程序对原生APP的影响和冲击?

@elikong:我觉得不会有影响,先从应用号获得流量,然后导流到app。

@土豆:针对小公司我觉得还是挺不错的,大公司应该会用但是不会抛弃原生吧。

**@巫山老妖:**H5刚出来的时候就已经开始要说取代原生app,直到现在app还活得好好的,微信应用号出来可能会带来一定影响,但影响真的有这么大?

@宋亮:感觉小程序只会小众范围使用,冲击不了原生的地位。

@Ben:我觉得应用号会是传统App的一个补充,并不能完全替代,说App已死为时尚早

**@就是我啊:**PC时代各个浏览器都做过”App in App”的扩展,但是也并没有多火爆呀。

@h3r3x3:功能要依附在微信,不可能替代原生App的,比如直播类,工具类。

@kevinkong:对于用户来说,对于一些低频刚需的应用,其实真的没有必要装一个app。

@Jon:我觉得应用号会替代掉一下交互相对简单的app。可以减少简单内容App开发者的工作量,不需要再去适配那么多终端。

四、关于小程序的痛点和解决方案猜想

1. 聊天与小程序之间的切换

@承香墨影:有个问题:如何在使用小程序的时候继续聊微信?现在公众账号里阅读的时候,根本没有办法聊天。但是如果使用其他App,我可以切回来继续聊,或者用通知条去快捷回复。如果能进入小程序的时候,给人感觉是个独立的App就好了。在任务管理器里看着像两个App,可以分别清理。

@就是我啊:为了沉浸体验,我觉得不会允许在使用应用号的时候继续聊天。要么用小程序,要么用聊天。或许微信会提供一个“Home键”,小程序“退后台”。

@h3r3x3:微信对小应用应该会开启单一进程,在历史纪录里面看起来是两个app。

@iWater:这个大问题无法解决的话,都不想用小程序了,层级太多。安卓还好办,iOS 呢?估计短期iOS解决不了多窗口的问题,所以小程序只能小,时间长了影响聊天。

@waterstar:可以multi window吧。

2. 缓存和离线使用

@宋亮:可以离线保存数据吗?没有网络的时候,也能打开操作。

@夜尽天:可以离线

@iWater:缓存数据,一定得给个好用一点的缓存清理工具啊,不然大家都缓存了,空间没了。

@kevinkong:是啊,现在微信占用的空间,就已经很大了。

@承香墨影:痛点肯定都会解决的。

3. 其他技术疑点

@万松:小程序对于原生机器上硬件资源的使用不知如何限制的? 微信下多个小程序可以多开还是独占?相互间有无影响呢。

@就是我啊:不过,假如小程序崩溃了或者有严重的性能问题会影响到微信本身的体验吗?

@juliandai:对哦,小程序会导致微信crash吗?如果写的不好的话…

@JalenChen:小应用导致ANR了会不会很尴尬…如果是子进程就不会导致微信Crash。

**@丿Ace_Seong:**android的话应该会单独分一个进程给它吧。

@Ben:渲染出问题还是会把微信弄挂的,小程序同时运行可能性不大,估计类似iOS这样,一次只能一个小程序前台运行

@空空一筑:不过微信应该会控制的很好。

@最火的man:本身微信都占用内存较高,微信会不会给小应用的内存也比较少。

@川川:这个不需要只运行一个吧,小程序对系统的压力跟H5差不了多少吧。

@沈治国:那得看微信的加载机制怎么做了。

@kevinkong:小应用的内存,也算在微信里面的化,不是很容易被Kill掉。

@iWater:估计用得好不会比WebView用的内存更多。

@quabqi:估计一次只能开一个小程序,退出就清理。

五、小程序的利弊分析

1. 使用小程序的潜在风险

@徐春:大家没感觉到支付宝早就有类似的功能了吗?里边集成了很多合作方的应用,但是问题是类似于微信和支付宝这种,给公司带来流量的同时,占比太多会对公司战略造成影响。自己的命脉放在别人手里总是不好的,哪天关系没处好,自己损失就大了,所以会影响到公司的战略。

@war:在大数据时代,使用小程序,信息安全是个问题,很多公司不会愿意自己的信息被掌握在他人手上。这种依赖于第三方平台的,就像把刀架在自己脖子上一样。

@宋亮:嗯对,很多涉及到敏感信息,未必愿意弄到微信里。

@Ben:腾讯现在还是很开放的,这点不用太担心吧。

@承香墨影:别说什么很开放,现在淘宝的分享还进不来呢。

@空空一筑:特别是和腾讯有竞争关系的,更不敢放了。不太相信什么开放,现在开放是因为没有竞争。网易云音乐以前也一直分享不过来,现在好像可以了。

@M君:小程序的推出,应该会比之前更加开放。

@kevinkong:我觉得还是权衡利弊吧,毕竟现在获取流量成本太高了。

2. 对于企业和创业者的价值

@承香墨影:其实还是看功能,很多app如果只是一些浏览,消息的展示,对硬件的api没有要求的,小程序也是个不错的选择。之前介绍的就是说:好的产品,就应该即插即用,用完就走。

@巫山老妖:微信小程序,为什么说小呢?一方面觉得它是能嵌入到微信,很方便,能更快的获取流量,解决了以前app推广成本大的痛点,另一方面能够让创业者更轻便的试错,这样来看确实是个机会。

@M君:成本低,效率快,又跨平台,运营人员一定很开心

@iWater:总之如果没有推送,我就用小程序不用公众号,不过公众号大家不会放弃的,所以可能会越来越碎片,订阅号服务号应用号各来一个。不过小程序会比网页好,在本地,又有缓存,回去可以比较快。

@徐春:对中小企业绝对是个很好的平台,降低了运营和研发成本,这点毋庸置疑,这样的话对原生App开发者的需求量自然会降低,但是对于大公司来讲,影响不大。

@kevinkong:
我觉得应用号肯定是有机会的,以前做服务号比较早的,粉丝量大的号,光靠广点通的广告,每个月收入就非常吓人的。
还要搞清楚,为什么要做小应用?什么阶段的公司适合做小应用?做小应用的目的是什么?
我个人的理解是:
1. 刚需高频的应用,用户可以在应用号中,体验到应用的核心价值,然后引导下载原生应用。
2. 刚需低频的应用,应用号应该就够用了。
3. 非刚需又低频的应用,做个应用号,还能少浪费点投资人的钱。

企业最终的目的是赚钱,应用号感觉只是一个过度的阶段,积累了一定的用户量,自己的产品用户价值也能得到认可了,最后还是要做一个原生的app。

六、对移动开发者的机会与挑战

@土豆:针对市场的开发者来讲,还是有一点冲击力。可能对前端工程师的要求更高了。

@张楷:不过对于开发者来说也是一个赚钱的机会。赶紧去学习js…

@空空一筑:不会点js,都不好意思说是做移动开发的。

@川川:就算不做小程序,也要学学JS吧,要不跟前端没共同语言呀。而且,我猜测还会影响到中等水平原生开发者的工资,就像当年的win程序员一样

@承香墨影:嗯,所有能用js重构的程序,最终都会用js重新写一遍,哈哈!

**@monkeyneye:**H5和js还有空间优化体验和性能。

@M君:感觉对于原生开发者来说是资本寒冬上又浇了盆冷水。

@iWater:微信平台上小程序,用js写比H5有更好的原生体验,虽然控件有限。而且它帮你解决了兼容性的问题。

@Ben:框架微信已经封装的很好了,所以上手难度不会太大。

@徐春:其实没有应用号之前我也一直想好好看看JS,不然RN都没法玩转,至于危机感嘛,我相信公司是会两条腿走路的,很少有公司会彻底放弃原生app开发。做技术的都要不断学习的,不然被淘汰只是时间问题。

总结

以上就是大家对微信“小程序”初步的想法和见解了,介于目前微信“小程序”仍处于邀请制的内测阶段,Dev Club 的同学们也只能根据已公布的信息进行一些分析和猜想,很多企业、创业者、开发者们也都在持续关注着微信“小程序”下一步的发展。

小程序在9月21日邀请内测开启后,为了让更多开发者了解平台新能力,9月23日微信公众平台又发布了《公众平台小程序文档和工具》,其中包括了:

  1. 小程序开发文档
  2. 小程序设计指南
  3. 小程序开发者工具

让尚未获得内测邀请的开发者,可以先通过以上文档,了解微信公众平台提供的新能力,并且可以使用提供的开发者工具开发小程序和模拟运行效果。

未来“小程序”最终将会以何种形式呈现,让我们拭目以待!

作者:Tencent_Bugly 发表于2016/10/8 10:00:44 原文链接
阅读:31 评论:0 查看评论

Handler的常用场景总结

$
0
0

1、安卓为什么只能通过消息机制更新UI呢?

最根本的目的就是解决多线程并发问题。(多个线程同时执行一个任务)

假如在一个Activity中,有多个线程去更新UI,并且都没有加锁机制,那就会造成更新UI错乱。如果对更新UI的操作都进行加锁处理,就会造成性能下降。使用消息机制,根本不用关系多线程的问题,因为更新UI的操作,都是在主线程的消息队列当中去轮询处理的

2、Handler是什么?

Handlerandroid提供的一套更新UI的机制,也是消息处理机制,我们可以用它发送消息,也可以通过它处理消息。

更新UI,传递消息,处理消息。

3、默认情况下Handler会与其被定义时所在线程的Looper绑定,比如,Handler在主线程中定义,那么它是与主线程的Looper绑定。

注意Handler的构造函数,New Handler()什么参数都不需要

/**
 * Created on 2016/9/23.
 * Author:crs
 * Description:测试消息机制:主要用于线程间通讯,注意Handler类的构造函数
 * 1)一个完整的消息要包括消息标识和消息内容.(消息对象里面包含消息消息标识和消息内容;如果是空消息,只有消息标识没有消息体)
 *
 * 2)只有在原生线程(主线程)才能更新UI,在子线程更新UI会报错。
 *
 * 3)使用消息机制更新UI的模板性代码:创建Handler实例,在子线程中调用post方法,就两步操作。
 * handler.post(r): r是要执行的任务代码,意思就是说r的代码实际是在UI线程执行的,可以写更新UI的代码。
 *
 * 4)使用消息机制更新UI的模板性代码:创建Handler实例,重写handleMessage()方法,创建消息对象并进行信息传递即可。
 * obtainMessage() Message类 arg1 arg2 sendMessage(message实例) sendToTarget(); sendEmptyMessage()
 *
 * 5)使用消息机制实现图片轮播:创建Handler实例,创建Runnable接口实例,调用postDelayed(runnable实例,毫秒值);
 * removeCallbacks(Runnable),就这几步常规的操作
 *
 * 6)创建Handler的时候,指定callBack;可以通过CallBack截获Handler传递的消息.
 */

案例一:安卓中更新UI的四种方式

package com.crs.demo.ui.handler;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.crs.demo.R;
import com.crs.demo.base.BaseActivity;

/**
 * Created on 2016/10/9.
 * Author:crs
 * Description:安卓中四种更新UI的方式
 * 1)handler.post()
 * 2)handler.sendMessage()
 * 3)runOnUIThread()
 * 4)View.post
 */
public class TestUpdateUIActivity extends BaseActivity {

    private static final int SUCCESS_GET_DATA = 1;
    private Handler mHandler = new Handler();
    private Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case SUCCESS_GET_DATA: {
                    tv_activity_test_update_ui.setText("使用handler.sendMessage()更新");
                }
                break;
            }
        }
    };
    private TextView tv_activity_test_update_ui;

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

        initViews();
    }

    private void initViews() {
        tv_activity_test_update_ui = findView(R.id.tv_activity_test_update_ui);
        Button btn_test_handler1 = findView(R.id.btn_test_handler1);
        Button btn_test_handler2 = findView(R.id.btn_test_handler2);
        Button btn_test_handler3 = findView(R.id.btn_test_handler3);
        Button btn_test_handler4 = findView(R.id.btn_test_handler4);

        //第一种方式:在子线程中调用runOnUIThread()更细UI
        btn_test_handler1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                tv_activity_test_update_ui.setText("调用runOnUIThread方法更新");
                            }
                        });
                    }
                }).start();

            }
        });

        //第二种方式:使用View的post()更新UI
        btn_test_handler2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv_activity_test_update_ui.post(new Runnable() {
                    @Override
                    public void run() {
                        tv_activity_test_update_ui.setText("使用View的post()更新UI");
                    }
                });

            }
        });

        //第三种方式:使用Handler的post()更新UI
        btn_test_handler3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                tv_activity_test_update_ui.setText("Handler的post()更新UI");
                            }
                        });
                    }
                }).start();
            }
        });

        //第四种方式:使用Handler的sendMessage()更新UI
        btn_test_handler4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //Message message = new Message();
                        //Message message = myHandler.obtainMessage();
                        myHandler.sendEmptyMessage(SUCCESS_GET_DATA);
                    }
                }).start();
            }
        });
    }
}
案例二:拦截Handler传递的消息

package com.crs.demo.ui.handler;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;

import com.crs.demo.R;
import com.crs.demo.base.BaseActivity;
import com.crs.demo.utils.ToastUtils;

/**
 * Created on 2016/10/8.
 * Author:crs
 * Description:测试创建Handler指定CallBack
 */
public class TestCallBackActivity extends BaseActivity {

    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            //可以通过CallBack截获Handler传递的消息,返回值表示是否截获消息,false表示不截获。
            ToastUtils.showShort(TestCallBackActivity.this, "1");
            //如果设置为true,就表示截获消息,后面的handleMessage()就不在继续执行了,即2不会被打印了。
            return false;
        }
    }) {
        @Override
        public void handleMessage(Message msg) {
            ToastUtils.showShort(TestCallBackActivity.this, "2");
        }
    };

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

        initViews();
    }

    private void initViews() {
        Button btn_activity_test_call_back = findView(R.id.btn_activity_test_call_back);
        btn_activity_test_call_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mHandler.sendEmptyMessage(1);
            }
        });
    }
}
案例三:使用Handler实现图片轮播效果

package com.crs.demo.ui.handler;

import android.os.Bundle;
import android.os.Handler;
import android.widget.ImageView;

import com.crs.demo.R;
import com.crs.demo.base.BaseActivity;

/**
 * Created on 2016/10/8.
 * Author:crs
 * Description:使用消息机制实现图片轮播效果
 * 关键是练习消息机制的使用
 */
public class TestPostDelayedActivity extends BaseActivity {

    int[] image = {R.drawable.iv_1, R.drawable.iv_2, R.drawable.iv_3};
    int index;
    private ImageView iv_test_post_delayed;

    //1)创建Handler实例
    private Handler mHandler = new Handler();
    //2)创建Runnable实例
    private MyRunnable myRunnable = new MyRunnable();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_post_delayed);
        iv_test_post_delayed = findView(R.id.iv_test_post_delayed);

        //3)调用此方法实现定时器效果
        mHandler.postDelayed(myRunnable, 1000);
    }

    //实现接口
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            index++;
            //余数只有三种类型0、1、2
            index = index % 3;
            //每隔多少秒要做的事(业务逻辑)
            iv_test_post_delayed.setImageResource(image[index]);
            mHandler.postDelayed(this, 1000);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        //清除此定时器效果
        mHandler.removeCallbacks(myRunnable);
    }
}
案例四:自定义与线程相关的handler

package com.crs.demo.ui.handler;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import com.crs.demo.R;
import com.crs.demo.base.BaseActivity;
import com.crs.demo.utils.LogUtils;

/**
 * Created on 2016/10/9.
 * Author:crs
 * Description:自定义与线程相关的Handler
 */
public class TestCustomHandlerActivity extends BaseActivity {

    private static final String TAG = "TestCustomHandler";
    private MyThread myThread;
    //创建主线程Handler
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            LogUtils.i(TAG, Thread.currentThread() + "主线程");
        }
    };


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



        myThread = new MyThread();
        myThread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //主线程中的Handler
        mHandler.sendEmptyMessage(1);

        //子线程中的Handler
        myThread.myHandler.sendEmptyMessage(2);
    }


    class MyThread extends Thread {
        private Handler myHandler;

        @Override
        public void run() {
            super.run();
            Looper.prepare();
            myHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    LogUtils.i(TAG, Thread.currentThread() +"子线程");
                }
            };
            Looper.loop();
        }
    }
}

Handler的原理是什么?

1)Handler封装了消息的发送,主要包括消息发送给谁。

2)Looper:内部包含一个消息队列,也就是MessageQueue,所有Handler发送的消息都会走向这个消息队列。

Looper类中有一个loop()方法,它不断的从MessageQueue中取消息,如果有消息就处理消息,如果没有消息就阻塞。

3)MessageQueue就是一个消息队列,可以添加消息、移除消息。

4)Handler内部会和Looper进行关联,在Handler内部可以找到Looper。使用Handler发送消息,其实就是向MessageQueue队列中发送消息。

总结:handler负责发送消息,Looper负责接收Handler发送的消息,并直接发消息传回给handler自己、MessageQueue就是一个存储消息的容器、looper是循环器。


消息标识与消息内容(int类型和对象类型)

如何获取一个Message对象?

Handler的两种作用:发送消息、传递消息.

MessageHandler之间的关系  sendToTarget()

Handler移除一个事件,停止事件的执行removeCallBack(runnable实例)

发送一个空消息的目的是什么原理是什么

截获Handler发送的消息  new CallBack()

作者:chenrushui 发表于2016/10/9 10:37:58 原文链接
阅读:41 评论:0 查看评论

openHMD-simple代码分析(2/2)

$
0
0

openHMD-simple代码分析(2/2)

接上篇内容,这里主要以oculars在openHMD中的提交,来分析oculars的DK1/DK2在openHMD中是如何运行的。
simple代码的主流程看上篇文章”openHMD-simple代码分析(1/2)”。

打开设备:ohmd_list_open_device

    //打开设备列表中第一个设备
    ohmd_device* hmd = ohmd_list_open_device(ctx, 0);

    if(!hmd){
        printf("failed to open device: %s\n", ohmd_ctx_get_error(ctx));
        return -1;
    }

ohmd_list_open_device函数:

ohmd_device* OHMD_APIENTRY ohmd_list_open_device(ohmd_context* ctx, int index)
{
    ohmd_device_settings settings;
    //设置设备数据的更新方式:
    //false:需要手动调用ohmd_ctx_update来更新数据
    //true:创建线程,调用设备的update方法来更新数据(如rift.c里的update_devices)
    settings.automatic_update = true;

    return ohmd_list_open_device_s(ctx, index, &settings);
}

ohmd_list_open_device_s函数:

ohmd_device* OHMD_APIENTRY ohmd_list_open_device_s(ohmd_context* ctx, int index, ohmd_device_settings* settings)
{
    ohmd_lock_mutex(ctx->update_mutex);
    //轮询所有设备
    if(index >= 0 && index < ctx->list.num_devices){

        ohmd_device_desc* desc = &ctx->list.devices[index];
        ohmd_driver* driver = (ohmd_driver*)desc->driver_ptr;
        ohmd_device* device = driver->open_device(driver, desc);

        if (device == NULL)
            return NULL;
        //填充device参数值
        device->rotation_correction.w = 1;

        device->settings = *settings;

        device->ctx = ctx;
        device->active_device_idx = ctx->num_active_devices;
        ctx->active_devices[ctx->num_active_devices++] = device;

        ohmd_unlock_mutex(ctx->update_mutex);

        if(device->settings.automatic_update)
            //创建线程,用来更新上报的数据。
            ohmd_set_up_update_thread(ctx);

        return device;
    }

    ohmd_unlock_mutex(ctx->update_mutex);
    //如果没有设备则返回错误
    ohmd_set_error(ctx, "no device with index: %d", index);
    return NULL;
}

其中ohmd_set_up_update_thread会创建更新数据的线程ohmd_update_thread,这个函数主要用来读取sensor数据,调用fusion算法,将raw数据处理为四元素。

static void ohmd_set_up_update_thread(ohmd_context* ctx)
{
    if(!ctx->update_thread){
        ctx->update_mutex = ohmd_create_mutex(ctx);
        ctx->update_thread = ohmd_create_thread(ctx, ohmd_update_thread, ctx);
    }
}
static unsigned int ohmd_update_thread(void* arg)
{
    ohmd_context* ctx = (ohmd_context*)arg;
    //ctx->update_request_quit 控制循环是否结束,一般在ohmd_ctx_destroy里设置为true
    while(!ctx->update_request_quit)
    {
        ohmd_lock_mutex(ctx->update_mutex);
        //调用所有处于激活状态的设备的update方法来循环更新数据(如rift.c里的update_device)
        for(int i = 0; i < ctx->num_active_devices; i++){
            if(ctx->active_devices[i]->settings.automatic_update && ctx->active_devices[i]->update)
                ctx->active_devices[i]->update(ctx->active_devices[i]);
        }

        ohmd_unlock_mutex(ctx->update_mutex);
        //休眠一段时间,释放CPU
        ohmd_sleep(AUTOMATIC_UPDATE_SLEEP);
    }

    return 0;
}

ctx->active_devices[i]->update(ctx->active_devices[i])实际上是调用到具体设备中的数据更新的接口中,这里以oculars的DK1/DK2为例进行说明数据的更新

static void update_device(ohmd_device* device)
{
    rift_priv* priv = rift_priv_get(device);
    unsigned char buffer[FEATURE_BUFFER_SIZE];

    //处理心跳包信息:按照keep_alive_interval参数值(默认1秒)为间隔,发送心跳包给HMD
    double t = ohmd_get_tick();
    if(t - priv->last_keep_alive >= (double)priv->sensor_config.keep_alive_interval / 1000.0 - .2){
        // send keep alive message
        pkt_keep_alive keep_alive = { 0, priv->sensor_config.keep_alive_interval };
        int ka_size = encode_keep_alive(buffer, &keep_alive);
        send_feature_report(priv, buffer, ka_size);

        // Update the time of the last keep alive we have sent.
        priv->last_keep_alive = t;
    }

    //处理HMD上报的所有数据
    while(true){
        //通过HID接口读取HMD的数据
        int size = hid_read(priv->handle, buffer, FEATURE_BUFFER_SIZE);
        if(size < 0){
            LOGE("error reading from device");
            return;
        } else if(size == 0) {//数据读完退出
            return; 
        }

        //根据oculars协议,对上报数据进行处理
        if(buffer[0] == RIFT_IRQ_SENSORS){
            handle_tracker_sensor_msg(priv, buffer, size);
        }else{
            LOGE("unknown message type: %u", buffer[0]);
        }
    }
}

其中handle_tracker_sensor_msg函数如下:

static void handle_tracker_sensor_msg(rift_priv* priv, unsigned char* buffer, int size)
{
    //按照oculars的数据协议解析buffer中的数据填充到priv->sensor中
    if(!decode_tracker_sensor_msg(&priv->sensor, buffer, size)){
        LOGE("couldn't decode tracker sensor message");
    }

    pkt_tracker_sensor* s = &priv->sensor;
    //打印调试信息
    dump_packet_tracker_sensor(s);

    //根据采样数设置dt值(一般采样数都在3以内)
    float dt = s->num_samples > 3 ? (s->num_samples - 2) * TICK_LEN : TICK_LEN;

    int32_t mag32[] = { s->mag[0], s->mag[1], s->mag[2] };
    //将上报数据转为浮点数(HMD上报时不能直接上报浮点数,所以先*10000转化为整数上报)
    //这里*0.0001相当于做了还原).
    vec3f_from_rift_vec(mag32, &priv->raw_mag);

    for(int i = 0; i < OHMD_MIN(s->num_samples, 3); i++){
        //将上报数据转为浮点数
        vec3f_from_rift_vec(s->samples[i].accel, &priv->raw_accel);
        //将上报数据转为浮点数
        vec3f_from_rift_vec(s->samples[i].gyro, &priv->raw_gyro);
        //融合算法
        ofusion_update(&priv->sensor_fusion, dt, &priv->raw_gyro, &priv->raw_accel, &priv->raw_mag);

        // reset dt to tick_len for the last samples if there were more than one sample
        dt = TICK_LEN;
    }
}

主要是解析HMD上报的数据,关于ofusion_update融合算法,中间的四元素操作比较复杂,可以看这篇老外的文章IMU指南,需要花点时间琢磨。

数据更新:ohmd_ctx_update

void OHMD_APIENTRY ohmd_ctx_update(ohmd_context* ctx)
{
    for(int i = 0; i < ctx->num_active_devices; i++){
        ohmd_device* dev = ctx->active_devices[i];
        //如果设备对应驱动中没有创建线程实时刷新数据则,通过这里刷新数据。
        //(Oculars DK1是通过线程刷新数据的.
        if(!dev->settings.automatic_update && dev->update)
            dev->update(dev);

        ohmd_lock_mutex(ctx->update_mutex);
        //获取位置信息填充到dev->position中
        dev->getf(dev, OHMD_POSITION_VECTOR, (float*)&dev->position);
        //获取经过融合算法的orient数据填充到dev->rotation中。
        dev->getf(dev, OHMD_ROTATION_QUAT, (float*)&dev->rotation);
        ohmd_unlock_mutex(ctx->update_mutex);
    }
}

获取四元素:ohmd_device_setf/ohmd_device_getf

        float zero[] = {.0, .1, .2, 1};
        //计算QUAT和VECTOR的校准因子correction
        ohmd_device_setf(hmd, OHMD_ROTATION_QUAT, zero);
        ohmd_device_setf(hmd, OHMD_POSITION_VECTOR, zero);
        //利用上面计算的校准因子,来得出最后的四元素并打印出来。
        print_infof(hmd, "rotation quat:", 4, OHMD_ROTATION_QUAT);
作者:dabenxiong666 发表于2016/10/9 10:41:32 原文链接
阅读:33 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>